STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
v1_cylinder.py
Go to the documentation of this file.
1"""Verification case V1: uniform closed-closed cylinder, passive (paper Section VII.A).
2
3Analytic reference (rigid walls at both ends and the side wall):
4
5 f_{m,n,k} = (c / 2*pi) * sqrt( (alpha'_{m,n} / R_c)^2 + (k*pi / L)^2 )
6
7where ``alpha'_{m,n}`` is the n-th positive zero of ``J'_m`` (hard-wall transverse
8eigenvalue — the same zeros used by the lumped model's
9``engine.pipeline.stability.core.TRANSVERSE_EIGENVALUES``, cross-checked here from
10first principles via a 2-D FEM solve rather than assumed) and ``k`` is the number of
11axial half-wavelengths (``k=0`` allowed: a pure transverse mode with no axial
12variation). ``m=n=k=0`` is excluded: it is the trivial uniform-pressure mode, an exact
13zero eigenvalue of an all-Neumann-boundary problem (constant pressure has zero
14gradient, hence zero stiffness) — physically real but not an *acoustic* mode.
15
16This module builds the FEM prediction (via ``eigen.passive.solve_passive_modes`` on a
17``synthetic_uniform_cylinder`` mean flow) and reports it against the closed form.
18"""
19
20from __future__ import annotations
21
22from dataclasses import dataclass
23from typing import List
24
25import numpy as np
26from scipy.special import jnp_zeros
27
28from engine.stability_hifi.eigen.passive import solve_passive_modes
29from engine.stability_hifi.meanflow.spec import synthetic_uniform_cylinder
30
31
32@dataclass
34 m: int
35 alpha: float # J'_m zero used (0.0 for the uniform-in-r branch, m=0 only)
36 k: int # axial half-wavelength count
37 f_analytic_hz: float
38 f_fem_hz: float
39
40 @property
41 def rel_error(self) -> float:
42 return abs(self.f_fem_hz - self.f_analytic_hz) / self.f_analytic_hz
43
44
45def radial_eigenvalues(m: int, n_max: int) -> np.ndarray:
46 """Positive zeros of J'_m relevant to azimuthal wavenumber ``m``.
47
48 For ``m=0`` the r=0 axis condition is Neumann (natural), so alpha=0 (uniform-in-r)
49 is an admissible branch, prepended to the true positive zeros of J'_0. For ``m>=1``
50 the axis condition is Dirichlet (p=0 at r=0, Section III.E), so alpha=0 is not
51 admissible and only the true positive zeros of J'_m apply.
52 """
53 zeros = jnp_zeros(m, n_max) if n_max > 0 else np.array([])
54 if m == 0:
55 return np.concatenate([[0.0], zeros])
56 return zeros
57
58
59def analytic_frequencies(c: float, L: float, R: float, m: int,
60 n_radial_max: int = 2, k_max: int = 2) -> List[ModeComparison]:
61 """All analytic (alpha, k) candidate frequencies for wavenumber ``m``, trivial mode excluded."""
62 out = []
63 for alpha in radial_eigenvalues(m, n_radial_max):
64 for k in range(k_max + 1):
65 if alpha == 0.0 and k == 0:
66 continue # trivial uniform-pressure mode (mu=0), not acoustic
67 f = (c / (2.0 * np.pi)) * np.sqrt((alpha / R) ** 2 + (k * np.pi / L) ** 2)
68 out.append(ModeComparison(m=m, alpha=float(alpha), k=k, f_analytic_hz=float(f), f_fem_hz=float("nan")))
69 out.sort(key=lambda mc: mc.f_analytic_hz)
70 return out
71
72
73def run_case(*, c: float, L: float, R: float, m: int, nx: int, nr: int,
74 n_radial_max: int = 2, k_max: int = 2, n_compare: int = 4) -> List[ModeComparison]:
75 """Run the FEM solve for wavenumber ``m`` and match the lowest ``n_compare`` modes
76 against the analytic candidates (both lists sorted ascending, matched pairwise).
77 """
78 candidates = analytic_frequencies(c, L, R, m, n_radial_max, k_max)[:n_compare]
79 sigma = (2.0 * np.pi * 0.5 * candidates[0].f_analytic_hz) ** 2 # below the lowest true mode
80
81 spec = synthetic_uniform_cylinder(L, R, nx, nr, c_sound=c)
82 freqs_hz, _ = solve_passive_modes(spec.mesh, spec.c, m, n_modes=n_compare + 2, sigma=sigma)
83
84 # Drop near-zero (trivial) modes before matching against the (already trivial-excluded)
85 # analytic candidates; keep the lowest n_compare survivors.
86 f_min_analytic = candidates[0].f_analytic_hz
87 freqs_hz = np.sort(freqs_hz[freqs_hz > 0.05 * f_min_analytic])[:n_compare]
88
89 for mc, f_fem in zip(candidates, freqs_hz):
90 mc.f_fem_hz = float(f_fem)
91 return candidates
np.ndarray radial_eigenvalues(int m, int n_max)
List[ModeComparison] analytic_frequencies(float c, float L, float R, int m, int n_radial_max=2, int k_max=2)
List[ModeComparison] run_case(*, float c, float L, float R, int m, int nx, int nr, int n_radial_max=2, int k_max=2, int n_compare=4)