STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
nlevp.py
Go to the documentation of this file.
1"""Solvers for the active-flame nonlinear eigenvalue problem (NLEVP), verification
2case V3. This is the first place ``lambda`` enters nonlinearly (through the flame
3delay ``exp(-lambda*tau)``, Eq. 7/11), so it is the first place the paper's Section IV
4solver hierarchy is actually needed rather than the plain linear GEVP of ``passive.py``.
5
6The discrete operator being solved (Eq. 11, specialized to this package's cases: a
7single azimuthal wavenumber m=0, a constant boundary-admittance matrix C from a compact
8nozzle, and a single compact flame reference, so F(lambda) is rank 1):
9
10 N(lambda) p = [ K + lambda*C + lambda^2*M2 - lambda*flame_gain*exp(-lambda*tau)*g@b.T ] p = 0
11
12``K``, ``C``, ``M2`` come from ``acoustics/assembly.py``. ``g`` and ``b`` are TWO
13DIFFERENT vectors, not the same one reused (see the long comment in
14``acoustics/assembly.py`` above ``disk_load_vector`` for why conflating them is a real,
15previously-caught bug): ``b`` (``assembly.point_sampling_vector``) is a plain point
16sample of the pressure at the flame's reference location (Eq. 7a); ``g``
17(``assembly.disk_load_vector``) is the r-weighted energy-injection weight, carrying the
18same ``r dOmega`` measure as every other matrix here. ``flame_gain`` and ``tau`` are the
19lumped Crocco interaction strength and time lag (see ``validation/v3_ntau_duct.py`` for
20how they relate to the physical n, tau of Eq. 2.1 and to this module's own dispersion
21relation).
22
23Scale note: everything here is DENSE (``numpy``/``scipy.linalg``, not sparse/Krylov).
24That is a deliberate scope choice for P0's verification meshes (a few hundred nodes):
25the sparse/shift-invert machinery of Section IV.E is already exercised (for the
26LINEAR-in-lambda case) by ``passive.py``; this module's job is to validate the
27NONLINEAR delay handling, a separate concern, at a problem size where a dense solve is
28simpler to write and just as fast. A production-scale (P1+) NLEVP solver would still
29need the sparse Krylov kernel underneath the same two algorithms below.
30"""
31
32from __future__ import annotations
33
34from typing import Tuple
35
36import numpy as np
37from scipy import linalg as sla
38
39
40def _frozen_A1(C: np.ndarray, flame_gain: float, tau: float,
41 g: np.ndarray, b: np.ndarray, lam: complex) -> np.ndarray:
42 """The coefficient of ``lambda`` in N(lambda), with the flame's delay factor
43 ``exp(-lambda*tau)`` FROZEN at the current iterate ``lam`` (Algorithm 1, step 3).
44
45 N(lambda) = K + lambda*[C - flame_gain*exp(-lam*tau)*g@b.T] + lambda^2*M2 once
46 frozen — a genuine QUADRATIC eigenvalue problem in lambda, since freezing the delay
47 factor at a fixed number removes the only source of nonlinearity.
48 """
49 return C - flame_gain * np.exp(-lam * tau) * np.outer(g, b)
50
51
52def _companion_solve(A0: np.ndarray, A1: np.ndarray, A2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
53 """All eigenpairs of the quadratic pencil ``A0 + lambda*A1 + lambda^2*A2 = 0`` via
54 the standard companion linearization to a size-2N generalized eigenvalue problem
55 (Algorithm 1, step 5 — "companion-linearize to GEVP of size 2N_h").
56
57 Writing ``z = [p; lambda*p]``, one checks by direct substitution that
58
59 [[0, I], [-A0, -A1]] z = lambda * [[I, 0], [0, A2]] z
60
61 reproduces exactly ``A0 p + lambda A1 p + lambda^2 A2 p = 0`` (the top block row
62 gives the trivial identity ``lambda*p = lambda*p``; the bottom block row gives the
63 QEP once that identity is substituted in). Solved directly and densely
64 (``scipy.linalg.eig``) since P0's verification meshes are small — see module
65 docstring for why that is an adequate, deliberate scope choice here.
66 """
67 n = A0.shape[0]
68 Z = np.zeros((n, n), dtype=complex)
69 I = np.eye(n, dtype=complex)
70 L0 = np.block([[Z, I], [-A0, -A1]])
71 L1 = np.block([[I, Z], [Z, A2]])
72 eigvals, eigvecs = sla.eig(L0, L1)
73 p_vecs = eigvecs[:n, :] # the physical eigenvector is the top half of z
74 return eigvals, p_vecs
75
76
77def fixed_point_solve(K: np.ndarray, C: np.ndarray, M2: np.ndarray, flame_gain: float,
78 tau: float, g: np.ndarray, b: np.ndarray, lambda_seed: complex,
79 *, tol: float = 1e-9, max_iter: int = 50
80 ) -> Tuple[complex, np.ndarray, int]:
81 """Algorithm 1 (Section IV.B): frozen-delay fixed point, specialized to this
82 package's single-flame-reference, dense-small-scale case.
83
84 At each outer iterate, freeze ``exp(-lambda*tau)`` at the current guess, solve the
85 resulting *linear* (in the sense of ordinary QEP, not NLEVP) quadratic eigenvalue
86 problem exactly via companion linearization, and take whichever of its 2N
87 eigenvalues is nearest the current guess as the next iterate. Converges linearly;
88 the paper reports 3-8 outer iterations for realistic parameters, matched here.
89
90 Returns ``(lambda, p, n_iterations)``.
91 """
92 lam = complex(lambda_seed)
93 p = None
94 for it in range(max_iter):
95 A1 = _frozen_A1(C, flame_gain, tau, g, b, lam)
96 eigvals, eigvecs = _companion_solve(K.astype(complex), A1, M2.astype(complex))
97 idx = int(np.argmin(np.abs(eigvals - lam)))
98 lam_new = eigvals[idx]
99 p = eigvecs[:, idx]
100 if abs(lam_new - lam) < tol * max(abs(lam_new), 1.0):
101 return lam_new, p, it + 1
102 lam = lam_new
103 return lam, p, max_iter
104
105
106def residual_N(K: np.ndarray, C: np.ndarray, M2: np.ndarray, flame_gain: float, tau: float,
107 g: np.ndarray, b: np.ndarray, lam: complex, p: np.ndarray) -> np.ndarray:
108 """N(lambda) @ p — the raw NLEVP residual, used to check solver agreement/quality."""
109 N = K + lam * C + lam ** 2 * M2 - lam * flame_gain * np.exp(-lam * tau) * np.outer(g, b)
110 return N @ p
111
112
113def newton_polish(K: np.ndarray, C: np.ndarray, M2: np.ndarray, flame_gain: float, tau: float,
114 g: np.ndarray, b: np.ndarray, lambda0: complex, p0: np.ndarray,
115 *, tol: float = 1e-12, max_iter: int = 30
116 ) -> Tuple[complex, np.ndarray, int]:
117 """Bordered Newton polish (Section IV.C), solving the *true* NLEVP (delay left in,
118 not frozen) to quadratic convergence from the fixed-point solver's output.
119
120 The bordered system (paper's own equations, Section IV.C):
121
122 [[N(lambda), N'(lambda) p], [dp ] [ N(lambda) p ]
123 [c^H, 0 ]] @ [dlambda] = - [ c^H p - 1 ]
124
125 with the normalization row ``c^H p = 1`` fixing scale/phase (paper uses a generic
126 ``c``; here ``c = e_k``, a unit vector at whichever component of ``p`` has the
127 largest magnitude — a simple, standard pivoting choice so the constraint row is
128 never close to singular).
129
130 N'(lambda) = C + 2*lambda*M2 - flame_gain*exp(-lambda*tau)*(1 - lambda*tau)*outer(g,b)
131 (derivative of the "-lambda*flame_gain*exp(-lambda*tau)" term in N(lambda) w.r.t.
132 lambda; C and M2 contribute their usual constant/2*lambda derivatives).
133 """
134 lam = complex(lambda0)
135 p = p0.astype(complex).copy()
136 n = len(p)
137 k = int(np.argmax(np.abs(p)))
138 p = p / p[k] # normalize so the pivot component is exactly 1, matching c^H p = 1
139
140 for it in range(max_iter):
141 N = K + lam * C + lam ** 2 * M2 - lam * flame_gain * np.exp(-lam * tau) * np.outer(g, b)
142 Nprime = (C + 2.0 * lam * M2
143 - flame_gain * np.exp(-lam * tau) * (1.0 - lam * tau) * np.outer(g, b))
144
145 bordered = np.zeros((n + 1, n + 1), dtype=complex)
146 bordered[:n, :n] = N
147 bordered[:n, n] = Nprime @ p
148 bordered[n, k] = 1.0 # c^H = e_k^T
149
150 rhs = np.zeros(n + 1, dtype=complex)
151 rhs[:n] = -(N @ p)
152 rhs[n] = -(p[k] - 1.0)
153
154 delta = np.linalg.solve(bordered, rhs)
155 dp, dlam = delta[:n], delta[n]
156 p = p + dp
157 lam = lam + dlam
158
159 if abs(dlam) < tol * max(abs(lam), 1.0) and np.linalg.norm(dp) < tol * max(np.linalg.norm(p), 1.0):
160 return lam, p, it + 1
161
162 return lam, p, max_iter
#define max(a, b)
Definition Arduino.h:43
np.ndarray residual_N(np.ndarray K, np.ndarray C, np.ndarray M2, float flame_gain, float tau, np.ndarray g, np.ndarray b, complex lam, np.ndarray p)
Definition nlevp.py:107
np.ndarray _frozen_A1(np.ndarray C, float flame_gain, float tau, np.ndarray g, np.ndarray b, complex lam)
Definition nlevp.py:41
Tuple[complex, np.ndarray, int] fixed_point_solve(np.ndarray K, np.ndarray C, np.ndarray M2, float flame_gain, float tau, np.ndarray g, np.ndarray b, complex lambda_seed, *, float tol=1e-9, int max_iter=50)
Definition nlevp.py:80
Tuple[np.ndarray, np.ndarray] _companion_solve(np.ndarray A0, np.ndarray A1, np.ndarray A2)
Definition nlevp.py:52
Tuple[complex, np.ndarray, int] newton_polish(np.ndarray K, np.ndarray C, np.ndarray M2, float flame_gain, float tau, np.ndarray g, np.ndarray b, complex lambda0, np.ndarray p0, *, float tol=1e-12, int max_iter=30)
Definition nlevp.py:116