STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
passive.py
Go to the documentation of this file.
1"""Passive (flame-off, rigid-wall) modal solve — the shift-invert Krylov kernel of
2Section IV.E, specialized to the *linear* case that appears with no flame and no
3boundary impedance.
4
5With ``C = 0`` (rigid walls only) and ``F = 0`` (no active flame), the discrete NLEVP
6(Eq. 11) collapses to
7
8 (K + m^2 Km) p = -lambda^2 M2 p .
9
10Writing ``mu = -lambda^2``, this is an ordinary real-symmetric *generalized* eigenvalue
11problem ``A p = mu M2 p`` with ``A = K + m^2 Km``. Both ``A`` and ``M2`` are symmetric
12positive semi-definite (a lossless rigid-wall cavity cannot amplify or dissipate), so
13``mu >= 0`` and every eigenvalue is purely oscillatory: ``lambda = +/- i*sqrt(mu)``,
14``omega = sqrt(mu)``, ``f = omega / (2 pi)``. This is exactly the passive-mode seeding
15step of Algorithm 2 (paper Section IV.F, step 5) and case V1 of the verification ladder.
16
17We solve it with shift-invert Lanczos (``scipy.sparse.linalg.eigsh``, which is symmetric
18Arnoldi/Lanczos — the real-symmetric specialization of the Krylov-Schur kernel described
19in Section IV.E): factor ``(A - sigma*M2)`` once and iterate on its action, which makes
20the eigenvalues nearest the shift ``sigma`` the *extremal* (best-converging) ones of the
21transformed operator, exactly the mechanism Section IV.E describes for the general
22(non-symmetric, shift-invert Krylov-Schur) case.
23"""
24
25from __future__ import annotations
26
27from typing import Tuple
28
29import numpy as np
30from scipy import sparse
31from scipy.sparse.linalg import eigsh
32
33from engine.stability_hifi.acoustics.assembly import assemble_passive
34from engine.stability_hifi.acoustics.mesh import MeridionalMesh
35
36
37def solve_passive_modes(mesh: MeridionalMesh, c_sound: np.ndarray, m: int,
38 *, n_modes: int, sigma: float) -> Tuple[np.ndarray, np.ndarray]:
39 """Lowest ``n_modes`` passive acoustic frequencies and mode shapes at wavenumber ``m``.
40
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.
44
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.
49
50 Returns ``(freqs_hz, mode_shapes)`` with ``mode_shapes`` shape ``(n_nodes, n_modes)``,
51 sorted by ascending frequency.
52 """
53 K, Km, M2 = assemble_passive(mesh, c_sound, m)
54 A = (K + (m * m) * Km).tocsc()
55 M = M2.tocsc()
56
57 if m == 0:
58 free = np.arange(mesh.n_nodes)
59 else:
60 axis = mesh.axis_nodes()
61 keep = np.ones(mesh.n_nodes, dtype=bool)
62 keep[axis] = False
63 free = np.nonzero(keep)[0]
64 A = A[free][:, free]
65 M = M[free][:, free]
66
67 k = min(n_modes, A.shape[0] - 2)
68 if k < 1:
69 raise ValueError("mesh too coarse for the requested number of modes")
70
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) # numerical noise can make near-zero mu slightly negative
74 vecs = vecs[:, order]
75
76 freqs_hz = np.sqrt(vals) / (2.0 * np.pi)
77
78 full_vecs = np.zeros((mesh.n_nodes, vecs.shape[1]))
79 full_vecs[free, :] = vecs
80 return freqs_hz, full_vecs
#define min(a, b)
Definition Arduino.h:42
Tuple[np.ndarray, np.ndarray] solve_passive_modes(MeridionalMesh mesh, np.ndarray c_sound, int m, *, int n_modes, float sigma)
Definition passive.py:38