38 *, n_modes: int, sigma: float) -> Tuple[np.ndarray, np.ndarray]:
39 """Lowest ``n_modes`` passive acoustic frequencies and mode shapes at wavenumber ``m``.
41 ``sigma`` is the shift [rad/s]^2 (i.e. an ``omega^2`` guess) placed near the band of
42 interest — shift placement is not guesswork (Section IV.E): pass ``(2*pi*f_guess)**2``
43 for whatever frequency band you expect from the analytic estimate.
45 Axis regularity (Section III.E) is imposed here, not in ``assemble_passive``: for
46 ``m == 0`` nothing is done (natural condition, automatic from the ``r``-weighted weak
47 form); for ``m >= 1`` the r=0 axis nodes are eliminated (essential p=0 Dirichlet
48 condition) before the solve and their (zero) values are scattered back afterward.
50 Returns ``(freqs_hz, mode_shapes)`` with ``mode_shapes`` shape ``(n_nodes, n_modes)``,
51 sorted by ascending frequency.
53 K, Km, M2 = assemble_passive(mesh, c_sound, m)
54 A = (K + (m * m) * Km).tocsc()
58 free = np.arange(mesh.n_nodes)
60 axis = mesh.axis_nodes()
61 keep = np.ones(mesh.n_nodes, dtype=bool)
63 free = np.nonzero(keep)[0]
67 k =
min(n_modes, A.shape[0] - 2)
69 raise ValueError(
"mesh too coarse for the requested number of modes")
71 vals, vecs = eigsh(A, k=k, M=M, sigma=sigma, which=
"LM")
72 order = np.argsort(vals)
73 vals = np.clip(vals[order], 0.0,
None)
76 freqs_hz = np.sqrt(vals) / (2.0 * np.pi)
78 full_vecs = np.zeros((mesh.n_nodes, vecs.shape[1]))
79 full_vecs[free, :] = vecs
80 return freqs_hz, full_vecs