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 be the disk of radius in the plane. The undeformed mid-surface is the graph of a shallow surface, meaning ; we warn when exceeds 0.4. The displacement of a mid-surface point has in-plane components , along and and a transverse component along . The Marguerre-von Karman membrane strains are
and the curvatures are those of the plate, . Setting recovers the von Karman plate. Dropping the quadratic terms , , 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 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 , Poisson ratio and density ; the thickness varies. Membrane forces and bending moments are and with
The strain energy is
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 the mass per unit area and the variational derivative,
where 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): gives a frequency-independent decay rate, so that the 60 dB decay time of the low modes is , and the term gives a wave of wavenumber the additional decay rate . For bending waves , 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; and 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 on the projected disk. The shoulder is the junction between the bell and the bow; its radius carries an ovality. The bow coordinate runs from 0 at the rim to 1 at the shoulder, and . With the distance from the (offset) bell centre and , , a ramp that is 1 on the bell plateau and 0 beyond the shoulder, the mid-surface height is
bow, bell, rim warp and hammer dents in that order. The bow exponent gives a cone and a bow that is curved near the rim and flat at the shoulder. The warp amplitudes are for the listed modes with random phases; it vanishes at the shoulder and grows linearly to the rim, so the mean rim height stays . 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
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 in the strains. Nodes are placed on concentric rings of spacing with for a target element size ; ring carries 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 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 .
4.2 Degrees of freedom#
Each node carries five unknowns with the DKT convention and . The state of the shell is the array of shape .
4.3 Membrane part: constant-strain triangle with the discrete initial surface#
On each triangle , and are interpolated linearly from their nodal values, so their gradients are constant: with , the usual CST operators. The initial surface is represented in the same space, that is, and on an element are the CST gradients of the nodal heights , not the analytic slope at the centroid. This choice is deliberate. A rigid tilt of the shell about the axis by an angle displaces a surface point by and (section 4.7); its linearized strain is , which vanishes exactly only if 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 of a typical stiffness; with the discrete slope the residual is at round-off (about in float64). The membrane energy of element is with 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 are interpolated quadratically over the triangle from the three nodal triples, with the mid-side values fixed by imposing the Kirchhoff condition (rotation equals slope) discretely along each edge. The resulting curvature operator , with for the nine bending DOFs of the element, is linear in the area coordinates, so the element stiffness
is integrated exactly by the three-point rule. The bending energy is . 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 ; 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 ), the state has energy to , and the 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 with the gather of the element's nodal values. The internal nodal force is
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 .
4.6 Lumped mass and rotational inertia#
The mass is lumped: element contributes to the , and entries of each of its nodes. The rotational entries receive
where is the physical rotary inertia per unit mass and (default ) 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 with , 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; 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 ( 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 . A tilt about the axis by an angle carries the surface point to first order to , so the mode is , , ; likewise the tilt about is , , . The in-plane components proportional to are what distinguish the curved shell from the plate and are essential: without them a tilt stretches the shell. The six vectors are made -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 leaves a residual force of order . 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 term is discretized in weak form with the CST gradients. The nodal force on is
which is the assembly of . Its power is non-negative, so the term only ever removes energy, and a uniform velocity is undamped by it. The term is diagonal and acts on all five DOFs.
5. Time integration#
Implemented in simulate.py.
5.1 Leapfrog with implicit #
With the diagonal lumped mass, one sub-step of size is
where is the rigid-mode projection of section 5.3. This is the symplectic Euler scheme in , equivalent to the central-difference (Stormer-Verlet) scheme in , second-order accurate and non-dissipative for the conservative part. The term is treated implicitly, which costs nothing because it is diagonal; the term is evaluated explicitly at , which is accurate enough because 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 , with the largest eigenvalue of . Instead of solving that global eigenproblem we use the element bound (Irons; see Hughes 2000): , where is the tangent stiffness of the element at rest and 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 and , so that the radiation buffers decimate exactly to the audio rate. The run is extended by 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, with the -orthonormal rigid modes 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 is about of the displacement itself, so float32 loses a few of its 24 bits per step in 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 that moves only along , with face height , coupled to the shell through springs at patch points . The surface height under point is , where is the barycentric interpolation of the nodal transverse displacements in the containing element and is the height of the initial surface above the exciter's face plane, the best-fit plane through the patch points' values. The penetration and the force at point are
The plate receives on the DOFs of the containing element, distributed with the weights , and the exciter obeys . The exponent is the Hertz law for a hard sphere on a hard surface; to fits felt. The contact spring is conservative (potential ), 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 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 through the strike point, at an angle patch_angle from the radial direction, with equally spaced springs and a given total stiffness . 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 at full stop on a rigid wall, . The set-up log reports, for each hit, the contact duration the exciter would have against a rigid wall,
which for is Hertz's , together with the frequency 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 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 an exciter is inactive: it produces no force and is held far above the shell. At step it is placed at , just above the current surface, with velocity . 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 and the rebound velocity.
6.4 Prescribed force#
A ForceExciter replaces the contact by a force given in advance, a raised cosine of peak and length applied at one point from the hit time :
distributed to the containing element's 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 from a baffled planar radiator is given by the Rayleigh integral, which we discretize with one point source per element:
where is the mean over the element's three nodes of the normal acceleration , being the unit normal of the rest surface at node (the area-weighted mean of the adjacent elements' constant slopes, pointing to the side), and the distance from the element centroid, placed on the curved surface at height , to the listener. The acceleration is taken from the integrator directly: for the leapfrog scheme is the exact central second difference of at step , 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 and lump). At the bell, where the slope reaches 0.39, the normal component differs from by several percent, which is why we use it rather than .
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 replaced by the shortest path over the rim, over 720 rim points, weighted by back_diffraction_gain, and subtract it: . 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 in sub-steps, , 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 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 , 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 , 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 . We use a Kaiser-windowed sinc spanning audio samples ( sub-steps) with and its dB point at : flat to about , dB at , below dB from . It is applied inside the compiled time loop through accumulator slots per node: at sub-step the node adds to slot , which belongs to audio sample ; 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 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 is centred on ; the price is that the newest samples are not final, which is why the run is extended by samples (section 5.2) and why a partial save stops 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 bytes per simulated second, about 0.4 GB per second at the default mesh. The per-sample scale keeps the quantisation noise at 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 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 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 ; 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 alone, and the membrane strain uses the linear interpolation of 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 , 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#
- Batoz, J.-L., Bathe, K.-J., Ho, L.-W. (1980). A study of three-node triangular plate bending elements. International Journal for Numerical Methods in Engineering 15, 1771-1812.
- Marguerre, K. (1938). Zur Theorie der gekrummten Platte grosser Formanderung. Proceedings of the 5th International Congress for Applied Mechanics, 93-101.
- Bilbao, S. (2009). Numerical Sound Synthesis: Finite Difference Schemes and Simulation in Musical Acoustics. Wiley. (Plate and shell schemes; the , loss model.)
- Chaigne, A., Touze, C., Thomas, O. (2005). Nonlinear vibrations and chaos in gongs and cymbals. Acoustical Science and Technology 26, 403-409.
- Touze, C., Bilbao, S., Cadot, O. (2012). Transition scenario to turbulence in thin vibrating plates. Journal of Sound and Vibration 331, 412-433.
- Ducceschi, M., Touze, C. (2015). Modal approach for nonlinear vibrations of damped impacted plates: application to sound synthesis of gongs and cymbals. Journal of Sound and Vibration 344, 313-331.
- Hughes, T. J. R. (2000). The Finite Element Method: Linear Static and Dynamic Finite Element Analysis. Dover. (Element eigenvalue bound for explicit time stepping.)
- Hairer, E., Lubich, C., Wanner, G. (2006). Geometric Numerical Integration. Springer. (Symplectic Euler and Stormer-Verlet.)
- Pierce, A. D. (1989). Acoustics: An Introduction to Its Physical Principles and Applications. Acoustical Society of America. (Rayleigh integral.)
12. Symbols and where to find them#
| symbol | meaning | configuration / code |
|---|---|---|
| Young's modulus, Poisson ratio, density | Material.E, .nu, .rho |
|
| loss parameters | Material.sigma0, .sigma1 |
|
| , | mid-surface height, thickness | CymbalProfile.z0, .thickness |
| membrane stiffness, bending rigidity, DKT stiffness of element | ShellModel.Cm_e, .D_e, .Ke |
|
| CST gradient operators | Mesh.Gx, .Gy |
|
| , , | lumped mass, rotational inertia, its scaling factor | ShellModel.M, .I_e, Numerics.rot_inertia_factor |
| rigid-body modes | ShellModel.Rm |
|
| , , | stability bound, sub-step, sub-steps per sample | ShellModel.omega_max, Simulator.dt, .substeps |
| contact stiffness, exponent, springs per patch | ContactPatch.k_total, .alpha, .n_points |
|
| exciter face height and velocity | state entries zs, vs |
|
| Rayleigh front and diffracted back pressure | SimulationResult.pressure_rayleigh, .pressure_unbaffled |
|
| , | nodal normal of the rest surface, normal acceleration | ShellModel.normal_node, SimulationResult.acceleration |
| , | decimation kernel, its span in audio samples | decimation.decimation_kernel, DECIMATION_SPAN |