The acoustic operator is \mathbf A(\omega)=\mathbf J_{\mathrm{alg}}+i\omega\mathbf M+\mathbf P(\omega)+\mathbf S(\omega). This notebook demonstrates the storage block \mathbf M, newly populated by a single new element: the cavity.
A cavity(V) is a wall to the mean flow (\dot m=0) and a compliance to acoustics: its enclosed gas compresses isentropically, storing energy with lumped compliance
C=\frac{V}{\overline\rho\,\overline c^{\,2}},
which populates \mathbf M with the single coefficient V/\overline c^{\,2} on the cavity’s mass row.
The Helmholtz resonator itself is not a new element: it is composed from the existing primitives: a junction (the tee), a short duct (the neck inertance L_a=\overline\rho\,\ell/A_n), and the new cavity (the compliance C). The resonance
Build the simplest storage network: a driven port, a neck duct, and the cavity dead-end (inlet → neck duct → cavity). The cavity blocks the mean flow (\dot m=0, exactly a wall), and its finite volume contributes one entry to \mathbf M: the compliance V/\overline c^{\,2} on the cavity’s mass row.
def neck_cavity(volume, neck_len, neck_area): net = nefes.Network(GAS) inlet = net.add(cat.total_pressure_inlet(P0, T0)) # quiescent driver, pins the pressure level neck = net.add(cat.duct(neck_len)) cav = net.add(cat.cavity(volume)) net.connect(inlet, neck, neck_area) e_cav = net.connect(neck, cav, neck_area) sol = net.solve()assert sol.convergedreturn sol, cav, e_cavsol1, cav_node, e_cav = neck_cavity(V_CAV, L_NECK, A_NECK)# mean flow: the cavity is a wall -> mdot = 0 everywhereprint(f"max |mdot| in the network = {np.max(np.abs(sol1.field('mdot'))):.2e} kg/s (cavity blocks the mean flow)")# the cavity's finite volume is a compliance C = V / c^2: the single coefficient it stamps into# the storage block M on its own mass row.c_cav = sol1.edge(e_cav)["c"]print(f"\ncavity compliance C = V / c^2 = {V_CAV / c_cav**2:.6e}")
max |mdot| in the network = 0.00e+00 kg/s (cavity blocks the mean flow)
cavity compliance C = V / c^2 = 1.659200e-08
Result. The mean flow is quiescent (\dot m\to0: the cavity is a wall), and its finite volume contributes exactly one coefficient, C=V/\overline c^{\,2}, on the cavity’s mass row. That single entry is the compliance: combined with the neck duct’s inertance it produces the resonance demonstrated next.
Part 2: the Helmholtz resonator, composed from primitives
Now place the neck + cavity as a side branch on a main duct, off a junction:
Network topology
At \overline f_0 the series neck-inertance / cavity-compliance branch impedance Z_b=i(\omega L_a-1/\omega C) passes through zero, shorting the junction pressure: a sharp transmission-loss peak. We compare the Nefes network against the analytic lumped side-branch TL
Result. The Nefes network reproduces the analytic lumped side-branch curve off resonance and peaks at \overline f_0 to within a fraction of a percent. The small residual offset is the compactness correction: the neck has a finite length and carries no end correction yet, so \ell_{\mathrm{eff}}=\ell exactly. The peak is tall and narrow because the resonator is purely reactive (lossless): a damped resonator needs the neck resistance, which is the next modeling step.
Part 3: tuning: \overline f_0\propto 1/\sqrt{V\,\ell}
The resonance is set by the composition, not by any one element. Sweeping the cavity volume moves the peak exactly along \overline f_0=\overline c\sqrt{A_n/(V\ell)}/2\pi: the compliance from \mathbf M and the inertance from the neck duct each enter under the square root.
fig = go.Figure()palette = [COLORWAY[0], COLORWAY[1], COLORWAY[2]]cases_V = [1.0e-3, 2.0e-3, 4.0e-3]for col, V inzip(palette, cases_V): sol, _net, ei, eo = side_branch_hr(V, L_NECK, A_NECK) tl = fns_tl(sol, ei, eo, FREQS) f0v = helmholtz_f0(C0, A_NECK, V, L_NECK) fig.add_trace( go.Scatter(x=FREQS, y=tl, name=f"V = {V*1e3:.0f} L (f0={f0v:.0f} Hz)", line=dict(color=col, width=1.8)) ) fig.add_vline(x=f0v, line=dict(color=col, width=1, dash="dot"))fig.update_layout( title="Tuning the Helmholtz resonator by cavity volume", xaxis_title="Frequency, Hz", yaxis_title="Transmission Loss, dB", width=820, height=460, legend=dict(x=0.99, y=0.99, xanchor="right"),)fig.update_yaxes(range=[0, 60])fig.show()
# verify the scaling law f0 ~ 1/sqrt(V) across the sweep (peak location vs analytic)print(" V [L] f0 analytic f_peak Nefes ratio")for V in [0.5e-3, 1.0e-3, 2.0e-3, 4.0e-3, 8.0e-3]: sol, _net, ei, eo = side_branch_hr(V, L_NECK, A_NECK) tl = fns_tl(sol, ei, eo, FREQS) f0v = helmholtz_f0(C0, A_NECK, V, L_NECK) fpk = FREQS[int(np.argmax(tl))]print(f" {V*1e3:4.1f}{f0v:9.1f}{fpk:10.1f}{fpk/f0v:6.3f}")
Result. Across a 16:1 range of cavity volume the Nefes peak tracks the analytic \overline f_0\propto1/\sqrt V to within the compactness tolerance: confirming that the compliance from \mathbf M and the inertance from the neck duct combine correctly, with no Helmholtz-specific code in any element.
Takeaway. The new ingredients are exactly two: the cavity element and the per-element storage machinery that assembles \mathbf M. With them the operator is complete: \mathbf A=\mathbf J_{\mathrm{alg}}+i\omega\mathbf M+\mathbf P+\mathbf S: and a Helmholtz resonator is just a tee + neck + cavity wired from the existing primitives.
Export for Nemo
The side-branch resonator is available as net2 (a Network) and its converged mean flow as sol2 (a Solution). Save either to a UI-readable YAML: sol2.to_yaml(path) embeds the mean-flow result fields, net2.to_yaml(path) writes the topology only: then open the file in Nemo.
import os, tempfile_out = os.path.join(tempfile.mkdtemp(), "helmholtz_resonator.yaml")sol2.to_yaml(_out) # embeds the mean-flow results; use net2.to_yaml(_out) for topology onlyprint("saved case:", _out)