← 首页← Home

拟一维 Euler 方程的离散伴随:格点格式

Discrete adjoints for quasi-one-dimensional Euler: node-centred

在 Burgers 的基础上,加入密度、动量、能量以及变截面积源项。本页先介绍方程组和它的作用,再从光滑亚声速流道出发,逐步学习一阶/二阶重构、守恒残差、matrix-free 前向与伴随、GS 及 GMRES 求解,最后用差分、复数步长和独立连续伴随检验设计梯度。读者只需具备微积分、线性代数与基础数值方法知识。

Extend Burgers to density, momentum, energy and variable-area source terms. After introducing the equations and what they are for, start with smooth subsonic duct flow and build first/second-order reconstruction, conservative residuals, matrix-free tangent and adjoint operators, and GS/GMRES solvers. Verify design gradients using finite differences, complex step and an independent continuous adjoint. Calculus, linear algebra and introductory numerical methods are the prerequisites.

全页路线图:从设计输入到梯度设计输入 DD = (A0, …, A12, pb, p0, T0)16 个分量(13 + 3)第 12 节求解R(U, D) = 0Newton,GS 预处理GMRES第 5、6 节状态 U39 个未知量13 个节点 × 3第 2 节目标 Q压力反设计 QIQUT = (∂Q/∂U)T第 7 节梯度 g16 个分量第 12 节前向(tangent)路线:从左往右,每个设计分量一次D˙ = ek第 k 个设计分量Jv = −RD D˙​一次线性求解v = U˙​状态的切向量Q˙ = QUv + QD D˙​方向导数gk梯度的一个分量16 个设计分量 → 16 次线性求解,每次得到梯度的一个分量(第 8、10 节)伴随(adjoint)路线:从右往左,只解一次g = QDT + RDTψ全部 16 个分量ψ伴随变量JTψ = −QUT一次转置求解QUT = (∂Q/∂U)T目标对状态的导数1 次转置求解,再做乘积 RDTψ(不再求解),得到全部 16 个分量(第 9、11、12 节)代价:16 个设计分量 ⇒ 前向 16 次线性求解,伴随 1 次全页路线图:从设计输入到梯度原始计算前向 ×16伴随 ×1从上往下从下往上设计输入 D16 个分量第 12 节求解R(U, D) = 0第 5、6 节状态 U39 个未知量第 2 节目标 QQUT = (∂Q/∂U)T第 7 节梯度 g16 个分量第 12 节D˙ = ek第 k 个设计分量Jv = −RD D˙​一次线性求解v = U˙​状态的切向量Q˙ = QUv + QD D˙​方向导数gk梯度的一个分量g = QDT + RDTψ全部 16 个分量ψ伴随变量JTψ = −QUT一次转置求解QUT = (∂Q/∂U)T目标对状态的导数16 个设计分量:前向 16 次线性求解,伴随 1 次
Page roadmap: from the design input to the gradientdesign input DD = (A0, …, A12, pb, p0, T0)16 components(13 + 3)Section 12solveR(U, D) = 0Newton, GS-preconditioned GMRESSections 5–6state U39 unknowns13 nodes × 3Section 2objective Qpressure inversedesign QIQUT = (∂Q/∂U)TSection 7gradient g16 componentsSection 12Tangent (forward) route: left to right, once per design componentD˙ = ekdesign component kJv = −RD D˙​one linear solvev = U˙​tangent of the stateQ˙ = QUv + QD D˙​directional derivativegkone gradient component16 design components → 16 linear solves, each giving one gradient component (Sections 8 and 10)Adjoint route: right to left, a single solveg = QDT + RDTψall 16 componentsψadjoint variableJTψ = −QUTone transposed solveQUT = (∂Q/∂U)Tobjective vs state1 transposed solve, then the product RDTψ (no further solve) gives all 16 components (Sections 9, 11 and 12)Cost: 16 design components ⇒ 16 tangent solves against 1 adjoint solvePage roadmap: from the design input to the gradientPrimalTangent ×16Adjoint ×1top to bottombottom to topdesign input D16 componentsSection 12solveR(U, D) = 0Sections 5–6state U39 unknownsSection 2objective QQUT = (∂Q/∂U)TSection 7gradient g16 componentsSection 12D˙ = ekcomponent kJv = −RD D˙​one linear solvev = U˙​state tangentQ˙ = QUv + QD D˙​along component kgkone componentg = QDT + RDTψall 16 componentsψadjoint variableJTψ = −QUTtransposed solveQUT = (∂Q/∂U)Tobjective vs state16 design components:16 tangent solves, 1 adjoint solve
全页路线图 · 从设计输入到梯度。上排是原始计算:由设计输入求解离散方程,得到状态,再算出目标。前向(tangent)路线顺着计算方向,每个设计分量解一次线性方程,得到梯度的一个分量;伴随(adjoint)路线逆着计算方向,只解一次转置方程,再做一次乘积就得到全部分量。本页默认设置下有 16 个设计分量:16 次前向求解对 1 次伴随求解。方框下方的节号可以点击。Page roadmap · From the design input to the gradient. The top row is the primal computation: solve the discrete equations for the design input, obtain the state, then evaluate the objective. The tangent (forward) route follows the direction of computation and solves one linear system per design component, each giving one gradient component; the adjoint route runs against it, solves a single transposed system and obtains every component with one more product. With this page's default settings there are 16 design components: 16 tangent solves against one adjoint solve. The section numbers under the boxes are links.

第 1 节Section 1

方程:定常拟一维 Euler 方程组

The equations: steady quasi-one-dimensional Euler

从守恒律到定常方程组

From conservation laws to the steady system

Burgers 方程只有一个守恒量;本页换成可压缩气体的三个守恒量 U=(ρ,m,e)TU=(\rho,m,e)^T,即单位体积的质量、动量和总能量。考虑一条截面积 A(x)A(x) 沿轴线缓慢变化的流道,假设流动在每个截面上近似均匀、只随 xx 变化。流道中一段 [a,b][a,b] 里的总量 ∫abAU dx\int_a^bAU\,dx,只因两端截面上的通量 AF(U)AF(U) 而改变;动量还多一项:截面积变化的地方,壁面压力在单位长度上有沿轴线的分力 A′pA'p。写成微分形式,就是

The Burgers equation has one conserved quantity; this page has the three of a compressible gas, U=(ρ,m,e)TU=(\rho,m,e)^T: mass, momentum and total energy per unit volume. Consider a duct whose cross-section A(x)A(x) varies slowly along its axis, and assume that the flow is nearly uniform over each cross-section and varies only with xx. The amount ∫abAU dx\int_a^bAU\,dx in a piece [a,b][a,b] of the duct changes only through the fluxes AF(U)AF(U) across its two end sections; momentum has one more term: where the area changes, the wall pressure has an axial component A′pA'p per unit length. In differential form,

∂t(AU)+∂x(AF(U))=A′p(U)e2,F=[mm2/ρ+p(e+p)m/ρ],e2=(0,1,0)T.\partial_t(AU)+\partial_x(AF(U))=A'p(U)e_2,\quad F=\begin{bmatrix}m\\m^2/\rho+p\\(e+p)m/\rho\end{bmatrix},\quad e_2=(0,1,0)^T.

理想气体取 γ=1.4、气体常数 R=1。m=ρu 是动量密度,e 是总能量密度,不能把 e 当作单位质量能量。先从 U 恢复压力、速度和声速,再计算通量。

Use an ideal gas with γ=1.4 and gas constant R=1. Momentum density is m=ρu; e is total energy per unit volume, not per unit mass. Recover pressure, velocity and sound speed before evaluating flux.

u=m/ρ,p=(γ−1)(e−m2/(2ρ)),c=γp/ρ,T=p/ρ.u=m/\rho,\quad p=(\gamma-1)(e-m^2/(2\rho)),\quad c=\sqrt{\gamma p/\rho},\quad T=p/\rho.

这里的“拟一维”并非忽略横截面:截面积同时乘在面通量上,并通过壁面压力源项进入动量方程。本页只研究定常解。令时间导数为零,得到要在 [0,1][0,1] 上离散的方程组:

Quasi-one-dimensional flow retains cross-section effects through both area-weighted flux and the wall-pressure momentum source. This page studies steady solutions only. Setting the time derivative to zero gives the system to be discretised on [0,1][0,1]:

ddx(A F(U))=A′ p e2,x∈[0,1].\frac{d}{dx}\bigl(A\,F(U)\bigr)=A'\,p\,e_2,\qquad x\in[0,1].

入口 x=0x=0 给定总压 p₀ 和总温 T₀,出口 x=1x=1 给定静压 pbp_b;下一小节说明为什么是这三个量。我们研究光滑的亚声速解:处处 u>0u>0,马赫数 M=u/c<1M=u/c<1。本例不含激波,所以第 3 节的二阶重构不使用限制器;间断和激波留到附录 B。

The inlet x=0x=0 takes the total pressure p₀ and the total temperature T₀, the outlet x=1x=1 the static pressure pbp_b; the next subsection explains why these three. We study smooth subsonic solutions: u>0u>0 everywhere and the Mach number M=u/c<1M=u/c<1. This example has no shocks, so the second-order reconstruction of Section 3 uses no limiter; discontinuities and shocks are left to Appendix B.

图 1把这个问题画在一张图上(当前设计;离散解取页首默认的 N=12N=12、二阶)。上:流道,高度与截面积 A(x)A(x) 成正比,两端标出边界数据;13 个圆点是作为设计变量的节点截面积。中:马赫数 M(x)M(x),实线是等熵精确解,圆点是本页算出的离散解(第 2 节起讲怎样算);全流道都在声速线 M=1M=1 之下,喉部 x=0.5x=0.5 流速最快。下:压力 p(x)p(x),喉部最低,出口回到 pbp_b;虚线是第 7 节反设计的目标压力 p⋆p^\star。圆点与实线的差别是离散误差,第 15 节测量它怎样随网格加密而减小。

Figure 1 draws the whole problem (the current design; the discrete solution at the defaults at the top of the page, N=12N=12 and second order). Top: the duct, drawn with height proportional to the area A(x)A(x), with the boundary data at its two ends; the 13 dots are the nodal areas that serve as design variables. Middle: the Mach number M(x)M(x); the solid curve is the exact isentropic solution and the dots are the discrete solution this page computes (Section 2 onwards shows how); the whole duct stays below the sonic line M=1M=1, and the flow is fastest at the throat x=0.5x=0.5. Bottom: the pressure p(x)p(x), lowest at the throat and back to pbp_b at the outlet; the dashed line is the target pressure p⋆p^\star of the inverse design of Section 7. The gap between the dots and the solid curves is the discretisation error, which Section 15 measures under mesh refinement.

图 1 · 本页的问题一览(当前设计;离散解取页首默认的 N = 12、二阶,为节点值)。上:流道,高度与截面积 A(x) 成正比;圆点是作为设计变量的 13 个节点截面积 A₀ … A₁₂;入口给定 p₀ = 1、T₀ = 1,出口给定 pb = 0.9。中:马赫数 M(x),实线为等熵精确解,圆点为本页离散解,全流道低于声速(喉部 M ≈ 0.65)。下:压力 p(x),实线与圆点同上;虚线为第 7 节反设计的目标压力 p*,由目标流道 A*(x) 在同一网格上算出;空心圈为出口给定的 pb。气流A₀A₁₂喉部 x = 0.5:A = 0.7入口 x = 0:给定 p₀ = 1、T₀ = 1出口 x = 1:给定 pb = 0.9流道壁面高度 ∝ A(x)设计变量节点截面积 A₀ … A₁₂00.51M(x)M = 1(声速)精确解等熵关系本页离散解N = 12,二阶0.70.80.900.250.50.751xp(x)目标压力 p*目标流道 A*(x)出口压力(给定)p(1) = pb = 0.9设计 D = (A₀, …, A₁₂, pb, p₀, T₀):13 个截面积与 3 个边界数据,共 16 个分量
图 1 · 本页的问题一览(当前设计;离散解取页首默认的 N = 12、二阶,为节点值)。上:流道,高度与截面积 A(x) 成正比;圆点是作为设计变量的 13 个节点截面积 A₀ … A₁₂;入口给定 p₀ = 1、T₀ = 1,出口给定 pb = 0.9。中:马赫数 M(x),实线为等熵精确解,圆点为本页离散解,全流道低于声速(喉部 M ≈ 0.65)。下:压力 p(x),实线与圆点同上;虚线为第 7 节反设计的目标压力 p*,由目标流道 A*(x) 在同一网格上算出;空心圈为出口给定的 pb。
Figure 1 · The problem at a glance (current design; the discrete solution at the defaults at the top of the page, N = 12, second order, nodal values). Top: the duct, drawn with height proportional to the area A(x); the dots are the 13 nodal areas A₀ … A₁₂, which are design variables; the inlet has the prescribed p₀ = 1 and T₀ = 1, the outlet pb = 0.9. Middle: the Mach number M(x); solid: the exact isentropic solution, dots: this page's discrete solution; the whole duct is subsonic (M ≈ 0.65 at the throat). Bottom: the pressure p(x), solid and dots as above; dashed: the target pressure p* of the inverse design of Section 7, computed on the same mesh from the target duct A*(x); ring: the prescribed outlet value pb.flowA₀A₁₂throat x = 0.5: A = 0.7inlet x = 0: p₀ = 1, T₀ = 1 prescribedoutlet x = 1: pb = 0.9 prescribedduct wallheight ∝ A(x)designnodal areas A₀ … A₁₂00.51M(x)M = 1 (sonic)exactisentropicdiscreteN = 12, 2nd order0.70.80.900.250.50.751xp(x)target p*target duct A*(x)outlet (prescribed)p(1) = pb = 0.9design D = (A₀, …, A₁₂, pb, p₀, T₀): 13 areas and 3 boundary data, 16 components
Figure 1 · The problem at a glance (current design; the discrete solution at the defaults at the top of the page, N = 12, second order, nodal values). Top: the duct, drawn with height proportional to the area A(x); the dots are the 13 nodal areas A₀ … A₁₂, which are design variables; the inlet has the prescribed p₀ = 1 and T₀ = 1, the outlet pb = 0.9. Middle: the Mach number M(x); solid: the exact isentropic solution, dots: this page's discrete solution; the whole duct is subsonic (M ≈ 0.65 at the throat). Bottom: the pressure p(x), solid and dots as above; dashed: the target pressure p* of the inverse design of Section 7, computed on the same mesh from the target duct A*(x); ring: the prescribed outlet value pb.

特征速度:每一端给几个条件

Characteristic speeds: how many conditions each end takes

Burgers 方程只有一个波速 F′(u)=uF'(u)=u。这里的通量 Jacobian ∂F/∂U\partial F/\partial U 是 3×3 矩阵(附录 A.1 写出它),它的三个特征值 u−cu-c、uu、u+cu+c 是三族小扰动沿 xx 的传播速度,称为特征速度:u+cu+c 与 u−cu-c 是相对气流以声速向前、向后传播的声波,uu 是随气流一起运动的扰动(熵波)。在亚声速正向流动中,向后的声波逆流而上:

The Burgers equation has the single wave speed F′(u)=uF'(u)=u. Here the flux Jacobian ∂F/∂U\partial F/\partial U is a 3×3 matrix (Appendix A.1 writes it out), and its three eigenvalues u−cu-c, uu and u+cu+c are the speeds at which three families of small disturbances travel along xx, the characteristic speeds: u+cu+c and u−cu-c are sound waves travelling forward and backward relative to the gas at the speed of sound, and uu is a disturbance carried with the gas (the entropy wave). In positive subsonic flow the backward sound wave moves upstream:

u−c<0<u<u+c.u-c<0<u<u+c.

每一端要给的条件数,等于在这一端进入计算域的特征族数。入口 x=0x=0 处 uu 与 u+cu+c 两族进入,给两个条件:总压 p₀ 和总温 T₀,相当于上游接着一个静止的储气罐;第三族 u−cu-c 在这里离开计算域,它携带的信息由内部决定。出口 x=1x=1 处只有 u−cu-c 一族从外部进入,给一个条件:静压 pbp_b。多给会过约束,少给则解不唯一。出口压力正是沿 u−cu-c 这一族逆流传到整个流道,所以在给定 p₀、T₀ 时,pbp_b 决定流量。第 4 节说明离散格式怎样施加这三个条件;伴随沿同样的特征反向传播,两端的条件数正好对调(附录 A 的图 A.1)。

Each end takes as many conditions as characteristic families enter the domain there. At the inlet x=0x=0 the families uu and u+cu+c enter, so the inlet takes two conditions, the total pressure p₀ and the total temperature T₀, as if a reservoir at rest were attached upstream; the third family, u−cu-c, leaves the domain there and carries information set by the interior. At the outlet x=1x=1 only u−cu-c enters from outside, so the outlet takes one condition, the static pressure pbp_b. More conditions would overconstrain the problem; fewer would leave the solution undetermined. The outlet pressure travels upstream along the family u−cu-c through the whole duct, which is why, for given p₀ and T₀, pbp_b sets the mass flow. Section 4 shows how the discretisation imposes the three conditions; the adjoint travels along the same characteristics in reverse, so the counts at the two ends swap (Figure A.1 in Appendix A).

图 2用同样的解画出三个特征速度。u+cu+c 和 uu 处处为正,u−cu-c 处处为负,所以入口有两族进入、出口有一族进入,与边界数据的个数一致。喉部 uu 最大,u−cu-c 最接近零(u−c≈−0.40u-c\approx{-0.40}),但仍为负:气流没有达到声速。若把出口压力降得足够低,喉部会达到声速(u−c=0u-c=0),流道壅塞,扩张段可能出现超声速流和激波;附录 B.5 另取一个喷管研究这种流动。

Figure 2 plots the three characteristic speeds of the same solutions. u+cu+c and uu are positive everywhere and u−cu-c is negative everywhere, so two families enter at the inlet and one at the outlet, as many as the boundary data. At the throat uu is largest and u−cu-c comes closest to zero (u−c≈−0.40u-c\approx{-0.40}), but it stays negative: the flow does not reach the speed of sound. If the outlet pressure is lowered far enough, the throat reaches sonic speed (u−c=0u-c=0), the duct chokes, and supersonic flow and a shock can appear in the diverging part; Appendix B.5 studies such a flow in a separate nozzle.

图 2 · 三个特征速度与每一端的条件数(图 1 的解;实线为等熵精确解,圆点为本页离散解)。全流道 u − c < 0 < u < u + c。两端的箭头标出每一族在该端进入(实线)还是离开(虚线)计算域,旁边是该端的特征速度:入口有 2 族进入,给 2 个条件 p₀、T₀;出口有 1 族进入,给 1 个条件 pb。−1−0.500.511.5200.250.50.751xu + cuu − c入口 x = 01.620.46−0.71出口 x = 11.620.46−0.71入口:u、u + c 进入 → 2 个条件 p₀、T₀出口:u − c 进入 → 1 个条件 pb进入计算域离开计算域离散解
图 2 · 三个特征速度与每一端的条件数(图 1 的解;实线为等熵精确解,圆点为本页离散解)。全流道 u − c < 0 < u < u + c。两端的箭头标出每一族在该端进入(实线)还是离开(虚线)计算域,旁边是该端的特征速度:入口有 2 族进入,给 2 个条件 p₀、T₀;出口有 1 族进入,给 1 个条件 pb。
Figure 2 · The three characteristic speeds and the number of conditions at each end (the solutions of Figure 1; solid: the exact isentropic solution, dots: this page's discrete solution). Throughout the duct u − c < 0 < u < u + c. The arrows at the two ends show whether each family enters (solid) or leaves (dashed) the domain there, with the speed at that end beside it: two families enter at the inlet, which takes two conditions, p₀ and T₀; one enters at the outlet, which takes one condition, pb.−1−0.500.511.5200.250.50.751xu + cuu − cinlet x = 01.620.46−0.71outlet x = 11.620.46−0.71inlet: u, u + c enter → 2 conditions, p₀ and T₀outlet: u − c enters → 1 condition, pbenters the domainleaves the domaindiscrete
Figure 2 · The three characteristic speeds and the number of conditions at each end (the solutions of Figure 1; solid: the exact isentropic solution, dots: this page's discrete solution). Throughout the duct u − c < 0 < u < u + c. The arrows at the two ends show whether each family enters (solid) or leaves (dashed) the domain there, with the speed at that end beside it: two families enter at the inlet, which takes two conditions, p₀ and T₀; one enters at the outlet, which takes one condition, pb.

本页的问题:流道、边界数据与设计变量

The problem on this page: the duct, the boundary data and the design variables

流道长度无量纲化为 1。截面积取 A(x)=1−d sin⁡2(πx)A(x)=1-d\,\sin^2(\pi x),默认收缩深度 d=0.3d=0.3:两端截面积为 1,喉部 x=0.5x=0.5 处为 0.7,先收缩、后扩张。边界数据默认为 p₀ = 1、T₀ = 1、pbp_b = 0.9(无量纲,气体常数 R=1)。离散时,截面积在 NN 个等长区间的端点 xk=k/Nx_k=k/N 上取值 Ak=A(xk)A_k=A(x_k),其间线性插值(第 2 节的网格);这 N+1N+1 个节点截面积与三个边界数据一起构成设计变量 DD,改变它们就改变流场:

The duct length is scaled to 1. The area is A(x)=1−d sin⁡2(πx)A(x)=1-d\,\sin^2(\pi x) with the default contraction depth d=0.3d=0.3: both ends have area 1 and the throat x=0.5x=0.5 has 0.7; the duct first contracts, then expands. The default boundary data are p₀ = 1, T₀ = 1 and pbp_b = 0.9 (nondimensional, gas constant R=1). In the discretisation the area takes the values Ak=A(xk)A_k=A(x_k) at the ends xk=k/Nx_k=k/N of NN equal intervals and is linear in between (the mesh of Section 2); these N+1N+1 nodal areas and the three boundary data form the design variables DD; changing them changes the flow:

D=(A0,…,AN, pb, p0, T0).D=(A_0,\ldots,A_N,\,p_b,\,p_0,\,T_0).

按伴随优化文献的通常写法,设计变量记作 DD。默认 N=12N=12 时,DD 共有 16 个分量:13 个截面积和 3 个边界数据。

As is usual in the adjoint-optimization literature, the design variables are written DD. With the default N=12N=12, DD has 16 components: 13 areas and 3 boundary data.

顶部参数改变当前光滑亚声速问题。面积 A(x)=1−d sin²(πx);参考目标面积固定为 (1−0.3sin²πx)(1−0.04sinπx),使用本次选定的边界数据生成后冻结。主求解器要求正密度、正压力且所有检查状态 M<0.95;失败时不会显示上一次结果。第 15、16 节加密表仍是固定默认参数的离线基准。

Top controls change the current smooth subsonic problem. Area is A(x)=1−d sin²(πx). The target area remains (1−0.3sin²πx)(1−0.04sinπx); its flow uses the selected boundary data and is then frozen. The primal requires positive density and pressure and M<0.95 at all checked states. Failed runs clear previous results. Sections 15–16 retain offline refinement data for the default parameters.

当前数据下全流道都是亚声速,定常解由等熵关系确定,不需要网格。没有激波、摩擦和传热时,总压、总温和流量 qm=ρuAq_m=\rho uA 沿流道不变。出口静压与总压之比给出出口马赫数 Me≈0.39M_e\approx0.39;流量不变又给出每个截面的马赫数(面积—马赫数关系):截面越小,亚声速气流越快、压力越低。喉部 M≈0.65M\approx0.65、p≈0.75p\approx0.75,是全流道流速最快、压力最低的地方。第 15 节用这个等熵解作为精确参考,附录 A 在它上面积分连续伴随。

With the current data the flow is subsonic throughout, and the steady solution follows from the isentropic relations, without a mesh. Without shocks, friction or heat transfer, the total pressure, the total temperature and the mass flow qm=ρuAq_m=\rho uA are constant along the duct. The ratio of the outlet static pressure to the total pressure gives the outlet Mach number Me≈0.39M_e\approx0.39; the constant mass flow then gives the Mach number at every section (the area–Mach relation): the smaller the section, the faster the subsonic flow and the lower the pressure. The throat has M≈0.65M\approx0.65 and p≈0.75p\approx0.75, the fastest flow and the lowest pressure in the duct. Section 15 uses this isentropic solution as the exact reference, and Appendix A integrates the continuous adjoint on it.

设计的目的在第 7 节给出。页首默认的目标是压力反设计 QIQ_I:调整设计,使压力接近目标压力 p⋆p^\star(图 1下部的虚线);p⋆p^\star 由一个固定的目标流道 A⋆(x)=(1−0.3 sin⁡2πx)(1−0.04 sin⁡πx)A^\star(x)=(1-0.3\,\sin^2\pi x)(1-0.04\,\sin\pi x) 在同一网格上算出,所以这是一个反设计问题。另一个可选目标是壁面推力 QTQ_T,即壁面压力的轴向合力 ∫01pA′ dx\int_0^1pA'\,dx。目标都是一个标量,所以一次伴随求解就能给出全部 16 个设计分量的梯度。

Section 7 gives the purpose of the design. The default objective at the top of the page is the pressure inverse design QIQ_I: adjust the design so that the pressure approaches the target pressure p⋆p^\star (the dashed line in the lower part of Figure 1); p⋆p^\star is computed on the same mesh from a fixed target duct A⋆(x)=(1−0.3 sin⁡2πx)(1−0.04 sin⁡πx)A^\star(x)=(1-0.3\,\sin^2\pi x)(1-0.04\,\sin\pi x), which makes this an inverse-design problem. The other objective is the wall thrust QTQ_T, the axial force of the wall pressure, ∫01pA′ dx\int_0^1pA'\,dx. Each objective is a single number, so one adjoint solve gives the gradient with respect to all 16 design components.

这个方程组的作用

What these equations are for

在流体力学里,拟一维 Euler 方程描述截面积沿轴线缓慢变化的管道、喷管和扩压器中的无黏可压缩流动,前提是流动在每个截面上近似均匀。它把 Burgers 方程没有的东西带了进来:压力和状态方程把密度、动量和能量耦合在一起;声波既能顺流也能逆流传播;截面积的变化使亚声速气流在收缩段加速、在扩张段减速,本页默认的先收缩后扩张的流道就是一个文丘里管。出口压力足够低时,喉部达到声速,扩张段出现超声速流;出口压力在一定范围内时,超声速区以一道正激波结束。火箭发动机和超声速风洞的拉瓦尔喷管就是这种先收缩后扩张、在喉部达到声速的喷管。正因为它只有一维,却保留了可压缩流的这些主要特征,可压缩流的教科书常用它讲解喷管流动,数值通量和特征边界条件也常先在它上面检验。

In fluid dynamics the quasi-one-dimensional Euler equations describe inviscid compressible flow in pipes, nozzles and diffusers whose area varies slowly along the axis, provided that the flow is nearly uniform over each cross-section. They bring in what the Burgers equation lacks: the pressure and the equation of state couple the density, the momentum and the energy; sound waves travel both downstream and upstream; and a change of area accelerates a subsonic flow in a contraction and slows it down in an expansion, as in the converging–diverging duct of this page, a venturi. When the outlet pressure is low enough, the throat reaches sonic speed and the diverging part carries supersonic flow; for a range of outlet pressures the supersonic region ends in a normal shock. The Laval nozzles of rocket engines and supersonic wind tunnels are converging–diverging nozzles of this kind, sonic at the throat. Because they are one-dimensional yet keep these main features of compressible flow, textbooks on compressible flow commonly use them to explain nozzle flow, and numerical fluxes and characteristic boundary conditions are often tested on them first.

在本课程里,它排在一维 Burgers 之后、二维 Euler 之前:先在一维里单独面对方程组、热力学、特征边界和几何设计变量,再进入需要第二个空间方向和网格坐标导数的二维页面。网格仍然是一维的,每个数组都能完整地写在页面上,但比 Burgers 多了下面几样东西:

In this course it comes after 1-D Burgers and before 2-D Euler, so that a system, thermodynamics, characteristic boundaries and geometric design variables are met in one dimension, before the 2-D pages add a second space direction and derivatives with respect to mesh coordinates. The mesh is still one-dimensional and every array fits on the page, but compared with Burgers it adds the following:

  • 方程组。每个位置有三个未知量,Jacobian 由 3×3 块组成,第 6 节的 GS 按块扫描;默认网格有 39 个未知量。
  • A system. Each location has three unknowns, the Jacobian consists of 3×3 blocks and the GS of Section 6 sweeps block by block; the default mesh has 39 unknowns.
  • 热力学。通量经由压力依赖全部三个守恒量;前向与伴随都要沿压力、声速这些中间量逐步求导(第 8、9 节)。
  • Thermodynamics. Through the pressure the flux depends on all three conserved variables; the tangent and the adjoint differentiate step by step through intermediate quantities such as the pressure and the sound speed (Sections 8 and 9).
  • 特征边界条件。入口两个、出口一个;边界状态是内部状态和边界数据的非线性函数,前向与伴随也要经过这些边界映射(第 4 节)。
  • Characteristic boundary conditions. Two at the inlet and one at the outlet; the boundary states are nonlinear functions of the interior state and the boundary data, and the tangent and the adjoint pass through these boundary maps too (Section 4).
  • 几何作为设计变量。截面积同时出现在面通量、壁面压力源项和目标函数的积分里,第 12 节的设计导数要把这三处都算上;二维页面再把几何导数推广到全部网格节点坐标。
  • Geometry as design variables. The areas enter the face fluxes, the wall-pressure source and the integrals of the objective, and the design derivatives of Section 12 must include all three; the 2-D pages extend geometric derivatives to every mesh-node coordinate.
  • 可以对照的参考解。等熵解是网格收敛的精确参考(第 15 节);连续伴随可以独立地数值积分(第 16 节、附录 A),与离散伴随对照。
  • References to compare with. The isentropic solution is the exact reference for mesh convergence (Section 15), and the continuous adjoint can be integrated independently (Section 16, Appendix A) for comparison with the discrete adjoint.

图 3把本页放在整个网站的路线上。每往右一步主要增加一类新内容:一维 Burgers 只有一个方程,设计变量在源项里;本页把它换成质量、动量、能量三个方程,引入压力和变截面积;二维 Euler 增加第二个空间方向,面有了法向,网格坐标也成为设计变量;二维层流 N–S 加入黏性应力和热传导,面上还要用到梯度;二维 RANS–SA 再加一个湍流模型方程。每点的未知量从 1 个增加到 5 个,但每一页的残差都由面循环组装,前向与伴随也沿同一个循环求导。另有一页与本页相邻:一维层流 N–S 页留在一维,但截面积不变,在 Euler 方程上加入黏性和热传导。图中的方框和这里的页面名都链接到各页的第 1 节。

Figure 3 places this page on the route through the site. Each step to the right mainly adds one kind of ingredient: 1-D Burgers has a single equation, with the design variables in its source; this page replaces it by three equations (mass, momentum and energy) and brings in the pressure and a varying cross-section; 2-D Euler adds a second space direction, so faces have normals and the mesh coordinates become design variables; 2-D laminar N–S adds viscous stresses and heat conduction, which need gradients at the faces; 2-D RANS–SA adds one turbulence-model equation. The unknowns per point grow from 1 to 5, yet every page still assembles its residual with face loops, and the tangent and the adjoint differentiate the same loops. One more page sits next to this one: the 1-D laminar N–S page stays in one dimension, at constant area, and adds viscosity and heat conduction to the Euler equations. The boxes of the figure and the page names here link to Section 1 of each page.

图 3 · 从本页到网站上的其他方程。每往右一步主要增加一类新内容,每点的未知量从 1 个增加到 5 个(取自各页的默认设置);每一页都用面循环组装残差,前向与伴随沿同一个循环求导。每个方框链接到该页(格点格式)的第 1 节。一维 Burgers每点 1 个未知量非线性对流 u ux设计在源项里拟一维 Euler每点 3 个未知量质量、动量、能量压力、变截面积本页二维 Euler每点 4 个未知量第二个空间方向面法向、网格坐标二维层流 N–S每点 4 个未知量黏性应力、热传导面上要用梯度二维 RANS–SA每点 5 个未知量湍流模型:多一个输运方程每一页都用面循环组装残差:收集两侧状态 → 重构 → 算通量 → 分发到两侧前向与伴随沿同一个循环求导;黏性页面在通量循环之前先算一遍梯度
图 3 · 从本页到网站上的其他方程。每往右一步主要增加一类新内容,每点的未知量从 1 个增加到 5 个(取自各页的默认设置);每一页都用面循环组装残差,前向与伴随沿同一个循环求导。每个方框链接到该页(格点格式)的第 1 节。
Figure 3 · From this page to the other equations of the site. Each step to the right mainly adds one kind of ingredient, and the unknowns per point grow from 1 to 5 (each page's default settings); every page assembles its residual with a face loop, and the tangent and the adjoint differentiate that loop. Each box links to Section 1 of that page (node-centred scheme).1-D Burgers1 unknownper pointnonlinearconvection u uxdesign in the sourceQuasi-1-D Euler3 unknownsper pointmass, momentum,energy; pressure,varying areathis page2-D Euler4 unknownsper pointa second direction,face normals,mesh coordinates2-D laminar N–S4 unknownsper pointviscous stress,heat conduction,face gradients2-D RANS–SA5 unknownsper pointturbulence model:one more transportequationEvery page assembles its residual with a face loop: gather → reconstruct → flux → scatter.The tangent and the adjoint differentiate the same loop; the viscous pages first compute gradients.
Figure 3 · From this page to the other equations of the site. Each step to the right mainly adds one kind of ingredient, and the unknowns per point grow from 1 to 5 (each page's default settings); every page assembles its residual with a face loop, and the tangent and the adjoint differentiate that loop. Each box links to Section 1 of that page (node-centred scheme).

本页要回答的问题

The question this page answers

本页只回答一个问题:设计 DD 改变一点,目标 QQ 改变多少;页首的全页路线图把后面各节串成一条线。按页首默认设置(二阶、GS 预处理的 GMRES、压力反设计)实际计数,16 次前向求解共用 468 次 GMRES 迭代,1 次伴随求解只用 27 次。

This page answers one question: how much does the objective QQ change when the design DD changes a little? The page roadmap at the top strings the later sections together. Counted with the default settings at the top of the page (second order, GS-preconditioned GMRES, pressure inverse design), the 16 tangent solves take 468 GMRES iterations in total and the single adjoint solve only 27.

要在计算机上回答这个问题,先要把连续方程组换成有限个未知量的代数方程。第 2 节先建立网格、决定三个守恒量放在哪里,第 3 节再把守恒律写成面上的通量差,并在每个面的两侧重构状态。

To answer it on a computer, the continuous equations must first become algebraic equations in finitely many unknowns. Section 2 builds the mesh and decides where the three conserved variables live; Section 3 writes the conservation laws as flux differences across faces and reconstructs the states on the two sides of each face.

第 2 节Section 2

网格:一个位置对应三个守恒量

Mesh: three conserved variables at each location

第 1 节的方程组在流道的每一个 xx 上都成立,但计算机只能存储有限个数。本节选定网格,决定三个守恒量存放在哪里,并把全部状态排成一个数组。

The equations of Section 1 hold at every xx of the duct, but a computer stores finitely many numbers. This section chooses the mesh, decides where the three conserved variables are stored and arranges the whole state in one array.

先从 Burgers 的一个未知量走向 Euler 的三个未知量。把流道长度无量纲化为 1,在 N 个等长区间上给出截面积 A。数组下标从 0 开始,状态排列为 [ρ₀,m₀,e₀,ρ₁,m₁,e₁,…]。

Move from one Burgers unknown to three Euler unknowns. Scale duct length to one and specify area A at the endpoints of N equal intervals. Arrays are zero-based; the state ordering is [ρ₀,m₀,e₀,ρ₁,m₁,e₁,…].

本页把 U 保存于原始节点 xi=i h。内部控制体为 [xi−h/2,xi+h/2],端点控制体只有半宽;对偶面截面积由原始节点的 A 线性插值得到。状态有 3(N+1) 个自由度。端点的三条强边界约束在残差装配完成后替换指定方程。

U is stored at primal nodes xi=i h. Interior dual volumes extend half an interval to either side; endpoint volumes have half width. Dual-face areas interpolate primal-node A values. There are 3(N+1) state degrees of freedom. Three strong endpoint constraints replace selected equations after residual assembly.

h=1/N,A(x)=1−0.3sin⁡2(πx),Ui=(ρi,mi,ei)T.h=1/N,\qquad A(x)=1-0.3\sin^2(\pi x),\qquad U_i=(\rho_i,m_i,e_i)^T.

下面选取控制体与显示分量。蓝色点是未知量位置,竖线是计算面,淡紫色区域是当前控制体;流道轮廓表示截面积,不是二维网格。所有后续交互共享顶端选择的空间阶数与线性求解方法。

Select a volume and component below. Blue dots locate unknowns, vertical lines mark computational faces, and violet shading selects a volume. The duct outline represents area, not a two-dimensional mesh. All subsequent interactions share the spatial order and solver selected at the top.

有了网格和未知量,还要把第 1 节的守恒律写到每个控制体上。第 3 节把它写成面上的通量差,并说明面两侧的状态从哪里来。

With the mesh and the unknowns in place, the conservation laws of Section 1 must be written for every control volume. Section 3 writes them as flux differences across faces and shows where the states on the two sides of a face come from.

第 3 节Section 3

面上的通量:通量差与二阶重构

The flux on a face: flux differences and second-order reconstruction

本节实验:改变一个未知量,哪些面值会变化?先预测重构邻域,再用图验证。

Experiment: Which traces change when one unknown changes? Predict the stencil, then inspect the diagram.

第 2 节的每个控制体存着一组守恒量。本节先把守恒律写成面上的通量差,再说明二阶格式怎样重构每个面两侧的状态,以及数值通量怎样把两侧的状态合成一个面通量。

Each control volume of Section 2 stores one set of conserved variables. This section first writes the conservation laws as flux differences across faces, then shows how the second-order scheme reconstructs the states on the two sides of each face and how the numerical flux combines them into one face flux.

从守恒律到面上的通量差

From conservation laws to flux differences on faces

把第 1 节的定常方程组在一个控制体上积分,控制体的两个面记作 xi−1/2x_{i-1/2} 和 xi+1/2x_{i+1/2}(位置见第 2 节的网格)。由微积分基本定理,导数变成两个面上面积加权通量之差,壁面压力项变成体积分:

Integrate the steady equations of Section 1 over one control volume, with faces xi−1/2x_{i-1/2} and xi+1/2x_{i+1/2} (their positions are given by the mesh of Section 2). By the fundamental theorem of calculus the derivative becomes the difference of the area-weighted fluxes at the two faces, and the wall-pressure term becomes a volume integral:

[A F(U)]xi−1/2xi+1/2=e2∫xi−1/2xi+1/2A′p dx.\bigl[A\,F(U)\bigr]_{x_{i-1/2}}^{x_{i+1/2}}=e_2\int_{x_{i-1/2}}^{x_{i+1/2}}A'p\,dx.

每个守恒量流出控制体的净通量,等于控制体内的源项;只有动量有源项,即壁面压力的轴向分力。这就是有限体积法的出发点。相邻两个控制体在同一个面 ff 上可能给出不同的状态 Uf−U_f^- 和 Uf+U_f^+;数值通量把它们合成一个共同的面通量 HfH_f,左右两个控制体以相反的符号共用它,所以内部面的贡献在全域求和时相互抵消。第 5 节把面通量和体积分装配成残差。HfH_f 用哪两个状态,由下面的重构决定。

For each conserved variable the net outward flux of a control volume equals the source inside it; only momentum has a source, the axial force of the wall pressure. This is the starting point of finite volumes. At a face ff the two neighbouring volumes may supply different states Uf−U_f^- and Uf+U_f^+; a numerical flux combines them into one shared face flux HfH_f, which the two volumes use with opposite signs, so the contributions of interior faces cancel in a global sum. Section 5 assembles the face fluxes and the volume integrals into the residual. Which two states HfH_f receives is decided by the reconstruction below.

二阶重构与数值通量

Second-order reconstruction and the numerical flux

一阶直接读取 Ui;二阶把守恒量在控制体内重构为直线。内部斜率采用中心差分,两个端点采用单边差分;同一条重构既用于面状态,也用于源项和目标函数的积分点。当前仅研究光滑亚声速流动,不使用限制器。高阶重构的导数包含邻点,不能把斜率冻结后仍称为精确二阶导数。

First order reads Ui directly. Second order reconstructs a linear conservative state inside each volume, using centred interior slopes and one-sided endpoint slopes. The same reconstruction supplies faces, source quadrature and objective quadrature. This smooth subsonic example has no limiter. Reconstruction derivatives include neighbouring states; freezing slopes would not give an exact second-order derivative.

Ui(x)=Ui+(x−xi)si,si=Ui+1−Ui−12h,s0=U1−U0h,sm−1=Um−1−Um−2h.U_i(x)=U_i+(x-x_i)s_i,\quad s_i=\frac{U_{i+1}-U_{i-1}}{2h},\quad s_0=\frac{U_1-U_0}{h},\quad s_{m-1}=\frac{U_{m-1}-U_{m-2}}h.
Hf=Af[F(Uf−)+F(Uf+)2−αf2(Uf+−Uf−)],αf=u−+c−+u++c+2.H_f=A_f\left[\frac{F(U_f^-)+F(U_f^+)}2-\frac{\alpha_f}2(U_f^+-U_f^-)\right],\quad\alpha_f=\frac{u^-+c^-+u^++c^+}{2}.

在本例的正向光滑分支上 u>0;上述平均波速是明确选择的耗散形式,不是 max 波速的近似求导。α 随状态变化,其全部导数保留。每次更新检查密度、压力和重构状态的亚声速条件。图中一阶两侧常值与二阶两侧直线使用同一收敛状态,便于看清重构本身的作用。

On the selected positive-flow branch u>0. The averaged speed defines the chosen dissipation; it is not a differentiated approximation to max. All state derivatives of α are retained. Updates check density, pressure and subsonic reconstructed states. The diagram compares constant and linear traces using the same converged state to isolate reconstruction.

面通量的局部导数

The local derivatives of the face flux

第 8 节的前向与第 9 节的伴随都要对面通量求导;这里先把它们需要的局部导数逐项写出。记 U=(ρ,m,e)TU=(\rho,m,e)^T、u=m/ρu=m/\rho、E=e/ρE=e/\rho,速度、压力和声速对 UU 的偏导数都是行向量:

The tangent of Section 8 and the adjoint of Section 9 both differentiate the face flux; the local derivatives they need are written out here. With U=(ρ,m,e)TU=(\rho,m,e)^T, u=m/ρu=m/\rho and E=e/ρE=e/\rho, the derivatives of the velocity, the pressure and the sound speed with respect to UU are rows:

uU=(−uρ, 1ρ, 0),pU=(γ−1)(u22, −u, 1),cU=c2(pUp−(1,0,0)ρ).u_U=\Bigl(-\frac u\rho,\ \frac1\rho,\ 0\Bigr),\quad p_U=(\gamma-1)\Bigl(\frac{u^2}2,\ -u,\ 1\Bigr),\quad c_U=\frac c2\Bigl(\frac{p_U}p-\frac{(1,0,0)}\rho\Bigr).

逐行对 F=(m, mu+p, (e+p)u)TF=(m,\ mu+p,\ (e+p)u)^T 求导,得到通量 Jacobian(附录 A.1 把同一个矩阵记作 BB;它的三个特征值就是第 1 节的 u−cu-c、uu、u+cu+c):

Differentiating F=(m, mu+p, (e+p)u)TF=(m,\ mu+p,\ (e+p)u)^T row by row gives the flux Jacobian (Appendix A.1 writes the same matrix as BB; its three eigenvalues are the u−cu-c, uu and u+cu+c of Section 1):

FU=[010γ−32u2(3−γ)uγ−1(γ−1)u3−γuEγE−32(γ−1)u2γu].F_U=\begin{bmatrix}0&1&0\\\frac{\gamma-3}2u^2&(3-\gamma)u&\gamma-1\\(\gamma-1)u^3-\gamma uE&\gamma E-\frac32(\gamma-1)u^2&\gamma u\end{bmatrix}.

每单位面积的数值通量 H^=Hf/Af\widehat H=H_f/A_f 既直接依赖两侧状态,也经由平均波速 α\alpha 依赖它们。记 ΔU=Uf+−Uf−\Delta U=U_f^+-U_f^-,α∓=12(uU+cU)\alpha_\mp=\tfrac12(u_U+c_U) 在 Uf∓U_f^\mp 处取值(行向量),则

The numerical flux per unit area, H^=Hf/Af\widehat H=H_f/A_f, depends on the two face states directly and through the averaged speed α\alpha. With ΔU=Uf+−Uf−\Delta U=U_f^+-U_f^- and the rows α∓=12(uU+cU)\alpha_\mp=\tfrac12(u_U+c_U) evaluated at Uf∓U_f^\mp,

∂H^∂Uf−=FU(Uf−)2+α2I−12ΔU α−,∂H^∂Uf+=FU(Uf+)2−α2I−12ΔU α+,∂Hf∂Af=H^.\frac{\partial\widehat H}{\partial U_f^-}=\frac{F_U(U_f^-)}2+\frac\alpha2I-\frac12\Delta U\,\alpha_-,\qquad\frac{\partial\widehat H}{\partial U_f^+}=\frac{F_U(U_f^+)}2-\frac\alpha2I-\frac12\Delta U\,\alpha_+,\qquad\frac{\partial H_f}{\partial A_f}=\widehat H.

两个式子的最后一项都是列向量乘行向量,秩为一:它来自耗散速度随状态的变化,只在两侧状态相同的地方为零。把它丢掉(“冻结 α\alpha”)得到的矩阵就不再是这个通量的导数。面状态对存储状态是线性的:Uf±=∑jWfj±UjU_f^\pm=\sum_jW^\pm_{fj}U_j,权重就是上面重构公式的系数(本页的权重是标量,三个分量相同)。二阶时,控制体 ii 与 i+1i+1 之间的内部面取

The last term of each is a column times a row, a rank-one matrix: it carries the change of the dissipation speed with the state and vanishes only where the two states agree. Dropping it (“freezing α\alpha”) gives a matrix that is no longer the derivative of this flux. The face states are linear in the stored states, Uf±=∑jWfj±UjU_f^\pm=\sum_jW^\pm_{fj}U_j, with the coefficients of the reconstruction above as weights (scalars on this page, the same for the three components). At second order an interior face between volumes ii and i+1i+1 takes

Uf−=Ui+14(Ui+1−Ui−1),Uf+=Ui+1−14(Ui+2−Ui),Af=(1−tf)Ak+tfAk+1,U_f^-=U_i+\tfrac14(U_{i+1}-U_{i-1}),\qquad U_f^+=U_{i+1}-\tfrac14(U_{i+2}-U_i),\qquad A_f=(1-t_f)A_k+t_fA_{k+1},

所以每个面读取 4 个存储状态(一阶只读 2 个),端点控制体用单边斜率。面积在设计截面积之间线性插值:格点页的内部面位于两个节点的中点,tf=12t_f=\tfrac12,两个端面在端节点上。

so each face reads four stored states (two at first order), and the end volumes use one-sided slopes. The face area is interpolated linearly between design areas: on the node page the interior faces sit halfway between two nodes, tf=12t_f=\tfrac12, and the two end faces on the end nodes.

算例(页首默认设置):面 f=6f=6 位于 xf=0.4583x_f=0.4583,在控制体 55 与 66 之间,Af=0.71A_f=0.71。两侧重构状态与平均波速为

Worked example (the defaults at the top of the page): the face f=6f=6 lies at xf=0.4583x_f=0.4583, between volumes 55 and 66, with Af=0.71A_f=0.71. Its reconstructed states and averaged speed are

Uf−=(0.8192, 0.6082, 2.118)T,Uf+=(0.8157, 0.6103, 2.11)T,α=1.882,U_f^-=\bigl(0.8192,\ 0.6082,\ 2.118\bigr)^T,\quad U_f^+=\bigl(0.8157,\ 0.6103,\ 2.11\bigr)^T,\quad\alpha=1.882,

两个 Jacobian 为

and its two Jacobians are

∂H^∂Uf−=[0.93980.50092.636×10−4−0.21971.5350.1998−1.2651.6471.461],∂H^∂Uf+=[−0.94250.50092.649×10−4−0.2231−0.34310.1998−1.2741.645−0.4168].\frac{\partial\widehat H}{\partial U_f^-}=\begin{bmatrix}0.9398&0.5009&2.636\times10^{-4}\\-0.2197&1.535&0.1998\\-1.265&1.647&1.461\end{bmatrix},\qquad\frac{\partial\widehat H}{\partial U_f^+}=\begin{bmatrix}-0.9425&0.5009&2.649\times10^{-4}\\-0.2231&-0.3431&0.1998\\-1.274&1.645&-0.4168\end{bmatrix}.

两者主要差在对角线上,约为 ±α/2\pm\alpha/2;秩一项的大小约为 12∥ΔU∥ ∥α−∥=0.0044\tfrac12\|\Delta U\|\,\|\alpha_-\|=0.0044(这里 ∥ΔU∥=0.0095\|\Delta U\|=0.0095,而整个矩阵的范数约为 3.23.2),在光滑解上很小,但不能省略。第 8 节用这两个矩阵做前向,第 9 节用它们的转置做伴随;两处都与 core.js 的运算记录(tape)比较,差在舍入量级。

They differ mainly on the diagonal, by about ±α/2\pm\alpha/2; the rank-one term has the size 12∥ΔU∥ ∥α−∥=0.0044\tfrac12\|\Delta U\|\,\|\alpha_-\|=0.0044 (here ∥ΔU∥=0.0095\|\Delta U\|=0.0095, while the whole matrix has norm about 3.23.2), small on a smooth solution but not negligible. Section 8 carries tangents through these two matrices and Section 9 their transposes; both agree with the operation tape of core.js to rounding.

到这里,每个内部面都由两侧的重构状态得到通量。两个端点不同:格点页的端点控制体只有半宽,外侧没有状态;按第 1 节的特征计数,入口只能给两个数据、出口只能给一个。第 4 节说明格点页的做法:端面直接用端点状态的物理通量,残差装配完成后,再用三条边界条件替换入口的两行和出口的一行。

Every interior face now gets its flux from the reconstructed states on its two sides. The two ends differ: on the node page the end volumes have half width and nothing lies outside them, and by the characteristic count of Section 1 the inlet can take only two data and the outlet only one. Section 4 shows what the node page does: the end faces use the physical flux of the end state, and after the residual is assembled, three boundary conditions replace two inlet rows and one outlet row.

第 4 节Section 4

边界条件:给定哪些量,哪些量由内部决定

Boundary conditions: prescribed and outgoing information

第 1 节由三条特征速度 u−c、u、u+c(通量 Jacobian ∂F/∂U 的三个特征值;附录 A.1 写出这个 3×3 矩阵)说明了边界条件的个数:左端两条进入计算域、右端一条进入计算域,所以给定入口总压 p₀、总温 T₀ 和出口静压 pbp_b。把入口全部三个守恒量再加出口压力一起钉死,会过度约束问题。本节说明离散格式怎样施加这三个条件。

Section 1 counted the boundary conditions from the three characteristic speeds u−c, u and u+c (the eigenvalues of the flux Jacobian ∂F/∂U; Appendix A.1 writes out this 3×3 matrix): two characteristics enter at the left and one at the right, so the inlet total pressure p₀, total temperature T₀ and outlet static pressure pbp_b are prescribed. Prescribing all three inlet conservative values as well as exit pressure would overconstrain the problem. This section shows how the discretisation imposes these three conditions.

T0=T(1+γ−12M2),p0=p(1+γ−12M2)γ/(γ−1),M=u/c.T_0=T\left(1+\frac{\gamma-1}{2}M^2\right),\quad p_0=p\left(1+\frac{\gamma-1}{2}M^2\right)^{\gamma/(\gamma-1)},\quad M=u/c.

格点页把入口节点的第 0、1 行(质量行与动量行)换成 p₀(U₀)−p₀ 和 T₀(U₀)−T₀,把出口节点的质量行(第 3N 行)换成 p(UNU_N)−pbp_b;右边的 p₀、T₀、pbp_b 是给定的边界数据,也就是设计变量。其余行仍为积分守恒方程。被覆盖的旧行必须在前向和反向计算中一并移除。

At the inlet node, rows 0 and 1 (the mass and momentum rows) become p₀(U₀)−p₀ and T₀(U₀)−T₀; the mass row of the outlet node (row 3N) becomes p(UNU_N)−pbp_b; the p₀, T₀ and pbp_b on the right are the prescribed boundary data, which are design variables. Remaining rows retain integral balances. Both tangent and reverse calculations must remove the overwritten contributions.

格点页出口的总温、总压与入口略有差别(默认 N=12、二阶时出口总温约 1.034)。原因在边界行:两端的质量行都换成了边界条件,没有方程把两端的质量流量 qm=ρuAq_m=\rho uA 联系起来;保留的能量行相加后要求两端能量通量相等,所以 T0(1)/T0(0)=qm(0)/qm(1)T_0(1)/T_0(0)=q_m(0)/q_m(1)。这是离散误差,随网格加密以二阶减小:二阶格式在 N=12、24、48、96 时 T0(1)−T0(0)T_0(1)-T_0(0) 为 3.4×10⁻²、8.1×10⁻³、2.0×10⁻³、5.0×10⁻⁴。

On the node page the outlet total temperature and pressure differ slightly from the inlet values (outlet total temperature about 1.034 at the default N=12, second order). The cause is the boundary rows: both end mass rows are replaced by boundary conditions, so no equation ties the two end mass flows qm=ρuAq_m=\rho uA together, while the kept energy rows sum to equal end energy fluxes, giving T0(1)/T0(0)=qm(0)/qm(1)T_0(1)/T_0(0)=q_m(0)/q_m(1). This is a discretisation error that decreases at second order: T0(1)−T0(0)T_0(1)-T_0(0) is 3.4×10⁻², 8.1×10⁻³, 2.0×10⁻³, 5.0×10⁻⁴ at N = 12, 24, 48, 96 for the second-order scheme.

B˙=BUU˙+BDD˙,Uˉ+=BUTBˉ,Dˉ+=BDTBˉ.\dot B=B_U\dot U+B_D\dot D,\qquad \bar U\mathrel{\text{+=}}B_U^T\bar B,\quad\bar D\mathrel{\text{+=}}B_D^T\bar B.

上式中的点表示前向(tangent)导数,横线表示反向(adjoint)累加权重,记号在第 8 节说明。边界也是计算图的一部分。固定设计参数不等于固定边界状态:即使 p₀、T₀、pbp_b 不变,内部扰动仍通过出域特征改变边界状态。

In the formula above a dot marks a forward (tangent) derivative and a bar a reverse (adjoint) accumulation weight; Section 8 introduces the notation. Boundaries belong to the computational graph. Fixed boundary parameters do not imply fixed boundary states: interior perturbations still propagate through outgoing characteristics.

边界行的导数

The derivatives of the boundary rows

格点页的边界条件是装配后替换的三行,而不是通量闭合:第 0、1 行(节点 0 的质量行和动量行)换成 p0(U0)−p0p_0(U_0)-p_0 与 T0(U0)−T0T_0(U_0)-T_0,第 3N3N 行(节点 NN 的质量行)换成 p(UN)−pbp(U_N)-p_b,其中 T=p/ρT=p/\rho、T0(U)=T+u2/7T_0(U)=T+u^2/7、p0(U)=p (T0/T)3.5p_0(U)=p\,(T_0/T)^{3.5}(γ=1.4\gamma=1.4,气体常数为 1)。它们对 UU 的行向量为

On the node page the boundary conditions are three rows replaced after assembly, not flux closures: rows 0 and 1 (the mass and momentum rows of node 0) become p0(U0)−p0p_0(U_0)-p_0 and T0(U0)−T0T_0(U_0)-T_0, and row 3N3N (the mass row of node NN) becomes p(UN)−pbp(U_N)-p_b, with T=p/ρT=p/\rho, T0(U)=T+u2/7T_0(U)=T+u^2/7 and p0(U)=p (T0/T)3.5p_0(U)=p\,(T_0/T)^{3.5} (γ=1.4\gamma=1.4, gas constant 1). Their rows with respect to UU are

TU=pUρ−pρ2(1,0,0),(T0)U=TU+2u7uU,(p0)U=p0(pUp+3.5((T0)UT0−TUT)),pU(UN).T_U=\frac{p_U}\rho-\frac p{\rho^2}(1,0,0),\qquad(T_0)_U=T_U+\frac{2u}7u_U,\qquad(p_0)_U=p_0\Bigl(\frac{p_U}p+3.5\Bigl(\frac{(T_0)_U}{T_0}-\frac{T_U}T\Bigr)\Bigr),\qquad p_U(U_N).

每一行只依赖一个设计变量,系数为 −1-1,而其他行都不依赖 p0p_0、T0T_0、pbp_b。于是 g=QDT+RDTψg=Q_D^T+R_D^T\psi 中这三个分量直接就是被替换行的乘子:

Each row depends on one design variable, with coefficient −1-1, and no other row depends on p0p_0, T0T_0 or pbp_b. In g=QDT+RDTψg=Q_D^T+R_D^T\psi these three components are therefore the multipliers of the replaced rows:

gp0=−ψ0=−0.01363,gT0=−ψ1=1.1×10−13,gpb=−ψ3N=0.02368g_{p_0}=-\psi_0=-0.01363,\qquad g_{T_0}=-\psi_1=1.1\times10^{-13},\qquad g_{p_b}=-\psi_{3N}=0.02368

(页首默认设置;第 12 节说明 gT0g_{T_0} 在精确算术中为零)。这与附录 A.4 的连续结论同构:那里边界数据的梯度由边界乘子给出,(p0,T0)(p_0,T_0) 的梯度是 μ0\mu_0,pbp_b 的梯度是 −μ1-\mu_1(μ1\mu_1 前有负号)。替换是覆盖赋值,所以前向直接覆盖这三行的切向量,伴随先把这三行的种子送入约束,再把它们清零,被覆盖的守恒行就不会再传到面通量:

(the defaults at the top of the page; Section 12 explains why gT0g_{T_0} is zero in exact arithmetic). This mirrors the continuous result of Appendix A.4, where the gradients with respect to the boundary data are given by boundary multipliers: that with respect to (p0,T0)(p_0,T_0) is μ0\mu_0 and that with respect to pbp_b is −μ1-\mu_1 (with a minus sign in front of μ1\mu_1). A replacement is an overwrite, so the tangent overwrites the tangents of these three rows, and the adjoint first sends their seeds into the constraints and then zeroes them, so the overwritten balance rows never reach the face fluxes:

R˙0=(p0)UU˙0−p˙0,R˙1=(T0)UU˙0−T˙0,R˙3N=pUU˙N−p˙b;Uˉ0+=(p0)UTRˉ0+(T0)UTRˉ1,UˉN+=pUTRˉ3N,pˉ0-=Rˉ0,Tˉ0-=Rˉ1,pˉb-=Rˉ3N,Rˉ0,Rˉ1,Rˉ3N←0.\begin{aligned}&\dot R_0=(p_0)_U\dot U_0-\dot p_0,\quad\dot R_1=(T_0)_U\dot U_0-\dot T_0,\quad\dot R_{3N}=p_U\dot U_N-\dot p_b;\\&\bar U_0\mathrel{\text{+=}}(p_0)_U^T\bar R_0+(T_0)_U^T\bar R_1,\quad\bar U_N\mathrel{\text{+=}}p_U^T\bar R_{3N},\quad\bar p_0\mathrel{\text{-=}}\bar R_0,\quad\bar T_0\mathrel{\text{-=}}\bar R_1,\quad\bar p_b\mathrel{\text{-=}}\bar R_{3N},\quad\bar R_0,\bar R_1,\bar R_{3N}\leftarrow0.\end{aligned}

图 4 把这三行排成一条运算记录,列出页首默认设置下每个量的值、设计种子 p˙b=1\dot p_b=1(配第 10 节解出的状态切向量)下的切向量,以及种子 Rˉ=ψ\bar R=\psi、Qˉ=1\bar Q=1 反传到它的横线量。解出的切向量使三行的 R˙\dot R 都为零:出口压力的切向量恰为 p˙(UN)=p˙b=1\dot p(U_N)=\dot p_b=1。

Figure 4 lays the three rows out as a tape and lists, at the defaults, every quantity’s value, its tangent for the design seed p˙b=1\dot p_b=1 (with the state tangent solved in Section 10), and the bar the seeds Rˉ=ψ\bar R=\psi, Qˉ=1\bar Q=1 send back to it. The solved tangent makes R˙\dot R of all three rows zero: the outlet pressure has exactly the tangent p˙(UN)=p˙b=1\dot p(U_N)=\dot p_b=1.

图 4 · 格点页三条边界行的运算记录:每条语句的数值、切向量与伴随语句数值前向(切向量)伴随(横线量)U₀(0.922, 0.435, 2.34)(0.71, −1.7, 1.52)(−0.00104, 0.00442, 0.00543)UN(0.898, 0.421, 2.35)(0.697, −1.66, 1.64)(−0.00104, 0.00444, −0.00947)p₀, T₀, pb1, 1, 0.90, 0, 1−0.0136, 1.09×10⁻¹³, 0.0237① T = p/ρ (U₀)0.96820.298−0.00157② T₀(U₀) = T + u²/7100.0477③ p₀(U₀) = p (T₀/T)3.51−8.88×10⁻¹⁶0.0136④ 行 0 ← p₀(U₀) − p₀6.66×10⁻¹⁶−8.88×10⁻¹⁶0.0136⑤ 行 1 ← T₀(U₀) − T₀2.22×10⁻¹⁶0−1.09×10⁻¹³⑥ 行 36 ← p(UN) − pb−6.66×10⁻¹⁵−6.66×10⁻¹⁶−0.0237↓ 前向:按原始顺序↑ 伴随:逆序,横线量用 += 累加
图 4 · 格点页三条边界行的运算记录(页首默认设置)。每行一条语句,三列依次是数值、设计种子 p˙b=1\dot p_b=1 配第 10 节解出的状态切向量时的切向量(绿),以及种子 Rˉ=ψ\bar R=\psi、Qˉ=1\bar Q=1 反传到它的横线量(红)。④⑤⑥ 是覆盖赋值:伴随把 ψ\psi 的这三个分量送进约束后清零,三个设计变量得到 −ψ-\psi;解出的切向量使三行的 R˙\dot R 都为零。Uˉ0\bar U_0、UˉN\bar U_N 只列约束行这一路:这两个节点的状态还经面通量、源项与目标接收横线量,全部贡献之和是伴随方程的残差,接近零。
Figure 4 · The tape of the three boundary rows of the node page: value, tangent and adjoint of every statementstatementvaluetangentadjointU₀(0.922, 0.435, 2.34)(0.71, −1.7, 1.52)(−0.00104, 0.00442, 0.00543)UN(0.898, 0.421, 2.35)(0.697, −1.66, 1.64)(−0.00104, 0.00444, −0.00947)p₀, T₀, pb1, 1, 0.90, 0, 1−0.0136, 1.09×10⁻¹³, 0.0237① T = p/ρ (U₀)0.96820.298−0.00157② T₀(U₀) = T + u²/7100.0477③ p₀(U₀) = p (T₀/T)3.51−8.88×10⁻¹⁶0.0136④ row 0 ← p₀(U₀) − p₀6.66×10⁻¹⁶−8.88×10⁻¹⁶0.0136⑤ row 1 ← T₀(U₀) − T₀2.22×10⁻¹⁶0−1.09×10⁻¹³⑥ row 36 ← p(UN) − pb−6.66×10⁻¹⁵−6.66×10⁻¹⁶−0.0237↓ tangent: primal order↑ adjoint: reverse order, bars accumulate with +=
Figure 4 · The tape of the three boundary rows of the node page (the defaults at the top of the page). One statement per row; the columns are its value, its tangent for the design seed p˙b=1\dot p_b=1 with the state tangent solved in Section 10 (green), and the bar the seeds Rˉ=ψ\bar R=\psi, Qˉ=1\bar Q=1 send back to it (red). ④⑤⑥ are overwrites: the adjoint sends these three entries of ψ\psi into the constraints and zeroes them, and the three design variables get −ψ-\psi; the solved tangent makes R˙\dot R of all three rows zero. Uˉ0\bar U_0 and UˉN\bar U_N list only the constraint-row path: these two states also receive bars through the face fluxes, the sources and the objective, and the sum of all their contributions is the residual of the adjoint equation, which is close to zero.
第 5 节Section 5

残差:面循环、体积分与强边界行

Residual: face loops, volume quadrature and boundary rows

先固定离散问题与符号

Fix the discrete problem and its notation

本页每个节点存储 3 个未知量。以下用 𝐔\mathbf U 表示把所有位置、所有分量按代码顺序排成的列向量。它是一个有限维向量,不是连续函数。物理方程中的空间导数只用于说明模型;求离散伴随时,求导对象是本页实际装配的有限维函数。

Each node stores 3 unknowns. Below, 𝐔\mathbf U stacks all locations and components in code order. It is a finite-dimensional vector, not a continuous function. Spatial derivatives in the physical equations explain the model; discrete differentiation acts on the actual finite-dimensional functions assembled on this page.

𝐔∈ℝnu,D∈ℝna,\mathbf U\in\mathbb R^{n_u},\quad D\in\mathbb R^{n_a},
R:ℝnu×ℝna→ℝnu,Q:ℝnu×ℝna→ℝ.R:\mathbb R^{n_u}\times\mathbb R^{n_a}\to\mathbb R^{n_u},\quad Q:\mathbb R^{n_u}\times\mathbb R^{n_a}\to\mathbb R.

DD 是本页全部设计变量排成的向量:截面积 AkA_k、背压 pbp_b、入口总压 p0p_0 与总温 T0T_0。本页的 AA 是截面积,所以全局残差 Jacobian 记作 JJ,目标记作 QQ;局部通量 Jacobian ∂F/∂U\partial F/\partial U 又是另一个对象。每个位置的局部状态 UiU_i 是一个小列向量,全局的 𝐔\mathbf U 才是 Jacobian 的输入。

DD stacks all design variables of this page: the areas AkA_k, the back pressure pbp_b, the inlet total pressure p0p_0 and the total temperature T0T_0. On this page AA is the area, so the global residual Jacobian is JJ and the objective is QQ; a local flux Jacobian ∂F/∂U\partial F/\partial U is yet another object. A local state UiU_i is a small column vector; the global 𝐔\mathbf U is the input of the Jacobian.

J=∂R∂𝐔∈ℝnu×nu,RD=∂R∂D∈ℝnu×na,J=\frac{\partial R}{\partial \mathbf U}\in\mathbb R^{n_u\times n_u},\quad R_D=\frac{\partial R}{\partial D}\in\mathbb R^{n_u\times n_a},
QUT=(∂Q∂𝐔)T∈ℝnu,QDT=(∂Q∂D)T∈ℝna.Q_U^T=\Big(\frac{\partial Q}{\partial \mathbf U}\Big)^{T}\in\mathbb R^{n_u},\quad Q_D^T=\Big(\frac{\partial Q}{\partial D}\Big)^{T}\in\mathbb R^{n_a}.

这里 QUTQ_U^T、QDTQ_D^T 都是列向量,它们的转置才是偏导数行。求导采用自动微分(AD)的记号:程序变量 xx 上加点的 x˙\dot x 是前向(tangent)模式沿给定种子方向的导数,加横线的 xˉ\bar x 是反向(adjoint)模式累加到 xx 上的权重,例如 𝐔˙\dot{\mathbf U} 与 𝐔ˉ\bar{\mathbf U}。两者都是对本页有限维代码求导;变分符号 δ\delta 只用于附录 A、B 的连续推导里连续函数的变分。

Both QUTQ_U^T and QDTQ_D^T are columns; their transposes are the derivative rows. Derivatives use the notation of automatic differentiation (AD): a dot x˙\dot x on a program variable xx is its forward (tangent) derivative along the chosen seed direction, and a bar xˉ\bar x is the weight accumulated into xx in reverse (adjoint) mode, for example 𝐔˙\dot{\mathbf U} and 𝐔ˉ\bar{\mathbf U}. Both differentiate the finite-dimensional code of this page; the variation symbol δ\delta is kept for variations of continuous functions in the continuous derivations of Appendices A and B.

Ri 是积分残差,没有除以体积。内部面只算一次,向左控制体加 H、向右控制体减 H;随后从动量分量减去压力壁面源项。面积在各原始区间内为直线;每段采用两点 Gauss 积分。格点对偶体若跨越原始节点,必须先拆成两段,不能跨过 A′ 的跳变只做一次积分。

Ri is an integrated residual, without volume division. Evaluate each internal face once, adding H to the left volume and subtracting it from the right. Subtract the wall-pressure source from momentum afterwards. Area is linear on each primal interval. Apply two-point Gauss quadrature on each piece; split node-centred dual volumes at primal nodes where A′ changes.

Ri=Hi+1/2−Hi−1/2−e2Si,Si=∑q∈Viwqp(Ui(xq))A′(xq).R_i=H_{i+1/2}-H_{i-1/2}-e_2 S_i,\quad S_i=\sum_{q\in V_i}w_qp(U_i(x_q))A'(x_q).
R[:] = 0
for face f:
    UL, UR = reconstruct_and_apply_boundary(U, f)
    H = area[f] * numerical_flux(UL, UR)
    if L exists: R[L] += H
    if R exists: R[R] -= H
for volume i:
    S[i] = sum(wq * pressure(reconstruct(U,i,xq)) * area_derivative(xq))
    R[i, momentum] -= S[i]
if node_centred: replace_three_boundary_rows(R, U, D)

播放器按照真实装配顺序展示累加。最后的强边界行覆盖是一个单独步骤;在格点页,全域所有“最终残差行”相加不再等于边界通量差,因为其中三行已经不是守恒方程。

The player follows the actual assembly order, including a separate final boundary-row replacement. Summing all final rows on the node page no longer yields only boundary flux differences because three rows are constraints rather than conservation equations.

第 6 节Section 6

Jacobian 与 GS:精确算子和低阶近似各做什么

Jacobian and GS: exact operator versus low-order approximation

把所有残差堆成向量,Newton 修正满足 J₂ ΔU=−R₂,J₂=∂R₂/∂U。这里用 J₂ 表示所选离散阶数的精确 Jacobian;一阶选择时它就是 J₁。二阶重构扩展了依赖范围。程序不存储 J₂,而沿标量计算图实施 Jv 和 JTw。为解线性系统,单独建立一阶近似 P=J₁;P 的每个对角块是 3×3。

Stack the residuals into a vector. Newton corrections satisfy J₂ ΔU=−R₂, with J₂ denoting the exact Jacobian of the selected order (J₁ when first order is selected). Reconstruction broadens dependencies. The program applies Jv and JTw through a scalar computational graph without storing J₂. A separate first-order approximation P=J₁ supplies 3×3 diagonal blocks for linear solves.

P=diag⁡(Pii)+L+V,zi(k+1)=Pii−1(ri−∑j<iPijzj(k+1)−∑j>iPijzj(k)).P=\operatorname{diag}(P_{ii})+L+V,\qquad z_i^{(k+1)}=P_{ii}^{-1}\left(r_i-\sum_{j<i}P_{ij}z_j^{(k+1)}-\sum_{j>i}P_{ij}z_j^{(k)}\right).

一次 GS 扫描使用已更新的左侧块和旧的右侧块。每次预处理从零开始做 12 次扫描,记所得线性映射为 B。外层固定点更新 x←x+0.7 B(b−J₂x)。多次内扫描使二阶缺陷校正在本页网格上稳定;不能把“一次扫描”等同于已解完 Pz=r。伴随使用整个 B 的转置,而不只是把某个局部 3×3 块转置。

A GS sweep uses updated earlier blocks and previous later blocks. Each preconditioner application starts from zero and performs 12 sweeps, defining a linear map B. Outer defect correction updates x←x+0.7 B(b−J₂x). Multiple inner sweeps stabilise this example’s second-order correction; one sweep is not an exact solution of Pz=r. The adjoint uses the transpose of the entire B map, not merely transposed diagonal blocks.

z = 0
repeat 12 times:
    z += solve_lower_blocks(diag(P_ii)+L, rhs-P*z)
# Transposed map: reverse block order, transpose blocks and residual operator.
# Never replace the exact residual J2*x by P*x in the outer equation.

符号提示:本页 A(x) 表示截面积;线性系统中的残差 Jacobian 记作 J 或 JhJ_h,目标函数记作 Q(其他页面分别记作 A 与 J)。P 是低阶近似矩阵,B 是从零开始固定次数 GS 扫描形成的预处理映射,通常不等于精确 P⁻¹。

Notation: on this page A(x) denotes area; in a linear system the residual Jacobian is written J or JhJ_h, and the objective Q (A and J on the other pages). P is the low-order matrix; B is the fixed number of zero-start GS sweeps, generally not the exact inverse of P.

精确算子与低阶近似的块结构

The block structure of the exact operator and of the low-order matrix

图 5 画出上面两个矩阵。二阶重构让一个面读取 i−1i-1 到 i+2i+2 的存储状态(第 3 节),所以 JJ 的每个块行读 5 个块,共 59 个非零块;一阶近似 PP 只读相邻块,共 37 个。块 GS 精确地倒转 diag⁡(Pii)+L\operatorname{diag}(P_{ii})+L,VV 和 J−PJ-P 留给 12 次内扫描与外层迭代;转置时 LL 与 VV 交换位置,所以伴随的扫描方向相反(第 11 节)。格点页被替换的三行只依赖一个端点的状态。

Figure 5 draws the two matrices above. Second-order reconstruction makes a face read the stored states i−1i-1 to i+2i+2 (Section 3), so a block row of JJ reads 5 blocks, 59 nonzero blocks in all; the first-order PP reads only neighbouring blocks, 37 in all. Block GS inverts diag⁡(Pii)+L\operatorname{diag}(P_{ii})+L exactly and leaves VV and J−PJ-P to the 12 inner sweeps and the outer iteration; transposing swaps LL and VV, which is why the adjoint sweeps the other way (Section 11). On the node page the three replaced rows depend on the state of one end only.

图 5 · 精确 Jacobian 与一阶近似的块结构J:二阶精确 Jacobian(59 个非零块)P = J₁:一阶近似(37 个非零块)对角块 diag(Pii)下三角块 L(上游)上三角块 V(下游)颜色越深量级越大(对数刻度,4 档);细线划分 3×3 块;每行块读取 5 个块(J)与 3 个块(P)红框:被三条边界条件替换的第 0、1、36 行
图 5 · 页首默认设置的收敛解处,二阶精确 Jacobian JJ(左)与一阶近似 P=J1P=J_1(右)的块结构。每个小方块是一个非零元素,细线划分 3×3 块;紫色是对角块 diag⁡(Pii)\operatorname{diag}(P_{ii}),蓝色是下三角块 LL(依赖上游状态),琥珀色是上三角块 VV(依赖下游状态),颜色越深量级越大。JJ 的每个块行读 5 个块,PP 读 3 个。红框是被三条边界条件替换的行。
Figure 5 · The block structure of the exact Jacobian and of the first-order matrixJ: exact second-order Jacobian (59 nonzero blocks)P = J₁: first-order matrix (37 nonzero blocks)diagonal blocks diag(Pii)lower blocks L (upstream)upper blocks V (downstream)darker = larger magnitude (log scale, 4 levels); thin lines mark the 3×3 blocks; a block row reads 5 blocks (J) and 3 (P)red frames: rows 0, 1, 36, replaced by the three boundary conditions
Figure 5 · The block structure of the exact second-order Jacobian JJ (left) and of the first-order matrix P=J1P=J_1 (right) at the converged state of the defaults at the top of the page. Each small square is one nonzero entry, and thin lines mark the 3×3 blocks; violet: the diagonal blocks diag⁡(Pii)\operatorname{diag}(P_{ii}), blue: the lower blocks LL (upstream states), amber: the upper blocks VV (downstream states); darker means larger. A block row of JJ reads 5 blocks, one of PP 3. Red frames: the rows replaced by the three boundary conditions.
第 7 节Section 7

目标函数:壁面推力与压力反设计

Objectives: wall thrust and pressure inverse design

目标是一个标量,所以其伴随只需求一次,就能得到全部面积和边界参数的梯度。壁面推力取 ∫pA′dx;本例入口出口面积相同,光滑等熵参考解的净推力为零,但粗网格离散值不必为零。反设计目标比较当前压力和固定目标压力,目标面积固定,以便改变当前收缩深度时仍比较同一个目标流道。

A scalar objective needs one adjoint solve for gradients with respect to all area and boundary parameters. Wall thrust is ∫pA′dx. Equal inlet and exit areas give zero thrust for the smooth isentropic reference, although a coarse discrete result need not vanish. Inverse design compares current pressure with a fixed target pressure obtained from a fixed target duct, even when the current contraction depth changes.

QT=∑qwqpqAq′,QI=12∑qwqAq(pq−pq⋆)2,A⋆(x)=(1−0.3sin⁡2πx)(1−0.04sin⁡πx).Q_T=\sum_qw_qp_qA'_q,\qquad Q_I=\frac12\sum_qw_q A_q(p_q-p_q^\star)^2,\quad A^\star(x)=(1-0.3\sin^2\pi x)(1-0.04\sin\pi x).

目标状态由同一阶数、同一种网格的独立收敛计算生成,并在所有扰动计算中保持不变。重构后再求压力;目标压力也重构到同一积分点。若每次扰动都重新生成目标,验证的就变成了另一个函数。在目标流道及目标状态上,Q 与总梯度应归零。

Generate the target with a separate converged solve using the same order and grid, then hold it fixed during perturbations. Reconstruct both states to the same quadrature locations before computing pressure. Regenerating the target after every perturbation would differentiate a different objective. At the target geometry and state, both Q and its total gradient should vanish.

从离散目标得到伴随右端

From a discrete objective to the adjoint right-hand side

不能只对连续积分写一个形式导数就结束。先确定代码实际使用的求积点、权重、重构状态及边界值,再对这个标量表达式求导。一般的求积和及其链式法则是:

A formal derivative of a continuous integral is not enough. First identify the quadrature points, weights, reconstructed states and boundary values actually used by the code, then differentiate that scalar expression. A general quadrature sum and its chain rule are:

Q=∑qωq(D) θq(𝐔,D),Q=\sum_q\omega_q(D)\,\theta_q(\mathbf U,D),
Q˙=∑q[ωq θ˙q+θq ω˙q].\dot Q=\sum_q\left[\omega_q\,\dot\theta_q+\theta_q\,\dot\omega_q\right].

平方跟踪目标是它的特例:θq=12eq2\theta_q=\tfrac12e_q^2,eqe_q 是被跟踪量减去它的目标值,于是 θ˙q=eq e˙q\dot\theta_q=e_q\,\dot e_q;e˙q\dot e_q 在被跟踪量的切向量之外,只多出目标值本身的切向量(目标冻结时为零)。

A squared tracking objective is the special case θq=12eq2\theta_q=\tfrac12e_q^2, where eqe_q is the tracked quantity minus its target; then θ˙q=eq e˙q\dot\theta_q=e_q\,\dot e_q, and e˙q\dot e_q adds to the tangent of the tracked quantity only the tangent of the target itself (zero for a frozen target).

本页的两个目标都是这种求积和。推力 QT=∑qwqpqAq′Q_T=\sum_qw_qp_qA'_q 取 θq=pqAq′\theta_q=p_qA'_q,所以 θ˙q=Aq′ p˙q+pq A˙q′\dot\theta_q=A'_q\,\dot p_q+p_q\,\dot A'_q。逆设计 QI=12∑qwqAq(pq−pq⋆)2Q_I=\tfrac12\sum_qw_qA_q(p_q-p_q^\star)^2 是带面积因子的平方跟踪型,θq=12Aq(pq−pq⋆)2\theta_q=\tfrac12A_q(p_q-p_q^\star)^2;目标压力 pq⋆p_q^\star 冻结,截面积 AqA_q 却随设计变化,所以上面的 θ˙q=eq e˙q\dot\theta_q=e_q\,\dot e_q 要乘上面积,再多一项:θ˙q=Aq(pq−pq⋆) p˙q+12(pq−pq⋆)2 A˙q\dot\theta_q=A_q(p_q-p_q^\star)\,\dot p_q+\tfrac12(p_q-p_q^\star)^2\,\dot A_q。第一项经过状态(p˙q\dot p_q 是求积点上压力的切向量),进入 QUTQ_U^T;第二项是截面积经过目标本身的直接路径,进入 QDTQ_D^T。

Both objectives on this page are such quadrature sums. The thrust QT=∑qwqpqAq′Q_T=\sum_qw_qp_qA'_q takes θq=pqAq′\theta_q=p_qA'_q, so θ˙q=Aq′ p˙q+pq A˙q′\dot\theta_q=A'_q\,\dot p_q+p_q\,\dot A'_q. The inverse design QI=12∑qwqAq(pq−pq⋆)2Q_I=\tfrac12\sum_qw_qA_q(p_q-p_q^\star)^2 is a squared tracking objective with an area factor, θq=12Aq(pq−pq⋆)2\theta_q=\tfrac12A_q(p_q-p_q^\star)^2; the target pressure pq⋆p_q^\star is frozen but the area AqA_q changes with the design, so the θ˙q=eq e˙q\dot\theta_q=e_q\,\dot e_q above is multiplied by the area and gains a term: θ˙q=Aq(pq−pq⋆) p˙q+12(pq−pq⋆)2 A˙q\dot\theta_q=A_q(p_q-p_q^\star)\,\dot p_q+\tfrac12(p_q-p_q^\star)^2\,\dot A_q. The first term runs through the state (p˙q\dot p_q is the tangent of the pressure at the quadrature point) and enters QUTQ_U^T; the second is the direct path of the area through the objective itself and enters QDTQ_D^T.

求 QUTQ_U^T 时固定所有设计变量;求 QDTQ_D^T 时固定全部离散状态槽位。本页轴向网格固定,采样点与求积权重 wqw_q 不随设计变化;但被积函数里的截面积(QIQ_I 的面积因子 AqA_q、QTQ_T 的斜率 Aq′A'_q)随设计变化,这部分导数必须保留。目标中的体积权重已经进入 QUTQ_U^T,不应在伴随右端再乘一次体积。

Hold every design variable fixed for QUTQ_U^T, and every stored state entry fixed for QDTQ_D^T. The axial mesh of this page is fixed, so the samples and the quadrature weights wqw_q do not move; the areas inside the integrands (the area factor AqA_q of QIQ_I, the slope Aq′A'_q of QTQ_T) do change with the design, and those derivatives must be kept. Objective volume weights already enter QUTQ_U^T and must not be multiplied into the adjoint right-hand side again.

伴随右端本身也可以由一次反向扫描得到:以 Qˉ=1\bar Q=1 为种子,把上面的求积和逐项反过来执行,

The adjoint right-hand side is itself obtained by a reverse sweep: seed Qˉ=1\bar Q=1 and run the terms of the quadrature sum above backwards,

Qˉ=1,θˉq=ωq Qˉ,𝐔ˉ+=(∂θq∂𝐔)Tθˉq,\bar{Q}=1,\qquad \bar\theta_q=\omega_q\,\bar{Q},\qquad \bar{\mathbf U}\mathrel{\text{+=}}\Big(\frac{\partial \theta_q}{\partial \mathbf U}\Big)^T\bar\theta_q,
Dˉ+=(∂θq∂D)Tθˉq+θq(∂ωq∂D)TQˉ.\bar D\mathrel{\text{+=}}\Big(\frac{\partial \theta_q}{\partial D}\Big)^T\bar\theta_q+\theta_q\Big(\frac{\partial\omega_q}{\partial D}\Big)^T\bar{Q}.

从 𝐔ˉ←0\bar{\mathbf U}\leftarrow0、Dˉ←0\bar D\leftarrow0 开始,扫完全部求积点后 𝐔ˉ=QUT\bar{\mathbf U}=Q_U^T、Dˉ=QDT\bar D=Q_D^T:一个状态被多个求积点读取,所以要先置零、再累加。前向的 Q˙\dot Q 与反向的这两个结果满足同一个对偶关系 Qˉ Q˙=𝐔ˉT𝐔˙+DˉTD˙\bar Q\,\dot Q=\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D。

Starting from 𝐔ˉ←0\bar{\mathbf U}\leftarrow0 and Dˉ←0\bar D\leftarrow0, after all quadrature points 𝐔ˉ=QUT\bar{\mathbf U}=Q_U^T and Dˉ=QDT\bar D=Q_D^T: a state is read by several quadrature points, so its adjoint is zeroed first and then accumulated. The forward Q˙\dot Q and these two reverse results satisfy one duality, Qˉ Q˙=𝐔ˉT𝐔˙+DˉTD˙\bar Q\,\dot Q=\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D.

两个目标的偏导数

The partial derivatives of the two objectives

两个目标都是第 5 节 Gauss 点上的求和,所以它们的偏导数由与源项相同的局部量组成。记 Uq=∑jWqjUjU_q=\sum_jW_{qj}U_j(控制体在 xqx_q 处的重构,WqjW_{qj} 是它的权重),pq=p(Uq)p_q=p(U_q),Gq=(Ak+1−Ak)/hG_q=(A_{k+1}-A_k)/h 是 xqx_q 所在区间 kk 的斜率,Aq=(1−tq)Ak+tqAk+1A_q=(1-t_q)A_k+t_qA_{k+1}。固定 DD 对状态求导,固定 𝐔\mathbf U 对设计求导:

Both objectives are sums over the Gauss points of Section 5, so their partial derivatives are made of the same local pieces as the source. Write Uq=∑jWqjUjU_q=\sum_jW_{qj}U_j (the reconstruction of the volume at xqx_q, with weights WqjW_{qj}), pq=p(Uq)p_q=p(U_q), Gq=(Ak+1−Ak)/hG_q=(A_{k+1}-A_k)/h the slope on the interval kk of xqx_q, and Aq=(1−tq)Ak+tqAk+1A_q=(1-t_q)A_k+t_qA_{k+1}. Holding DD fixed for the state derivatives and 𝐔\mathbf U fixed for the design derivatives:

∂QT∂Uj=∑qwqGq pU(Uq) Wqj,∂QT∂Ak=∑qwqpq∂Gq∂Ak,∂Gq∂Ak=−1h, ∂Gq∂Ak+1=1h;∂QI∂Uj=∑qwqAq(pq−pq⋆) pU(Uq) Wqj,∂QI∂Ak=∑q12wq(pq−pq⋆)2∂Aq∂Ak.\begin{aligned}&\frac{\partial Q_T}{\partial U_j}=\sum_qw_qG_q\,p_U(U_q)\,W_{qj},&&\frac{\partial Q_T}{\partial A_k}=\sum_qw_qp_q\frac{\partial G_q}{\partial A_k},\quad\frac{\partial G_q}{\partial A_k}=-\frac1h,\ \frac{\partial G_q}{\partial A_{k+1}}=\frac1h;\\&\frac{\partial Q_I}{\partial U_j}=\sum_qw_qA_q(p_q-p_q^\star)\,p_U(U_q)\,W_{qj},&&\frac{\partial Q_I}{\partial A_k}=\sum_q\tfrac12w_q(p_q-p_q^\star)^2\frac{\partial A_q}{\partial A_k}.\end{aligned}

两个目标都不直接读取 pbp_b、p0p_0、T0T_0:QDQ_D 的这三个分量为零,对应的梯度完全来自 RDTψR_D^T\psi(第 4、12 节)。目标压力 pq⋆p_q^\star 冻结,没有切向量。用上面一般求积和的记号,反设计是按截面积加权的跟踪项 θq=12Aq(pq−pq⋆)2\theta_q=\tfrac12A_q(p_q-p_q^\star)^2,所以 θ˙q=Aq(pq−pq⋆) p˙q+12(pq−pq⋆)2A˙q\dot\theta_q=A_q(p_q-p_q^\star)\,\dot p_q+\tfrac12(p_q-p_q^\star)^2\dot A_q;第二项是面积梯度的“目标路径”(图 11),默认设置下它的最大分量只有 3.5×10−53.5\times10^{-5},因为 pp 与 p⋆p^\star 很接近。第 9 节以 Qˉ=1\bar Q=1 为种子的反向扫描执行的正是这些语句:pˉq+=Qˉ wqAq(pq−pq⋆)\bar p_q\mathrel{\text{+=}}\bar Q\,w_qA_q(p_q-p_q^\star)(推力为 Qˉ wqGq\bar Q\,w_qG_q),再经 Uˉq=pUTpˉq\bar U_q=p_U^T\bar p_q 与重构权重散回存储状态。

Neither objective reads pbp_b, p0p_0 or T0T_0 directly: those three entries of QDQ_D are zero, and their gradients come entirely from RDTψR_D^T\psi (Sections 4 and 12). The target pressure pq⋆p_q^\star is frozen and has no tangent. In the notation of the general quadrature sum above, the inverse design is a tracking term weighted by the area, θq=12Aq(pq−pq⋆)2\theta_q=\tfrac12A_q(p_q-p_q^\star)^2, so θ˙q=Aq(pq−pq⋆) p˙q+12(pq−pq⋆)2A˙q\dot\theta_q=A_q(p_q-p_q^\star)\,\dot p_q+\tfrac12(p_q-p_q^\star)^2\dot A_q; the second term is the “objective path” of the area gradient (Figure 11), whose largest entry is only 3.5×10−53.5\times10^{-5} at the defaults because pp is close to p⋆p^\star. The reverse sweep of Section 9 with the seed Qˉ=1\bar Q=1 performs exactly these statements: pˉq+=Qˉ wqAq(pq−pq⋆)\bar p_q\mathrel{\text{+=}}\bar Q\,w_qA_q(p_q-p_q^\star) (for the thrust Qˉ wqGq\bar Q\,w_qG_q), then Uˉq=pUTpˉq\bar U_q=p_U^T\bar p_q and the reconstruction weights scatter it to the stored states.

第 8 节Section 8

前向传播:把每一步变成乘积法则

Tangent propagation: apply the product rule at every step

本节实验:一次 tangent 计算给出残差对指定方向的变化,不是已经求出了状态对设计的敏感度。比较第 10 节的线性求解。

Experiment: One tangent application gives the residual change along a supplied direction. It is not yet the solved state sensitivity; compare the linear solve in Section 10.

给状态切向量 U˙=v\dot U=v 和设计切向量 D˙\dot D,前向传播同时给出 R˙=RUv+RDD˙\dot R=R_Uv+R_D\dot D 与 Q˙=QUv+QDD˙\dot Q=Q_Uv+Q_D\dot D。先对重构加权和求导,再经过压力、声速、边界和通量,最后沿同样的面循环累加。对每个标量操作保存其输入和局部偏导,这就是本页自行实现的前向计算图。

For a state tangent U˙=v\dot U=v and a design tangent D˙\dot D, the tangent returns R˙=RUv+RDD˙\dot R=R_Uv+R_D\dot D and Q˙=QUv+QDD˙\dot Q=Q_Uv+Q_D\dot D. Differentiate reconstruction, pressure, sound speed, boundary closure and flux, then scatter in the primal face order. The self-contained scalar graph stores inputs and local derivatives for each arithmetic operation.

记号:x˙\dot x 是离散程序变量 xx 的前向(tangent)导数,xˉ\bar x 是它的反向(adjoint)累加权重,二者都是对本页离散代码求导;δ\delta 只用于附录 A 与附录 B 中连续函数的变分。伪代码与执行演示沿用 Tapenade 的命名:后缀 _d 表示 x˙\dot x(x_d)。Tapenade 用后缀 _b 表示 xˉ\bar x,但本页的 pbp_b 是背压,所以这里写作 x_bar。

Notation: x˙\dot x is the forward-mode (tangent) derivative of a discrete program variable xx and xˉ\bar x its reverse-mode (adjoint) accumulation weight; both differentiate this page’s discrete code. δ\delta is kept for variations of continuous functions in Appendices A and B. Pseudo-code and the execution players follow Tapenade’s names: the suffix _d marks x˙\dot x (x_d). Tapenade marks xˉ\bar x with _b, but on this page pbp_b is the back pressure, so the code here writes x_bar.

p˙=(γ−1)(e˙−um˙+u22ρ˙),c˙=c2(p˙p−ρ˙ρ).\dot p=(\gamma-1)\left(\dot e-u\dot m+\frac{u^2}{2}\dot\rho\right),\quad\dot c=\frac{c}{2}\left(\frac{\dot p}{p}-\frac{\dot\rho}{\rho}\right).
H˙=A˙fH^+Af[FU−U˙−+FU+U˙+2−α˙2(U+−U−)−α2(U˙+−U˙−)].\dot H=\dot A_f\widehat H+A_f\left[\frac{F_U^-\dot U^-+F_U^+\dot U^+}{2}-\frac{\dot\alpha}{2}(U^+-U^-)-\frac\alpha2(\dot U^+-\dot U^-)\right].

其中 Ĥ=HfH_f/AfA_f 是方括号内、单位面积上的数值通量。

Here Ĥ=HfH_f/AfA_f is the numerical flux per unit area inside the brackets.

S˙i=∑qwq{Aq′pUU˙i(xq)+pqA˙q′}.\dot S_i=\sum_qw_q\{A'_q p_U\dot U_i(x_q)+p_q\dot A'_q\}.
for operation in forward_order:
    z_d = sum(local_partial[z,input] * input_d)
# e.g. z=x*y: z_d=y*x_d+x*y_d
# reconstruction: Uf_d=sum(weight[j]*U_d[j])
# final rows use the differentiated boundary constraints

局部导数怎样组成全局 Jacobian

How local derivatives form the global Jacobian

固定网格和当前可微分支,把一个面的所有输入记为 zfz_f;它包括两侧的重构状态与面面积(边界面只读相邻的那一个状态与它的面积:边界数据 pbp_b、p0p_0、T0T_0 进入被替换的行,不进入任何面)。令 TfT_f、KfK_f 分别是 zfz_f 对存储状态和设计变量的导数,Hz,fH_{z,f} 是局部通量 Jacobian。于是每个残差行的导数按同一个有向面关联矩阵 ℬ\mathcal B 装配,这里的求和包括相应的边界通量映射(本页的这些求和是面循环的,即覆盖之前的残差 RrawR_{raw} 的);被替换的边界行另按第 9 节处理。前向模式按计算顺序传播切向量,状态种子是 v≡𝐔˙v\equiv\dot{\mathbf U},设计种子是 D˙\dot D:

At fixed mesh and differentiable branch, let zfz_f collect all face inputs: the reconstructed states of both sides and the face area (a boundary face reads only the state next to it and its area: the boundary data pbp_b, p0p_0 and T0T_0 enter the replaced rows, not the faces). Let TfT_f and KfK_f be the derivatives of zfz_f with respect to the stored states and the design variables, and Hz,fH_{z,f} the local flux Jacobian. Assemble each residual derivative with the same oriented face incidence ℬ\mathcal B, including the boundary flux maps (on this page these sums are those of the face loop, that is of the residual RrawR_{raw} before any row is overwritten); replaced boundary rows are handled in Section 9. Forward mode carries the tangents in the order of the computation, with the state seed v≡𝐔˙v\equiv\dot{\mathbf U} and the design seed D˙\dot D:

z˙f=Tfv+KfD˙,H˙f=Hz,fz˙f,\dot z_f=T_fv+K_f\dot D,\quad\dot H_f=H_{z,f}\dot z_f,
R˙raw,i=∑fℬifH˙f−e2S˙i,\dot R_{raw,i}=\sum_f\mathcal B_{if}\dot H_f-e_2\dot S_i,
(Jraw)ij=∑fℬifHz,f(Tf)j−e2∂Si∂Uj,(J_{raw})_{ij}=\sum_f\mathcal B_{if}H_{z,f}(T_f)_j-e_2\frac{\partial S_i}{\partial U_j},
(RD,raw)ik=∑fℬifHz,f(Kf)k−e2∂Si∂Dk.(R_{D,raw})_{ik}=\sum_f\mathcal B_{if}H_{z,f}(K_f)_k-e_2\frac{\partial S_i}{\partial D_k}.

无体源项时 Si=0S_i=0。本页的 SiS_i 是每个控制体上的一个标量,只进入第 ii 行的动量分量,e2e_2 标出这个分量。面面积插值与源项里的截面积随设计变化,这些依赖属于 zfz_f,不能只对状态求导。二阶重构使 TfT_f 连接更多邻居,因此精确 Jacobian 的模板变宽;matrix-free 仍只需依次执行这些局部乘积,不必存储全局矩阵。

Set Si=0S_i=0 when there is no volume source. On this page SiS_i is one scalar per control volume and enters only the momentum component of row ii, which e2e_2 marks. The areas in the face-area interpolation and in the source change with the design; that dependence belongs in zfz_f, so differentiating the states alone is insufficient. Second-order reconstruction connects TfT_f to more neighbours and widens the exact Jacobian stencil. Matrix-free evaluation performs these local products without storing the global matrix.

截面积的导数也是离散运算

Area differentiation is also a discrete operation

源项中写作 A′(xq)A'(x_q) 的量,是相邻设计截面积 AkA_k、Ak+1A_{k+1} 之间的分段线性斜率,不是对未知连续面积函数另求导。固定轴向网格时:

The A′(xq)A'(x_q) in the source is the piecewise linear slope between neighbouring design areas AkA_k and Ak+1A_{k+1}, not a separate derivative of an unknown continuous area function. On the fixed axial mesh:

GqA=Ak+1−Akh,G˙qA=A˙k+1−A˙kh,G_q^{A}=\frac{A_{k+1}-A_k}{h},\quad\dot G_q^{A}=\frac{\dot A_{k+1}-\dot A_k}{h},
Aˉk-=GˉqA/h,Aˉk+1+=GˉqA/h.\bar A_k\mathrel{\text{-=}}\bar G_q^{A}/h,\quad\bar A_{k+1}\mathrel{\text{+=}}\bar G_q^{A}/h.

面面积插值、压力求值及源项求积还有各自的导数路径;截面积对目标的直接影响也必须保留。二阶模式的重构权重不等于一阶模式,不能只换预处理器而沿用旧的精确算子。

Face-area interpolation, pressure evaluation and source quadrature have additional derivative paths; retain the direct area dependence in the objective too. Second-order reconstruction has different weights from first order, so changing only the preconditioner cannot update the exact operator.

从程序到切向量:面循环的四条语句逐条线性化

From program to tangent: linearise the four statements of the face loop one by one

上面的 z˙f\dot z_f、H˙f\dot H_f 与 R˙raw,i\dot R_{raw,i} 来自同一段短程序:残差代码对每个面依次执行四条语句。设 𝒮f\mathcal S_f 是面 ff 读取的存储位置(两侧的 LL、RR,以及重构需要时它们的邻居),把这些位置上的状态排成 𝐔f\mathbf U_f:

The z˙f\dot z_f, H˙f\dot H_f and R˙raw,i\dot R_{raw,i} above come from one short program: the residual code runs four statements on every face. Let 𝒮f\mathcal S_f be the stored locations that face ff reads (its two sides LL and RR, and their neighbours when the reconstruction needs them), and stack the states stored there as 𝐔f\mathbf U_f:

(1)𝐔f=𝒢f 𝐔,(1)\quad \mathbf U_f=\mathcal G_f\,\mathbf U,
(2)zf=zf(D,𝐔f),(2)\quad z_f=z_f(D,\mathbf U_f),
(3)Hf=H(zf),(3)\quad H_f=H(z_f),
(4)Rraw+=ℬ:,f Hf.(4)\quad R_{raw}\mathrel{\text{+=}}\mathcal B_{:,f}\,H_f.

(1)取值:𝒢f\mathcal G_f 的每一行只有一个 1,从全局数组里挑出 𝒮f\mathcal S_f 上的状态;(2)重构:由这些状态和设计变量算出面的全部输入 zfz_f,在本页是两侧的重构状态 Uf−U_f^-、Uf+U_f^+ 与面面积 AfA_f;(3)通量:Hf=H(zf)H_f=H(z_f);(4)分发:ℬ:,f\mathcal B_{:,f} 是有向关联矩阵 ℬ\mathcal B 的第 ff 列,在第 LL 行为 +1+1、第 RR 行为 −1-1,即 Rraw,L+=HfR_{raw,L}\mathrel{\text{+=}}H_f、Rraw,R-=HfR_{raw,R}\mathrel{\text{-=}}H_f(边界面只写一行);本页每个位置有 3 个分量,±1\pm1 代表 ±I\pm I。这四条语句是残差代码的骨架;本页代码里的每一条语句(含边界面与源项)都在本节里逐句写出,并附各自的切向量语句。

(1) Gather: each row of 𝒢f\mathcal G_f holds a single 1 and picks one state of 𝒮f\mathcal S_f out of the global array. (2) Reconstruct: the states and the design give all inputs zfz_f of the face, on this page the reconstructed states Uf−U_f^-, Uf+U_f^+ and the face area AfA_f. (3) Flux: Hf=H(zf)H_f=H(z_f). (4) Scatter: ℬ:,f\mathcal B_{:,f} is column ff of the oriented incidence ℬ\mathcal B, +1+1 in row LL and −1-1 in row RR, that is Rraw,L+=HfR_{raw,L}\mathrel{\text{+=}}H_f and Rraw,R-=HfR_{raw,R}\mathrel{\text{-=}}H_f (a boundary face writes one row); with 3 components per location, ±1\pm1 stands for ±I\pm I. These four statements are the skeleton of the residual code; every statement of this page's code, boundary faces and sources included, is written out with its tangent statement in this section.

前向模式对每条语句用一次链式法则:每条语句输出的切向量是它输入的切向量的线性组合,每个中间变量都有自己的切向量:

Forward mode applies the chain rule once per statement: the tangent of each output is a linear combination of the tangents of the statement's inputs, and every intermediate variable gets a tangent of its own:

(1)𝐔˙f=𝒢f 𝐔˙,(1)\quad \dot{\mathbf U}_f=\mathcal G_f\,\dot{\mathbf U},
(2)z˙f=Zf 𝐔˙f+Kf D˙,(2)\quad \dot z_f=Z_f\,\dot{\mathbf U}_f+K_f\,\dot D,
(3)H˙f=Hz,f z˙f,(3)\quad \dot H_f=H_{z,f}\,\dot z_f,
(4)R˙raw+=ℬ:,f H˙f,(4)\quad \dot R_{raw}\mathrel{\text{+=}}\mathcal B_{:,f}\,\dot H_f,
Zf=∂zf∂𝐔f,Kf=∂zf∂D,Hz,f=∂H∂zf.Z_f=\frac{\partial z_f}{\partial \mathbf U_f},\qquad K_f=\frac{\partial z_f}{\partial D},\qquad H_{z,f}=\frac{\partial H}{\partial z_f}.

取值与分发都是线性的,它们的切向量就是同样的取值与分发作用在切向量上;只有重构与通量两条语句带来依赖当前状态的局部导数 ZfZ_f、KfK_f 与 Hz,fH_{z,f}。把(1)代入(2),(2)代入(3),(3)代入(4),对所有面求和,再减去体源项的切向量 e2S˙e_2\dot S(e2e_2 把每个 SiS_i 放进它那一行的动量分量),就逐条语句得到 R˙raw=Jraw𝐔˙+RD,rawD˙\dot R_{raw}=J_{raw}\dot{\mathbf U}+R_{D,raw}\dot D:

Gather and scatter are linear: their tangents are the same gather and scatter applied to the tangents. Only the reconstruct and flux statements bring local derivatives that depend on the current state: ZfZ_f, KfK_f and Hz,fH_{z,f}. Substituting (1) into (2), (2) into (3) and (3) into (4), summing over all faces and subtracting the tangent e2S˙e_2\dot S of the volume source (e2e_2 puts each SiS_i into the momentum component of its row) assembles R˙raw=Jraw𝐔˙+RD,rawD˙\dot R_{raw}=J_{raw}\dot{\mathbf U}+R_{D,raw}\dot D statement by statement:

R˙raw=∑fℬ:,fHz,f(Zf𝒢f 𝐔˙+KfD˙)−e2S˙\dot R_{raw}=\sum_f\mathcal B_{:,f}H_{z,f}\big(Z_f\mathcal G_f\,\dot{\mathbf U}+K_f\dot D\big)-e_2\dot S
=(∑fℬ:,fHz,fZf𝒢f−e2∂S∂𝐔⏟Jraw)𝐔˙=\Big(\underbrace{\sum_f\mathcal B_{:,f}H_{z,f}Z_f\mathcal G_f-e_2\frac{\partial S}{\partial \mathbf U}}_{J_{raw}}\Big)\dot{\mathbf U}
+(∑fℬ:,fHz,fKf−e2∂S∂D⏟RD,raw)D˙.+\Big(\underbrace{\sum_f\mathcal B_{:,f}H_{z,f}K_f-e_2\frac{\partial S}{\partial D}}_{R_{D,raw}}\Big)\dot D.

上面的 TfT_f 就是 Zf𝒢fZ_f\mathcal G_f。所以一个面对 JrawJ_{raw} 的贡献是四个初等 Jacobian 的乘积

The TfT_f above is Zf𝒢fZ_f\mathcal G_f. One face therefore contributes to JrawJ_{raw} the product of four elementary Jacobians,

Jraw,f=ℬ:,f Hz,f Zf 𝒢f,J_{raw,f}=\mathcal B_{:,f}\,H_{z,f}\,Z_f\,\mathcal G_f,
Jraw,f𝐔˙=ℬ:,f(Hz,f(Zf(𝒢f 𝐔˙))).J_{raw,f}\dot{\mathbf U}=\mathcal B_{:,f}\Big(H_{z,f}\Big(Z_f\big(\mathcal G_f\,\dot{\mathbf U}\big)\Big)\Big).

它从右往左作用在 𝐔˙\dot{\mathbf U} 上,与程序执行的顺序相同:先取值,再重构、通量,最后分发。𝒮f\mathcal S_f 只有几个位置,所以 Jraw,fJ_{raw,f} 只有一小块非零,matrix-free 乘积就是按面执行这几次局部运算,不存储任何全局矩阵。下面的面板按这四条语句逐条播放本页一个面的数值。

It acts on 𝐔˙\dot{\mathbf U} from right to left, in the order in which the program runs: gather first, then reconstruct and flux, scatter last. 𝒮f\mathcal S_f holds only a few locations, so Jraw,fJ_{raw,f} has a small nonzero block, and the matrix-free product runs these local operations face by face without storing any global matrix. The panel below plays the four statements with the numbers of one face of this page.

本页一个面的算例:前向扫描

A worked example on this page: the forward sweep of one face

取面 f=6f=6(xf=0.4583x_f=0.4583),它两侧是控制体 5、6;二阶重构用中心斜率,每一侧都要读它的两个邻居,𝒮f={4,5,6,7}\mathcal S_f=\{4,5,6,7\}。设计种子取 D˙=ek\dot D=e_k,DkD_k 是喉道截面积 A6A_6;状态种子取本页前向求解对同一方向给出的解 𝐔˙=v\dot{\mathbf U}=v,于是下面每个切向量都是对 DkD_k 的导数。表中的数由本页代码在构建时算出:基准状态、前向求解与局部导数都用本页自己的程序。

Take face f=6f=6 (xf=0.4583x_f=0.4583), between control volumes 5 and 6; second-order reconstruction uses centred slopes, so each side reads its two neighbours, 𝒮f={4,5,6,7}\mathcal S_f=\{4,5,6,7\}. Seed the design with D˙=ek\dot D=e_k, where DkD_k is the throat area A6A_6, and the states with this page's tangent solution for the same direction, 𝐔˙=v\dot{\mathbf U}=v; every tangent below is then a derivative with respect to DkD_k. The numbers are computed by this page's code at build time: its baseline state, its tangent solve and its local derivatives.

语句Statement量Quantity原始值Primal value切向量Tangent
(1)取值(1) gatherU4U_{4}0.863
0.5563
2.215
−0.04312
0.1485
−0.02482
U5U_{5}0.8321
0.5943
2.148
−0.07686
−0.05698
−0.2706
U6U_{6}0.8114
0.6117
2.098
0.1451
−0.2448
0.2374
U7U_{7}0.8151
0.5999
2.1
0.2825
−0.07871
0.7634
设计种子design seedDk=A6D_k=A_{6}0.71
(2)重构(2) reconstructUf−U_f^-0.8192
0.6082
2.118
−0.02982
−0.1553
−0.2051
Uf+U_f^+0.8157
0.6103
2.11
0.0552
−0.2394
−0.0211
AfA_f0.710.5
(3)通量(3) fluxHfH_f0.4349
0.857
1.524
0.109
0.4564
0.3822
Hz,fZf𝐔˙fH_{z,f}Z_f\dot{\mathbf U}_f−0.1973
−0.1471
−0.691
Hz,fKfD˙H_{z,f}K_f\dot D0.3063
0.6035
1.073
(4)分发(4) scatterR5+=HfR_{5}\mathrel{\text{+=}}H_f0.4349
0.857
1.524
0.109
0.4564
0.3822
R6-=HfR_{6}\mathrel{\text{-=}}H_f−0.4349
−0.857
−1.524
−0.109
−0.4564
−0.3822

A6A_6 通过面面积 AfA_f 直接进入这个面(Af=12(A5+A6)A_f=\tfrac12(A_5+A_6),所以 A˙f=12\dot A_f=\tfrac12),所以通量的切向量分成两部分:经过状态的 Hz,fZf𝐔˙fH_{z,f}Z_f\dot{\mathbf U}_f 与直接来自设计的 Hz,fKfD˙H_{z,f}K_f\dot D,表中两行相加就是 H˙f\dot H_f。

A6A_6 enters this face directly through the face area AfA_f (Af=12(A5+A6)A_f=\tfrac12(A_5+A_6), so A˙f=12\dot A_f=\tfrac12), so the flux tangent has two parts: Hz,fZf𝐔˙fH_{z,f}Z_f\dot{\mathbf U}_f through the states and Hz,fKfD˙H_{z,f}K_f\dot D straight from the design; the two rows of the table add up to H˙f\dot H_f.

所有面与源项的这些贡献加起来是替换之前的残差的切向量 Jrawv+RD,raw ekJ_{raw}v+R_{D,raw}\,e_k。它在保留的行上于收敛解处为零;在被替换的行上它不为零(那里模最大的一项是 −0.01626-0.01626),因为残差代码在面循环之后把这些行整行覆盖,覆盖之后的切向量是约束的切向量。下面前向求解的方程是整个方程组的:

Summed over all faces and the source terms, these contributions give the tangent of the raw residual, Jrawv+RD,raw ekJ_{raw}v+R_{D,raw}\,e_k. In the retained rows it vanishes at the converged solution; in the replaced rows it does not (its entry of largest magnitude there is −0.01626-0.01626), because the residual code overwrites those rows after the face loop and the tangent after the overwrite is the constraint's. The tangent equation solved further on is the system's:

R˙=MbcR˙raw+Ebcc˙bc,c˙bc=∂cbc∂𝐔 v+∂cbc∂D D˙,\dot R=M_{\mathrm{bc}}\dot R_{raw}+E_{\mathrm{bc}}\dot c_{\mathrm{bc}},\qquad \dot c_{\mathrm{bc}}=\frac{\partial c_{\mathrm{bc}}}{\partial \mathbf U}\,v+\frac{\partial c_{\mathrm{bc}}}{\partial D}\,\dot D,
Jv+RDD˙=Mbc(Jrawv+RD,rawD˙)Jv+R_D\dot D=M_{\mathrm{bc}}\big(J_{raw}v+R_{D,raw}\dot D\big)
+Ebcc˙bc=0.+E_{\mathrm{bc}}\dot c_{\mathrm{bc}}=0.
一个面的前向扫描
The forward sweep of one face

按程序顺序执行四条语句,箭头上是各自的初等 Jacobian;下方是它们的乘积与形状(行 × 列,本页这个面的尺寸)。

The four statements in program order, each arrow labelled with its elementary Jacobian; below, their product with the shapes (rows × columns, this face of this page).

全部存储状态all stored statesUU˙​面读取的状态states the face readsUf​U˙​f​面输入:重构值等face inputszf​z˙​f​面通量face fluxHf​H˙​f​两侧残差行rows L and RRL, RR​R˙​L, R˙​R​+=设计输入design inputDD˙​𝒢f​Zf​Hz,f​ℬ:,f​Kf​Jraw,f=ℬ:,f​39×3Hz,f​3×7Zf​7×12𝒢f​12×39U˙​R˙​raw​从右往左作用:先取值(最右),最后分发(最左)applied right to left: the gather (rightmost) first, the scatter (leftmost) last全部状态all statesUU˙​模板上的状态stencilUf​U˙​f​面输入face inputszf​z˙​f​面通量face fluxHf​H˙​f​L、R 两行rows L, RRL, RR​R˙​L, R˙​R​+=设计输入design inputDD˙​𝒢f​Zf​Hz,f​ℬ:,f​Kf​Jraw,f=ℬ:,f​39×3Hz,f​3×7Zf​7×12𝒢f​12×39U˙​R˙​raw​从右往左作用:applied right to left:先取值,最后分发the gather first, the scatter last
  1. 种子:D˙=ek\dot D=e_k(DkD_k 是喉道截面积 A6A_6),𝐔˙=v\dot{\mathbf U}=v。在 𝒮f\mathcal S_f 上:U˙4=\dot U_{4}= (−0.04312, 0.1485, −0.02482),U˙5=\dot U_{5}= (−0.07686, −0.05698, −0.2706),U˙6=\dot U_{6}= (0.1451, −0.2448, 0.2374),U˙7=\dot U_{7}= (0.2825, −0.07871, 0.7634)。

    Seeds: D˙=ek\dot D=e_k (DkD_k is the throat area A6A_6) and 𝐔˙=v\dot{\mathbf U}=v. On 𝒮f\mathcal S_f: U˙4=\dot U_{4}= (−0.04312, 0.1485, −0.02482), U˙5=\dot U_{5}= (−0.07686, −0.05698, −0.2706), U˙6=\dot U_{6}= (0.1451, −0.2448, 0.2374), U˙7=\dot U_{7}= (0.2825, −0.07871, 0.7634).

  2. (1)取值:𝐔˙f=𝒢f𝐔˙\dot{\mathbf U}_f=\mathcal G_f\dot{\mathbf U},只挑出 𝒮f\mathcal S_f 上的 4 个状态的切向量(上一步列出的数)。

    (1) Gather: 𝐔˙f=𝒢f𝐔˙\dot{\mathbf U}_f=\mathcal G_f\dot{\mathbf U} picks the tangents of the 4 states of 𝒮f\mathcal S_f (the numbers of the previous step).

  3. (2)重构:z˙f=Zf𝐔˙f+KfD˙\dot z_f=Z_f\dot{\mathbf U}_f+K_f\dot D。U˙f−=\dot U_f^-= (−0.02982, −0.1553, −0.2051),U˙f+=\dot U_f^+= (0.0552, −0.2394, −0.0211),A˙f=\dot A_f= 0.5。A6A_6 通过面面积 AfA_f 直接进入这个面(Af=12(A5+A6)A_f=\tfrac12(A_5+A_6),所以 A˙f=12\dot A_f=\tfrac12)。

    (2) Reconstruct: z˙f=Zf𝐔˙f+KfD˙\dot z_f=Z_f\dot{\mathbf U}_f+K_f\dot D. U˙f−=\dot U_f^-= (−0.02982, −0.1553, −0.2051), U˙f+=\dot U_f^+= (0.0552, −0.2394, −0.0211), A˙f=\dot A_f= 0.5. A6A_6 enters this face directly through the face area AfA_f (Af=12(A5+A6)A_f=\tfrac12(A_5+A_6), so A˙f=12\dot A_f=\tfrac12).

  4. (3)通量:H˙f=Hz,fz˙f=\dot H_f=H_{z,f}\dot z_f= (0.109, 0.4564, 0.3822),其中经过状态的部分 (−0.1973, −0.1471, −0.691),直接来自设计的部分 (0.3063, 0.6035, 1.073)。

    (3) Flux: H˙f=Hz,fz˙f=\dot H_f=H_{z,f}\dot z_f= (0.109, 0.4564, 0.3822), of which (−0.1973, −0.1471, −0.691) through the states and (0.3063, 0.6035, 1.073) straight from the design.

  5. (4)分发:R˙5+=H˙f=\dot R_{5}\mathrel{\text{+=}}\dot H_f= (0.109, 0.4564, 0.3822),R˙6-=H˙f\dot R_{6}\mathrel{\text{-=}}\dot H_f,即加上 (−0.109, −0.4564, −0.3822)。

    (4) Scatter: R˙5+=H˙f=\dot R_{5}\mathrel{\text{+=}}\dot H_f= (0.109, 0.4564, 0.3822) and R˙6-=H˙f\dot R_{6}\mathrel{\text{-=}}\dot H_f, that is (−0.109, −0.4564, −0.3822) added.

逐句的前向:残差循环

The tangent, statement by statement: the residual loops

第 5 节的残差由下面的语句装配;按乘积法则逐句求导,就得到右列的前向语句,执行顺序与原始计算相同。用到的局部导数都在第 3、4、7 节:

Section 5 assembles the residual with the statements below; differentiating each by the product rule gives the tangent statement in the right column, executed in the same order as the primal. The local derivatives it needs are those of Sections 3, 4 and 7:

##原始语句primal statement前向语句(同一顺序)tangent statement (same order)
① 面 f① face fUf−=∑jWfj−UjU_f^-=\sum_jW^-_{fj}U_jU˙f−=∑jWfj−U˙j\dot U_f^-=\sum_jW^-_{fj}\dot U_j
②②Uf+=∑jWfj+UjU_f^+=\sum_jW^+_{fj}U_jU˙f+=∑jWfj+U˙j\dot U_f^+=\sum_jW^+_{fj}\dot U_j
③③Af=(1−tf)Ak+tfAk+1A_f=(1-t_f)A_k+t_fA_{k+1}A˙f=(1−tf)A˙k+tfA˙k+1\dot A_f=(1-t_f)\dot A_k+t_f\dot A_{k+1}
④ 端面④ end faceHf=AfF(Uf)H_f=A_fF(U_f)H˙f=A˙fF+AfFUU˙f\dot H_f=\dot A_fF+A_fF_U\dot U_f
⑤⑤Hf=AfH^(Uf−,Uf+)H_f=A_f\widehat H(U_f^-,U_f^+)H˙f=A˙fH^+Af(H^−U˙f−+H^+U˙f+)\dot H_f=\dot A_f\widehat H+A_f\bigl(\widehat H_-\dot U_f^-+\widehat H_+\dot U_f^+\bigr)
⑥⑥RL+=Hf, RR-=HfR_L\mathrel{\text{+=}}H_f,\ R_R\mathrel{\text{-=}}H_fR˙L+=H˙f, R˙R-=H˙f\dot R_L\mathrel{\text{+=}}\dot H_f,\ \dot R_R\mathrel{\text{-=}}\dot H_f
⑦ 控制体 i、Gauss 点 q⑦ volume i, Gauss point qUq=∑jWqjUjU_q=\sum_jW_{qj}U_jU˙q=∑jWqjU˙j\dot U_q=\sum_jW_{qj}\dot U_j
⑧⑧Si+=wq p(Uq) GqS_i\mathrel{\text{+=}}w_q\,p(U_q)\,G_qS˙i+=wq(pU(Uq)U˙q Gq+pqG˙q)\dot S_i\mathrel{\text{+=}}w_q\bigl(p_U(U_q)\dot U_q\,G_q+p_q\dot G_q\bigr)
⑨⑨Q+=wqθqQ\mathrel{\text{+=}}w_q\theta_qQ˙+=wqθ˙q\dot Q\mathrel{\text{+=}}w_q\dot\theta_q
⑩⑩Ri,m-=SiR_{i,m}\mathrel{\text{-=}}S_iR˙i,m-=S˙i\dot R_{i,m}\mathrel{\text{-=}}\dot S_i
⑪ 装配之后⑪ after assemblyRr=cr(𝐔,D), r=0,1,3NR_r=c_r(\mathbf U,D),\ r=0,1,3NR˙r=∂cr∂𝐔𝐔˙+∂cr∂DD˙\dot R_r=\frac{\partial c_r}{\partial\mathbf U}\dot{\mathbf U}+\frac{\partial c_r}{\partial D}\dot D

其中 H^∓=∂H^/∂Uf∓\widehat H_\mp=\partial\widehat H/\partial U_f^\mp(第 3 节),G˙q=(A˙k+1−A˙k)/h\dot G_q=(\dot A_{k+1}-\dot A_k)/h,θ˙q\dot\theta_q 见第 7 节。① ② 两句的重构权重 Wfj∓W^\mp_{fj} 就是上面共享推导里 (Tf)j(T_f)_j 的对应部分:TfT_f 是面的全部输入 zfz_f 对存储状态的导数,对内部面,它里面 Uf∓U_f^\mp 对 UjU_j 的块是 Wfj∓W^\mp_{fj} 乘单位阵。每一句只用到本句的输入、它们的切向量和局部偏导数;这就是本页 tape 在每个标量运算上做的事,只是这里按公式成块地写出。

Here H^∓=∂H^/∂Uf∓\widehat H_\mp=\partial\widehat H/\partial U_f^\mp (Section 3), G˙q=(A˙k+1−A˙k)/h\dot G_q=(\dot A_{k+1}-\dot A_k)/h, and θ˙q\dot\theta_q is given in Section 7. The reconstruction weights Wfj∓W^\mp_{fj} of statements ① and ② are the corresponding part of the (Tf)j(T_f)_j of the shared derivation above: TfT_f is the derivative of all the face inputs zfz_f with respect to the stored states, and for an interior face its block for Uf∓U_f^\mp and UjU_j is Wfj∓W^\mp_{fj} times the identity. Each statement uses only its own inputs, their tangents and its local partial derivatives; this is what the page’s tape does for every scalar operation, written here in blocks as formulas.

算例:取第 3 节的面 f=6f=6 和设计种子 D˙=e6\dot D=e_{6}(截面积 A6A_{6}),状态切向量取第 10 节解出的 U˙=UDD˙\dot U=U_D\dot D。上面的推导(“本页一个面的算例”)在同一个面上用同样的种子算过,表与面板里的切向量和下面相同;它的语句(1)至(2)、(3)、(4)就是本表的 ①–③、⑤、⑥。第 ③ 句给出 A˙f=0.5\dot A_f=0.5;第 ①② 句给出

Worked example: the face f=6f=6 of Section 3 and the design seed D˙=e6\dot D=e_{6} (the area A6A_{6}), with the state tangent U˙=UDD˙\dot U=U_D\dot D solved in Section 10. The derivation above (“A worked example on this page”) works out the same face with the same seeds; its table and panel list the same tangents, and its statements (1)–(2), (3) and (4) are ①–③, ⑤ and ⑥ of this table. Statement ③ gives A˙f=0.5\dot A_f=0.5; statements ① and ② give

U˙f−=(−0.02982, −0.1553, −0.2051)T,U˙f+=(0.0552, −0.2394, −0.0211)T,\dot U_f^-=\bigl(-0.02982,\ -0.1553,\ -0.2051\bigr)^T,\qquad\dot U_f^+=\bigl(0.0552,\ -0.2394,\ -0.0211\bigr)^T,

第 ⑤ 句用第 3 节的两个矩阵给出

and statement ⑤ with the two matrices of Section 3 gives

H˙f=A˙fH^+Af(H^−U˙f−+H^+U˙f+)=(0.109, 0.4564, 0.3822)T.\dot H_f=\dot A_f\widehat H+A_f\bigl(\widehat H_-\dot U_f^-+\widehat H_+\dot U_f^+\bigr)=\bigl(0.109,\ 0.4564,\ 0.3822\bigr)^T.

它与 core.js 运算记录对这个面的结果相差 1.1×10−161.1\times10^{-16}。整个扫描结束时 ∥R˙∥2=1.9×10−12\|\dot R\|_2=1.9\times10^{-12};同一设计种子配 U˙=0\dot U=0 时,∥R˙∥2=∥RDD˙∥2=1.61\|\dot R\|_2=\|R_D\dot D\|_2=1.61。解出的切向量正好使所有面与控制体的贡献相互抵消(三条被替换的行除外,见下一段)——这就是切线方程 JU˙+RDD˙=0J\dot U+R_D\dot D=0。同时第 ⑨ 句累加出 Q˙=6.687323×10−4\dot Q=6.687323\times10^{-4},即第 12 节梯度的分量 gA6g_{A_{6}}(伴随给出 6.687323×10−46.687323\times10^{-4})。

It differs from the result of core.js’s tape for this face by 1.1×10−161.1\times10^{-16}. At the end of the sweep ∥R˙∥2=1.9×10−12\|\dot R\|_2=1.9\times10^{-12}, whereas the same design seed with U˙=0\dot U=0 gives ∥R˙∥2=∥RDD˙∥2=1.61\|\dot R\|_2=\|R_D\dot D\|_2=1.61. The solved tangent makes the contributions of all faces and volumes cancel exactly (except in the three replaced rows, see the next paragraph): this is the tangent equation JU˙+RDD˙=0J\dot U+R_D\dot D=0. Statement ⑨ accumulates Q˙=6.687323×10−4\dot Q=6.687323\times10^{-4} at the same time, the component gA6g_{A_{6}} of the gradient of Section 12 (the adjoint gives 6.687323×10−46.687323\times10^{-4}).

格点页有一点不同:①–⑩ 得到的是覆盖之前的残差 RrawR_{raw} 的切向量(上面共享推导里的 R˙raw=Jrawv+RD,rawD˙\dot R_{raw}=J_{raw}v+R_{D,raw}\dot D),它在三条被替换的行(第 0、1、3N3N 行)上不为零(这里最大为 0.0160.016,其余各行合起来只有 1.9×10−121.9\times10^{-12});⑪ 就是上面共享推导里的覆盖语句(5),它用约束的切向量覆盖这三行,解出的切向量使覆盖后的三个分量为零。所以切线方程 JU˙+RDD˙=0J\dot U+R_D\dot D=0 的三条约束行由 ⑪ 给出,其余各行由 ①–⑩ 给出。

The node page differs in one point: ①–⑩ give the tangent of the raw residual RrawR_{raw}, before any row is overwritten (the R˙raw=Jrawv+RD,rawD˙\dot R_{raw}=J_{raw}v+R_{D,raw}\dot D of the shared derivation above), which is not zero in the three replaced rows (rows 0, 1 and 3N3N): here at most 0.0160.016, against 1.9×10−121.9\times10^{-12} in all the other rows together. ⑪ is the overwrite (5) of that derivation: it replaces those rows with the tangents of the constraints, which the solved tangent makes zero. So the three constraint rows of the tangent equation JU˙+RDD˙=0J\dot U+R_D\dot D=0 come from ⑪ and the other rows from ①–⑩.

图 6 逐句播放这一扫描:先是全部面(第 ①–⑥ 句),再是全部控制体(第 ⑦–⑩ 句),最后是三条边界行(第 ⑪ 句)。可以选择设计种子(任一截面积或边界数据)与状态种子:U˙=0\dot U=0 时扫描结束于右端项 RDD˙R_D\dot D,一个截面积只触及用到它的面和它两侧区间的控制体;U˙\dot U 取第 10 节解出的切向量时,所有贡献相互抵消,R˙\dot R 结束于零(格点页要等 ⑪ 覆盖了三条被替换的行之后)。

Figure 6 plays this sweep statement by statement: all faces first (statements ①–⑥), then all volumes (⑦–⑩), then the three boundary rows (⑪). Choose the design seed (any area or boundary datum) and the state seed: with U˙=0\dot U=0 the sweep ends at the right-hand side RDD˙R_D\dot D, and an area touches only the faces that read it and the volumes of the two intervals beside it; with U˙\dot U set to the tangent solved in Section 10 every contribution cancels and R˙\dot R ends at zero (on the node page once ⑪ has overwritten the three replaced rows).

图 6 · 前向扫描:设计种子 D˙=ek\dot D=e_k 沿残差循环逐句传播(播放、暂停、单步、调速)。上:流道、当前语句所在的面或控制体,菱形标出种子;下:R˙\dot R 的三个分量(ρ\rho、mm、ee 三行)按执行顺序累加,本步改变的量加框。U˙=0\dot U=0 时扫描结束于 RDD˙R_D\dot D;取解出的切向量时结束于零。Figure 6 · The tangent sweep: a design seed D˙=ek\dot D=e_k travels through the residual loops statement by statement (play, pause, step, speed). Top: the duct, the face or volume of the current statement, and the seed (diamond); bottom: the three components of R˙\dot R (rows ρ\rho, mm, ee) accumulating in execution order, the entries changed by the current step outlined. With U˙=0\dot U=0 the sweep ends at RDD˙R_D\dot D; with the solved tangent it ends at zero.
第 9 节Section 9

伴随传播:同一条依赖链反向累加

Adjoint propagation: reverse the same dependency graph

本节实验:只给一个输出非零权重,预测反向影响能传到哪些输入;共享输入应累加所有路径。

Experiment: Seed one output and predict which inputs receive reverse contributions. Shared inputs accumulate every path.

反向传播接受残差种子 Rˉ\bar R(常记作 w)和目标种子 Qˉ\bar Q,返回 Uˉ=RUTRˉ+QUTQˉ\bar U=R_U^T\bar R+Q_U^T\bar Q 与 Dˉ=RDTRˉ+QDTQˉ\bar D=R_D^T\bar R+Q_D^T\bar Q。在内部面,H 的种子为 RˉL−RˉR\bar R_L-\bar R_R;源项 S 的种子为 −Rˉi,m-\bar R_{i,m}。二阶面状态的伴随通过重构权重散回多个节点。共享输入必须用 += 累加,不能用赋值覆盖另一条路径;语句按原始计算的逆序执行。

A reverse sweep takes a residual seed Rˉ\bar R (often called w) and an objective seed Qˉ\bar Q, and returns Uˉ=RUTRˉ+QUTQˉ\bar U=R_U^T\bar R+Q_U^T\bar Q and Dˉ=RDTRˉ+QDTQˉ\bar D=R_D^T\bar R+Q_D^T\bar Q. An internal face receives the seed RˉL−RˉR\bar R_L-\bar R_R; a source receives −Rˉi,m-\bar R_{i,m}. Second-order trace adjoints scatter through reconstruction weights to several states. Shared inputs accumulate every path with += instead of being overwritten, and the statements run in the reverse of the primal order.

Hˉf=RˉL−RˉR,Uˉj+=WfjTUˉf,Sˉi=−Rˉi,m,RˉTR˙=UˉTU˙.\bar H_f=\bar R_L-\bar R_R,\quad \bar U_j\mathrel{\text{+=}}W_{fj}^T\bar U_f,\quad \bar S_i=-\bar R_{i,m},\quad \bar R^T\dot R=\bar U^T\dot U.

强边界行反传时,先把这三行的种子 Rˉr\bar R_r 送入边界约束,再令旧守恒行的种子 Rˉr←0\bar R_r\leftarrow0,之后才反传源项和面通量。否则会把已经被覆盖的旧行又加回导数。

For strong boundary rows, first send the seeds Rˉr\bar R_r of these three rows into the boundary constraints, then set the overwritten balance-row seeds Rˉr←0\bar R_r\leftarrow0, and only then reverse sources and faces. Otherwise derivatives of erased rows would be reintroduced.

# each operation computes z = f(inputs)
for operation in reverse_order:
    for input in operation.inputs:
        input_bar += local_partial * z_bar
    z_bar = 0
# z=x*y: x_bar += y*z_bar; y_bar += x*z_bar
#        z_bar = 0
# second-order reconstruction: U_bar[j] += weight[j]*Uf_bar

为什么反向累加给出矩阵转置

Why reverse accumulation gives the matrix transpose

把所有残差和状态分量展平后,下面只是交换两个有限求和的次序。残差种子 Rˉ\bar R 是任意给定的,尚未求解伴随方程;只有选择满足第 11 节线性方程的 Rˉ=ψ\bar R=\psi,才得到该目标对应的离散伴随解。取 D˙=0\dot D=0、𝐔˙=v\dot{\mathbf U}=v,前向得到 R˙=Jv\dot R=Jv;以 Rˉ\bar R 为种子反向扫描,得到 𝐔ˉ=JTRˉ\bar{\mathbf U}=J^T\bar R。下式就是 AD 的对偶(点积)恒等式:

After flattening all residual and state components, the following simply exchanges two finite sums. The residual seed Rˉ\bar R is arbitrary and has not solved an adjoint equation; only Rˉ=ψ\bar R=\psi satisfying Section 11 gives the discrete adjoint solution for the chosen objective. With D˙=0\dot D=0 and 𝐔˙=v\dot{\mathbf U}=v the forward sweep gives R˙=Jv\dot R=Jv; the reverse sweep seeded with Rˉ\bar R gives 𝐔ˉ=JTRˉ\bar{\mathbf U}=J^T\bar R. The identity below is the duality (dot-product) identity of AD:

RˉTR˙=RˉT(Jv)=∑r=1nu∑j=1nuRˉr∂Rr∂Ujvj\bar R^T\dot R=\bar R^T(Jv)=\sum_{r=1}^{n_u}\sum_{j=1}^{n_u}\bar R_r\frac{\partial R_r}{\partial U_j}v_j
=∑j=1nuvj(∑r=1nu∂Rr∂UjRˉr)=vT(JTRˉ)=𝐔˙T𝐔ˉ.=\sum_{j=1}^{n_u}v_j\left(\sum_{r=1}^{n_u}\frac{\partial R_r}{\partial U_j}\bar R_r\right)=v^T(J^T\bar R)=\dot{\mathbf U}^T\bar{\mathbf U}.

本页的残差代码在面循环与源项之后还有一条语句(5):把三行守恒方程覆盖为边界约束 cbcc_{\mathrm{bc}},即第一个节点的第 0、1 行覆盖为 p0(U0)−p0p_0(U_0)-p_0 与 T0(U0)−T0T_0(U_0)-T_0,最后一个节点的质量行(第 3N3N 行)覆盖为 p(UN)−pbp(U_N)-p_b。MbcM_{\mathrm{bc}} 是保留行的对角掩码,EbcE_{\mathrm{bc}} 把 cbcc_{\mathrm{bc}} 放入被替换的行,它们在固定网格拓扑上是常矩阵;覆盖之前的残差、Jacobian 与设计 Jacobian 记作 RrawR_{raw}、JrawJ_{raw}、RD,rawR_{D,raw},面循环与源项写出的就是它们。

The residual code of the node page has one more statement (5) after the face loop and the sources: it overwrites three balance rows by the boundary constraints cbcc_{\mathrm{bc}}, namely rows 0 and 1 of the first node by p0(U0)−p0p_0(U_0)-p_0 and T0(U0)−T0T_0(U_0)-T_0 and the mass row of the last node (row 3N3N) by p(UN)−pbp(U_N)-p_b. MbcM_{\mathrm{bc}} masks the retained rows and EbcE_{\mathrm{bc}} inserts cbcc_{\mathrm{bc}} into the replaced rows; both are constant for fixed topology. The residual, Jacobian and design Jacobian before the overwrite are RrawR_{raw}, JrawJ_{raw} and RD,rawR_{D,raw}, and the face loop and the sources write exactly these.

这条语句最后执行,所以它的反向语句(5′)最先执行:种子 Rˉ\bar R 分成两部分,保留行的部分 Rˉraw=MbcRˉ\bar R_{raw}=M_{\mathrm{bc}}\bar R 交给转置的面循环与源项,被替换的行的部分 cˉbc=EbcTRˉ\bar c_{\mathrm{bc}}=E_{\mathrm{bc}}^T\bar R 交给约束,𝐔ˉ\bar{\mathbf U} 与 Dˉ\bar D 各加上它的状态路径与设计路径;设计路径在本页就是 pˉ0-=Rˉ0\bar p_0\mathrel{\text{-=}}\bar R_0、Tˉ0-=Rˉ1\bar T_0\mathrel{\text{-=}}\bar R_1、pˉb-=Rˉ3N\bar p_b\mathrel{\text{-=}}\bar R_{3N}(这三行里的设计导数是 −1-1)。完整转置必须包含约束的 Jacobian,而不是简单地把边界伴随设为零;被替换的行也不能把种子交给面循环与源项:第一个节点的动量行被 T0T_0 约束覆盖,写进这一行的源项因此得不到权重。

This statement runs last, so its reverse statement (5′) runs first: the seed Rˉ\bar R splits into the part of the retained rows, Rˉraw=MbcRˉ\bar R_{raw}=M_{\mathrm{bc}}\bar R, which goes to the transposed face loop and the sources, and the part of the replaced rows, cˉbc=EbcTRˉ\bar c_{\mathrm{bc}}=E_{\mathrm{bc}}^T\bar R, which goes to the constraints, and 𝐔ˉ\bar{\mathbf U} and Dˉ\bar D each receive its state path and its design path; on this page the design path is pˉ0-=Rˉ0\bar p_0\mathrel{\text{-=}}\bar R_0, Tˉ0-=Rˉ1\bar T_0\mathrel{\text{-=}}\bar R_1 and pˉb-=Rˉ3N\bar p_b\mathrel{\text{-=}}\bar R_{3N} (the design derivative is −1-1 in these three rows). The transpose includes the constraint Jacobian; it does not simply zero the boundary adjoints, and the replaced rows must not hand their seeds to the face loop or to the sources: the momentum row of the first node is overwritten by the T0T_0 constraint, so a source written into that row receives no weight.

R=MbcRraw+Ebccbc,R=M_{\mathrm{bc}}R_{raw}+E_{\mathrm{bc}}c_{\mathrm{bc}},
J=MbcJraw+Ebc∂cbc∂𝐔,J=M_{\mathrm{bc}}J_{raw}+E_{\mathrm{bc}}\frac{\partial c_{\mathrm{bc}}}{\partial \mathbf U},
𝐔ˉ=JTRˉ=JrawTMbcTRˉ+(∂cbc∂𝐔)TEbcTRˉ.\bar{\mathbf U}=J^T\bar R=J_{raw}^TM_{\mathrm{bc}}^T\bar R+\Big(\frac{\partial c_{\mathrm{bc}}}{\partial \mathbf U}\Big)^TE_{\mathrm{bc}}^T\bar R.

内部面通量向左加、向右减,所以先把这两条路径合并成通量的伴随 Hˉf\bar H_f;之后按通量、重构、原始状态的相反依赖顺序传播。反向语句中的 += 很重要:同一状态会被多个面和多个求积点读取,这些贡献必须累加。这里没有分部积分,也没有另行猜测连续伴随边界条件。

An interior flux is added on the left and subtracted on the right, so combine those two paths into its adjoint Hˉf\bar H_f before reversing fluxes, reconstruction and stored states. The += is essential: multiple faces and quadrature points read the same state, and their contributions must accumulate. There is no integration by parts and no separately guessed continuous adjoint boundary condition.

Rraw,L+=Hf, Rraw,R-=HfR_{raw,L}\mathrel{\text{+=}}H_f,\ R_{raw,R}\mathrel{\text{-=}}H_f
⟹Hˉf=Rˉraw,L−Rˉraw,R,\Longrightarrow\quad\bar H_f=\bar R_{raw,L}-\bar R_{raw,R},
z˙f=∑j∈𝒮f(Tf)j U˙j+KfD˙\dot z_f=\sum_{j\in\mathcal S_f}(T_f)_j\,\dot U_j+K_f\dot D
⟹Uˉj+=(Tf)jT zˉf.\Longrightarrow\quad\bar U_j\mathrel{\text{+=}}(T_f)_j^T\,\bar z_f.

第 8 节局部链里的每个切向量都有对应的伴随。反向语句按与前向相反的顺序执行,每个输入的横线量都用 += 累加:

Every tangent of the local chain of Section 8 has its adjoint. The reverse statements run in the opposite order of the forward ones, and the bar of every input accumulates with +=:

𝐔ˉ←0, Dˉ←0;Hˉf=∑iℬifRˉraw,i,\bar{\mathbf U}\leftarrow0,\ \bar D\leftarrow0;\quad \bar H_f=\sum_i\mathcal B_{if}\bar R_{raw,i},
Sˉi=−Rˉraw,i,m,zˉf=Hz,fTHˉf,\bar S_i=-\bar R_{raw,i,m},\quad \bar z_f=H_{z,f}^T\bar H_f,
𝐔ˉ+=TfTzˉf+(∂S∂𝐔)TSˉ,\bar{\mathbf U}\mathrel{\text{+=}}T_f^T\bar z_f+\Big(\frac{\partial S}{\partial \mathbf U}\Big)^T\bar S,
Dˉ+=KfTzˉf+(∂S∂D)TSˉ.\bar D\mathrel{\text{+=}}K_f^T\bar z_f+\Big(\frac{\partial S}{\partial D}\Big)^T\bar S.

从切向量到伴随:转置把语句顺序反过来

From tangent to adjoint: the transpose runs the statements backwards

一个面的切向量是四个初等 Jacobian 的乘积。乘积的转置等于各个因子的转置按相反的顺序相乘:

The tangent of one face is the product of four elementary Jacobians. The transpose of a product is the product of the transposes in the reverse order:

Jraw,fT=(ℬ:,f Hz,f Zf 𝒢f)T=𝒢fT ZfT Hz,fT ℬ:,fT,J_{raw,f}^T=\big(\mathcal B_{:,f}\,H_{z,f}\,Z_f\,\mathcal G_f\big)^T=\mathcal G_f^T\,Z_f^T\,H_{z,f}^T\,\mathcal B_{:,f}^T,
(ℬ:,f Hz,f Kf)T=KfT Hz,fT ℬ:,fT.\big(\mathcal B_{:,f}\,H_{z,f}\,K_f\big)^T=K_f^T\,H_{z,f}^T\,\mathcal B_{:,f}^T.

仍从右往左读,这就是反向扫描的语句顺序:程序最后执行的分发最先处理,最先执行的取值最后处理。先把所有伴随置零,再以 Rˉ\bar R 为种子逐条执行;覆盖(5)在整个面循环之后执行,所以它的反向语句(5′)排在最前:

Read again from right to left, this is the order of the reverse sweep: the scatter, which the program ran last, comes first, and the gather, which it ran first, comes last. Zero every adjoint, seed with Rˉ\bar R, and run; the overwrite (5) ran after the whole face loop, so its reverse statement (5′) comes first:

(0)𝐔ˉ←0,  Dˉ←0,(0)\quad \bar{\mathbf U}\leftarrow0,\ \ \bar D\leftarrow0,
(5′)Rˉraw=MbcRˉ,cˉbc=EbcTRˉ,(5')\quad \bar R_{raw}=M_{\mathrm{bc}}\bar R,\quad \bar c_{\mathrm{bc}}=E_{\mathrm{bc}}^T\bar R,
𝐔ˉ+=(∂cbc∂𝐔)Tcˉbc,Dˉ+=(∂cbc∂D)Tcˉbc.\bar{\mathbf U}\mathrel{\text{+=}}\Big(\frac{\partial c_{\mathrm{bc}}}{\partial \mathbf U}\Big)^T\bar c_{\mathrm{bc}},\quad \bar D\mathrel{\text{+=}}\Big(\frac{\partial c_{\mathrm{bc}}}{\partial D}\Big)^T\bar c_{\mathrm{bc}}.
(4′)Hˉf=ℬ:,fTRˉraw=Rˉraw,L−Rˉraw,R,(4')\quad \bar H_f=\mathcal B_{:,f}^T\bar R_{raw}=\bar R_{raw,L}-\bar R_{raw,R},
(3′)zˉf=Hz,fT Hˉf,(3')\quad \bar z_f=H_{z,f}^T\,\bar H_f,
(2′)𝐔ˉf=ZfT zˉf,Dˉ+=KfT zˉf,(2')\quad \bar{\mathbf U}_f=Z_f^T\,\bar z_f,\quad \bar D\mathrel{\text{+=}}K_f^T\,\bar z_f,
(1′)𝐔ˉ+=𝒢fT 𝐔ˉf.(1')\quad \bar{\mathbf U}\mathrel{\text{+=}}\mathcal G_f^T\,\bar{\mathbf U}_f.

面循环结束后,源项的一对也按同样的规则反过来:Sˉi=−Rˉraw,i,m\bar S_i=-\bar R_{raw,i,m},𝐔ˉ+=(∂S/∂𝐔)TSˉ\bar{\mathbf U}\mathrel{\text{+=}}(\partial S/\partial \mathbf U)^T\bar S,Dˉ+=(∂S/∂D)TSˉ\bar D\mathrel{\text{+=}}(\partial S/\partial D)^T\bar S;被替换的行已由上面的(5′)处理:它们的种子不进入面循环,写进这些行的源项因此得不到权重。本页代码里每一条语句的伴随语句(顺序与前向相反)也都在本节里逐句写出。

After the face loop the source is reversed by the same rule: Sˉi=−Rˉraw,i,m\bar S_i=-\bar R_{raw,i,m}, 𝐔ˉ+=(∂S/∂𝐔)TSˉ\bar{\mathbf U}\mathrel{\text{+=}}(\partial S/\partial \mathbf U)^T\bar S, Dˉ+=(∂S/∂D)TSˉ\bar D\mathrel{\text{+=}}(\partial S/\partial D)^T\bar S; the replaced rows were handled by (5′) above: their seeds do not enter the face loop, so a source written into such a row receives no weight. The adjoint statement of every statement of this page's code, in the reverse order, is written out in this section as well.

分发与取值互为转置。分发 Rraw+=ℬ:,fHfR_{raw}\mathrel{\text{+=}}\mathcal B_{:,f}H_f 把一个面通量写进两行;它的转置 Hˉf=ℬ:,fTRˉraw\bar H_f=\mathcal B_{:,f}^T\bar R_{raw} 从这两行读回一个数,是一次取值。取值 𝐔f=𝒢f𝐔\mathbf U_f=\mathcal G_f\mathbf U 从全局数组读出几个状态;它的转置 𝒢fT\mathcal G_f^T 把它们的伴随加回原来的位置,是一次带累加的分发。

Scatter and gather are transposes of each other. The scatter Rraw+=ℬ:,fHfR_{raw}\mathrel{\text{+=}}\mathcal B_{:,f}H_f writes one face flux into two rows; its transpose Hˉf=ℬ:,fTRˉraw\bar H_f=\mathcal B_{:,f}^T\bar R_{raw} reads those two rows back into one number: a gather. The gather 𝐔f=𝒢f𝐔\mathbf U_f=\mathcal G_f\mathbf U reads a few states out of the global array; its transpose 𝒢fT\mathcal G_f^T adds their adjoints back into their places: a scatter with accumulation.

为什么要先置零、再用 += 累加:程序里的一次扇出(一个值被多处读取)在反向扫描里变成扇入。一个存储状态属于它每个相邻面的 𝒮f\mathcal S_f,设计变量也进入许多面与源项;链式法则对变量的每一次使用各给出一项,xˉ\bar x 是所有这些项的和:

Why zero first and then +=: a fan-out of the program (one value read in several places) becomes a fan-in of the reverse sweep. A stored state belongs to the 𝒮f\mathcal S_f of every neighbouring face, and the design enters many faces and the sources; the chain rule gives one term per use of a variable, and xˉ\bar x is the sum of all these terms:

Uˉj=∑f: j∈𝒮f(𝒢fT 𝐔ˉf)j+(∂S∂Uj)TSˉ.\bar U_j=\sum_{f:\ j\in\mathcal S_f}\big(\mathcal G_f^T\,\bar{\mathbf U}_f\big)_j+\Big(\frac{\partial S}{\partial U_j}\Big)^T\bar S.

xˉ←0\bar x\leftarrow0 让这个和从零开始,每个 += 加上一次使用的贡献。反过来,程序里的扇入(Rraw,LR_{raw,L} 收集它各个面的通量)在反向扫描里变成扇出:Rˉraw,L\bar R_{raw,L} 只被读取,不需要累加。

xˉ←0\bar x\leftarrow0 starts this sum at zero, and each += adds the term of one use. Conversely, a fan-in of the program (Rraw,LR_{raw,L} collecting the fluxes of all its faces) becomes a fan-out of the reverse sweep: Rˉraw,L\bar R_{raw,L} is only read and needs no accumulation.

对偶恒等式的逐步证明,以及它的数值形式:点积测试

The duality identity step by step, and its numerical form: the dot-product test

把同一个面的前向语句与反向语句配对。每一步只用一次转置的定义 ⟨xˉ,ℰx˙⟩=⟨ℰTxˉ,x˙⟩\langle\bar x,\mathcal E\dot x\rangle=\langle\mathcal E^T\bar x,\dot x\rangle,把一个初等 Jacobian 从内积的右边移到左边,所以内积在每一级都相同:

Pair the forward and reverse statements of the same face. Each step uses the definition of the transpose once, ⟨xˉ,ℰx˙⟩=⟨ℰTxˉ,x˙⟩\langle\bar x,\mathcal E\dot x\rangle=\langle\mathcal E^T\bar x,\dot x\rangle, to move one elementary Jacobian from the right of the inner product to the left, so the inner product is the same at every stage:

RˉrawT(ℬ:,f H˙f)=(ℬ:,fTRˉraw)TH˙f=HˉfTH˙f,\bar R_{raw}^T\big(\mathcal B_{:,f}\,\dot H_f\big)=\big(\mathcal B_{:,f}^T\bar R_{raw}\big)^T\dot H_f=\bar H_f^T\dot H_f,
HˉfTH˙f=HˉfTHz,f z˙f=(Hz,fTHˉf)Tz˙f=zˉfTz˙f,\bar H_f^T\dot H_f=\bar H_f^TH_{z,f}\,\dot z_f=\big(H_{z,f}^T\bar H_f\big)^T\dot z_f=\bar z_f^T\dot z_f,
zˉfTz˙f=zˉfT(Zf𝐔˙f+KfD˙)\bar z_f^T\dot z_f=\bar z_f^T\big(Z_f\dot{\mathbf U}_f+K_f\dot D\big)
=𝐔ˉfT𝐔˙f+(KfTzˉf)TD˙,=\bar{\mathbf U}_f^T\dot{\mathbf U}_f+\big(K_f^T\bar z_f\big)^T\dot D,
𝐔ˉfT𝐔˙f=𝐔ˉfT𝒢f 𝐔˙=(𝒢fT𝐔ˉf)T𝐔˙.\bar{\mathbf U}_f^T\dot{\mathbf U}_f=\bar{\mathbf U}_f^T\mathcal G_f\,\dot{\mathbf U}=\big(\mathcal G_f^T\bar{\mathbf U}_f\big)^T\dot{\mathbf U}.

对所有面求和,再加上源项的一对 SˉTS˙\bar S^T\dot S(Sˉi=−Rˉraw,i,m\bar S_i=-\bar R_{raw,i,m}),右边的两项正好累加成反向扫描的输出 𝐔ˉ\bar{\mathbf U} 与 Dˉ\bar D:

Summed over all faces, with the pair of the source added (SˉTS˙\bar S^T\dot S with Sˉi=−Rˉraw,i,m\bar S_i=-\bar R_{raw,i,m}), the two terms on the right accumulate exactly into the outputs 𝐔ˉ\bar{\mathbf U} and Dˉ\bar D of the reverse sweep:

RˉrawTR˙raw=𝐔ˉT𝐔˙+DˉTD˙.\bar R_{raw}^T\dot R_{raw}=\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D.

被替换的行再加上它们约束的一对 cˉbcTc˙bc\bar c_{\mathrm{bc}}^T\dot c_{\mathrm{bc}}(c˙bc\dot c_{\mathrm{bc}} 是切向量一节里覆盖语句的切向量):它正是(5′)里约束的状态路径与设计路径对 𝐔ˉT𝐔˙+DˉTD˙\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D 的贡献;所以同一个恒等式对本页的方程组也成立(𝐔ˉ\bar{\mathbf U} 与 Dˉ\bar D 取整个反向扫描结束时的值),下面的测试用的就是它:

The replaced rows add the pair of their constraints, cˉbcTc˙bc\bar c_{\mathrm{bc}}^T\dot c_{\mathrm{bc}} (c˙bc\dot c_{\mathrm{bc}} is the tangent of the overwrite in the tangent section): it is exactly what the state path and the design path of the constraints in (5′) contribute to 𝐔ˉT𝐔˙+DˉTD˙\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D. The same identity therefore holds for this page's system (𝐔ˉ\bar{\mathbf U} and Dˉ\bar D taken at the end of the whole reverse sweep), and the test below uses that system:

RˉTR˙=RˉrawTR˙raw+cˉbcTc˙bc=𝐔ˉT𝐔˙+DˉTD˙.\bar R^T\dot R=\bar R_{raw}^T\dot R_{raw}+\bar c_{\mathrm{bc}}^T\dot c_{\mathrm{bc}}=\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D.

这就是 AD 的点积测试:任取种子 (𝐔˙,D˙)(\dot{\mathbf U},\dot D) 与 Rˉ\bar R,前向扫描算出 R˙\dot R,反向扫描算出 (𝐔ˉ,Dˉ)(\bar{\mathbf U},\bar D),两边之差应在舍入误差量级;只要有一条反向语句的转置写错,差就是 O(1)O(1) 的。本页在构建时用它自己的 matrix-free 前向与反向乘积(nu=39n_u=39 个分量的确定性种子,设计方向取下面算例的 DkD_k)做了这个测试:RˉTR˙=17.220812929\bar R^T\dot R=17.220812929,𝐔ˉT𝐔˙+DˉTD˙=17.220812929\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D=17.220812929,相对差 2×10−162\times10^{-16}。

This is the dot-product test of AD: take any seeds (𝐔˙,D˙)(\dot{\mathbf U},\dot D) and Rˉ\bar R, run the forward sweep for R˙\dot R and the reverse sweep for (𝐔ˉ,Dˉ)(\bar{\mathbf U},\bar D); the two sides must agree to round-off, and a single wrong transpose in the reverse statements gives an O(1)O(1) difference. This page ran the test on its own matrix-free forward and reverse products at build time (deterministic seeds with nu=39n_u=39 components, the design direction the DkD_k of the example below): RˉTR˙=17.220812929\bar R^T\dot R=17.220812929 and 𝐔ˉT𝐔˙+DˉTD˙=17.220812929\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D=17.220812929, a relative difference of 2×10−162\times10^{-16}.

同一个面的反向扫描

The reverse sweep of the same face

反向种子取本页伴随求解的解 Rˉ=ψ\bar R=\psi;面循环的种子是掩码后的 Rˉraw=Mbcψ\bar R_{raw}=M_{\mathrm{bc}}\psi,这个面的两行都是保留行,所以它在这两行等于 ψ\psi。于是 𝐔ˉf\bar{\mathbf U}_f 与 Dˉk\bar D_k 是这个面对 JrawTMbcψJ_{raw}^TM_{\mathrm{bc}}\psi 与 RD,rawTMbcψR_{D,raw}^TM_{\mathrm{bc}}\psi 的贡献(也是对 JTψJ^T\psi 与 RDTψR_D^T\psi 的贡献)。下表按反向语句的顺序列出伴随值;最后一列是每一级的内积:先是 Rˉ5TR˙5+Rˉ6TR˙6\bar R_{5}^T\dot R_{5}+\bar R_{6}^T\dot R_{6},然后是 HˉfTH˙f\bar H_f^T\dot H_f、zˉfTz˙f\bar z_f^T\dot z_f,最后是 𝐔ˉfT𝐔˙f+DˉTD˙\bar{\mathbf U}_f^T\dot{\mathbf U}_f+\bar D^T\dot D,切向量取第 8 节“本页一个面的算例:前向扫描”里同一个面的前向算例的值。

Seed the reverse sweep with this page's adjoint solution, Rˉ=ψ\bar R=\psi; the seed of the face loop is the masked one, Rˉraw=Mbcψ\bar R_{raw}=M_{\mathrm{bc}}\psi, and both rows of this face are retained, so it equals ψ\psi in them. Then 𝐔ˉf\bar{\mathbf U}_f and Dˉk\bar D_k are this face's contributions to JrawTMbcψJ_{raw}^TM_{\mathrm{bc}}\psi and RD,rawTMbcψR_{D,raw}^TM_{\mathrm{bc}}\psi (and so to JTψJ^T\psi and RDTψR_D^T\psi). The table lists the adjoints in the order of the reverse statements; the last column is the inner product at each stage: first Rˉ5TR˙5+Rˉ6TR˙6\bar R_{5}^T\dot R_{5}+\bar R_{6}^T\dot R_{6}, then HˉfTH˙f\bar H_f^T\dot H_f, zˉfTz˙f\bar z_f^T\dot z_f, and last 𝐔ˉfT𝐔˙f+DˉTD˙\bar{\mathbf U}_f^T\dot{\mathbf U}_f+\bar D^T\dot D, with the tangents of the forward example of the same face, the one under “A worked example on this page: the forward sweep of one face” in Section 8.

语句Statement量Quantity伴随Adjoint内积Inner product
(0)置零(0) zero𝐔ˉ←0, Dˉ←0\bar{\mathbf U}\leftarrow0,\ \bar D\leftarrow00
(4′)种子(4′) seedRˉ5=ψ5\bar R_{5}=\psi_5−0.01341
0.03201
−0.003943
−0.00164553151
Rˉ6=ψ6\bar R_{6}=\psi_6−0.01694
0.03729
−0.004937
(4′)取值(4′) gatherHˉf=Rˉ5−Rˉ6\bar H_f=\bar R_{5}-\bar R_{6}0.003538
−0.005283
9.935×10⁻⁴
−0.00164553151
(3′)通量的转置(3′) flux, transposedUˉf−\bar U_f^-0.002292
−0.003336
2.82×10⁻⁴
−0.00164553151
Uˉf+\bar U_f^+−0.00243
0.003706
−0.001043
Aˉf\bar A_f−0.002076
(2′)(1′)加回(2′) (1′) add backUˉ4+=\bar U_{4}\mathrel{\text{+=}}−5.731×10⁻⁴
8.34×10⁻⁴
−7.051×10⁻⁵
−0.00164553151
Uˉ5+=\bar U_{5}\mathrel{\text{+=}}0.001685
−0.00241
2.13×10⁻⁵
Uˉ6+=\bar U_{6}\mathrel{\text{+=}}−0.001857
0.002872
−9.724×10⁻⁴
Uˉ7+=\bar U_{7}\mathrel{\text{+=}}6.074×10⁻⁴
−9.264×10⁻⁴
2.607×10⁻⁴
Dˉk+=\bar D_k\mathrel{\text{+=}}−0.001038

4 级内积相同,最大相对差 8×10−168\times10^{-16}:对偶恒等式在这些数上成立;下面的面板按反向语句逐条播放这些数。Dˉk=−0.001038\bar D_k=-0.001038 是这个面对梯度分量 dQ^/dDkd\widehat{Q}/dD_k 的贡献,经过 KfTzˉfK_f^T\bar z_f 这条直接路径得到。

The 4 inner products agree, with a largest relative difference of 8×10−168\times10^{-16}: the duality identity holds in these numbers. The panel below plays them statement by statement in reverse. Dˉk=−0.001038\bar D_k=-0.001038 is this face's contribution to the gradient component dQ^/dDkd\widehat{Q}/dD_k, along the direct path KfTzˉfK_f^T\bar z_f.

同一个面的反向扫描
The reverse sweep of the same face

箭头全部反向,每个初等 Jacobian 转置,顺序相反;存储状态与设计变量处用 += 累加。下方:转置的乘积与形状。

Every arrow reversed, every elementary Jacobian transposed, in the reverse order; the stored states and the design accumulate with +=. Below: the transposed product and its shapes.

全部存储状态all stored statesUU¯​+=面读取的状态states the face readsUf​U¯​f​面输入:重构值等face inputszf​z¯​f​面通量face fluxHf​H¯​f​两侧残差行rows L and RRL, RR​R¯​L, R¯​R​设计输入design inputDD¯​+=𝒢fT​ZfT​Hz,fT​ℬ:,fT​KfT​Jraw,fT=𝒢fT​39×12ZfT​12×7Hz,fT​7×3ℬ:,fT​3×39R¯​raw​U¯​从右往左作用:先转置的分发(最右),最后转置的取值(最左)applied right to left: the transposed scatter (rightmost) first, the transposed gather last全部状态all statesUU¯​+=模板上的状态stencilUf​U¯​f​面输入face inputszf​z¯​f​面通量face fluxHf​H¯​f​L、R 两行rows L, RRL, RR​R¯​L, R¯​R​设计输入design inputDD¯​+=𝒢fT​ZfT​Hz,fT​ℬ:,fT​KfT​Jraw,fT=𝒢fT​39×12ZfT​12×7Hz,fT​7×3ℬ:,fT​3×39R¯​raw​U¯​从右往左作用:applied right to left: the先转置的分发,最后转置的取值transposed scatter first, gather last
  1. (0)置零并给种子:𝐔ˉ←0\bar{\mathbf U}\leftarrow0,Dˉ←0\bar D\leftarrow0;Rˉ5=ψ5=\bar R_{5}=\psi_5= (−0.01341, 0.03201, −0.003943),Rˉ6=ψ6=\bar R_{6}=\psi_6= (−0.01694, 0.03729, −0.004937)(这个面的两行都是保留行,掩码后的种子 Rˉraw=Mbcψ\bar R_{raw}=M_{\mathrm{bc}}\psi 在这两行等于 ψ\psi)。

    (0) Zero and seed: 𝐔ˉ←0\bar{\mathbf U}\leftarrow0, Dˉ←0\bar D\leftarrow0; Rˉ5=ψ5=\bar R_{5}=\psi_5= (−0.01341, 0.03201, −0.003943), Rˉ6=ψ6=\bar R_{6}=\psi_6= (−0.01694, 0.03729, −0.004937) (both rows of this face are retained, so the masked seed Rˉraw=Mbcψ\bar R_{raw}=M_{\mathrm{bc}}\psi equals ψ\psi in them).

  2. (4′)分发的转置是一次取值:Hˉf=Rˉ5−Rˉ6=\bar H_f=\bar R_{5}-\bar R_{6}= (0.003538, −0.005283, 9.935×10⁻⁴)。内积 Rˉ5TR˙5+Rˉ6TR˙6=HˉfTH˙f=\bar R_{5}^T\dot R_{5}+\bar R_{6}^T\dot R_{6}=\bar H_f^T\dot H_f= −0.00164553151。

    (4′) The transposed scatter is a gather: Hˉf=Rˉ5−Rˉ6=\bar H_f=\bar R_{5}-\bar R_{6}= (0.003538, −0.005283, 9.935×10⁻⁴). Inner product Rˉ5TR˙5+Rˉ6TR˙6=HˉfTH˙f=\bar R_{5}^T\dot R_{5}+\bar R_{6}^T\dot R_{6}=\bar H_f^T\dot H_f= −0.00164553151.

  3. (3′)通量的转置:zˉf=Hz,fTHˉf\bar z_f=H_{z,f}^T\bar H_f,Uˉf−=\bar U_f^-= (0.002292, −0.003336, 2.82×10⁻⁴),Uˉf+=\bar U_f^+= (−0.00243, 0.003706, −0.001043),Aˉf=\bar A_f= −0.002076。内积 zˉfTz˙f=\bar z_f^T\dot z_f= −0.00164553151。

    (3′) The transposed flux: zˉf=Hz,fTHˉf\bar z_f=H_{z,f}^T\bar H_f, Uˉf−=\bar U_f^-= (0.002292, −0.003336, 2.82×10⁻⁴), Uˉf+=\bar U_f^+= (−0.00243, 0.003706, −0.001043), Aˉf=\bar A_f= −0.002076. Inner product zˉfTz˙f=\bar z_f^T\dot z_f= −0.00164553151.

  4. (2′)重构的转置:𝐔ˉf=ZfTzˉf\bar{\mathbf U}_f=Z_f^T\bar z_f,Dˉ+=KfTzˉf\bar D\mathrel{\text{+=}}K_f^T\bar z_f,这个面给出 Dˉk=\bar D_k= −0.001038。

    (2′) The transposed reconstruction: 𝐔ˉf=ZfTzˉf\bar{\mathbf U}_f=Z_f^T\bar z_f and Dˉ+=KfTzˉf\bar D\mathrel{\text{+=}}K_f^T\bar z_f; this face gives Dˉk=\bar D_k= −0.001038.

  5. (1′)取值的转置是带累加的分发:𝐔ˉ+=𝒢fT𝐔ˉf\bar{\mathbf U}\mathrel{\text{+=}}\mathcal G_f^T\bar{\mathbf U}_f,Uˉ4+=\bar U_{4}\mathrel{\text{+=}} (−5.731×10⁻⁴, 8.34×10⁻⁴, −7.051×10⁻⁵),Uˉ5+=\bar U_{5}\mathrel{\text{+=}} (0.001685, −0.00241, 2.13×10⁻⁵),Uˉ6+=\bar U_{6}\mathrel{\text{+=}} (−0.001857, 0.002872, −9.724×10⁻⁴),Uˉ7+=\bar U_{7}\mathrel{\text{+=}} (6.074×10⁻⁴, −9.264×10⁻⁴, 2.607×10⁻⁴)。内积 𝐔ˉfT𝐔˙f+DˉTD˙=\bar{\mathbf U}_f^T\dot{\mathbf U}_f+\bar D^T\dot D= −0.00164553151。

    (1′) The transposed gather is a scatter with accumulation: 𝐔ˉ+=𝒢fT𝐔ˉf\bar{\mathbf U}\mathrel{\text{+=}}\mathcal G_f^T\bar{\mathbf U}_f, Uˉ4+=\bar U_{4}\mathrel{\text{+=}} (−5.731×10⁻⁴, 8.34×10⁻⁴, −7.051×10⁻⁵), Uˉ5+=\bar U_{5}\mathrel{\text{+=}} (0.001685, −0.00241, 2.13×10⁻⁵), Uˉ6+=\bar U_{6}\mathrel{\text{+=}} (−0.001857, 0.002872, −9.724×10⁻⁴), Uˉ7+=\bar U_{7}\mathrel{\text{+=}} (6.074×10⁻⁴, −9.264×10⁻⁴, 2.607×10⁻⁴). Inner product 𝐔ˉfT𝐔˙f+DˉTD˙=\bar{\mathbf U}_f^T\dot{\mathbf U}_f+\bar D^T\dot D= −0.00164553151.

  6. 对偶:4 级内积相同(最大相对差 8×10⁻¹⁶),RˉTR˙=𝐔ˉT𝐔˙+DˉTD˙\bar R^T\dot R=\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D 在这个面上逐级成立。

    Duality: the 4 inner products agree (largest relative difference 8×10⁻¹⁶); RˉTR˙=𝐔ˉT𝐔˙+DˉTD˙\bar R^T\dot R=\bar{\mathbf U}^T\dot{\mathbf U}+\bar D^T\dot D holds stage by stage on this face.

逐句的伴随:逆序与累加

The adjoint, statement by statement: reverse order and accumulation

伴随把上一节的语句表倒过来执行:从最后一句开始,每一句把它输出的横线量乘以局部偏导数,用 += 累加到它的每个输入上;第 3、4 节的矩阵在这里以转置出现。

The adjoint executes the statement table of the previous section backwards: starting from the last statement, each multiplies the bar of its output by its local partial derivatives and accumulates the result into every input with +=; the matrices of Sections 3 and 4 appear transposed.

##伴随语句(逆序,累加 +=)adjoint statement (reverse order, accumulate +=)
⑪ 先做⑪ first𝐔ˉ+=(∂cr∂𝐔)TRˉr,Dˉ+=(∂cr∂D)TRˉr,Rˉr←0\bar{\mathbf U}\mathrel{\text{+=}}\Bigl(\frac{\partial c_r}{\partial\mathbf U}\Bigr)^T\bar R_r,\quad\bar D\mathrel{\text{+=}}\Bigl(\frac{\partial c_r}{\partial D}\Bigr)^T\bar R_r,\quad\bar R_r\leftarrow0
⑩ 控制体 i(从后向前);行的种子保留⑩ volume i (last to first); the row seed is keptSˉi=−Rˉi,m\bar S_i=-\bar R_{i,m}
⑨ Gauss 点 q⑨ Gauss point qpˉq+=Qˉ wq∂θq∂pq,Dˉ+=Qˉ wq∂θq∂D\bar p_q\mathrel{\text{+=}}\bar Q\,w_q\frac{\partial\theta_q}{\partial p_q},\quad\bar D\mathrel{\text{+=}}\bar Q\,w_q\frac{\partial\theta_q}{\partial D}
⑧⑧pˉq+=wqGqSˉi,Gˉq=wqpqSˉi,Aˉk+1+=Gˉq/h,Aˉk-=Gˉq/h\bar p_q\mathrel{\text{+=}}w_qG_q\bar S_i,\quad\bar G_q=w_qp_q\bar S_i,\quad\bar A_{k+1}\mathrel{\text{+=}}\bar G_q/h,\quad\bar A_k\mathrel{\text{-=}}\bar G_q/h
⑦⑦Uˉq=pUTpˉq,Uˉj+=WqjUˉq\bar U_q=p_U^T\bar p_q,\quad\bar U_j\mathrel{\text{+=}}W_{qj}\bar U_q
⑥ 面 f(从后向前)⑥ face f (last to first)Hˉf=RˉL−RˉR\bar H_f=\bar R_L-\bar R_R
⑤⑤Aˉf=H^THˉf,Uˉf−=AfH^−THˉf,Uˉf+=AfH^+THˉf\bar A_f=\widehat H^T\bar H_f,\quad\bar U_f^-=A_f\widehat H_-^T\bar H_f,\quad\bar U_f^+=A_f\widehat H_+^T\bar H_f
④ 端面(代替 ⑤)④ end face (instead of ⑤)Aˉf=FTHˉf,Uˉf=AfFUTHˉf\bar A_f=F^T\bar H_f,\quad\bar U_f=A_fF_U^T\bar H_f
③③Aˉk+=(1−tf)Aˉf,Aˉk+1+=tfAˉf\bar A_k\mathrel{\text{+=}}(1-t_f)\bar A_f,\quad\bar A_{k+1}\mathrel{\text{+=}}t_f\bar A_f
②②Uˉj+=Wfj+Uˉf+\bar U_j\mathrel{\text{+=}}W^+_{fj}\bar U_f^+
①①Uˉj+=Wfj−Uˉf−\bar U_j\mathrel{\text{+=}}W^-_{fj}\bar U_f^-

表中有两类语句。⑥ 与 ⑩ 是累加(RL+=HfR_L\mathrel{\text{+=}}H_f、Ri,m-=SiR_{i,m}\mathrel{\text{-=}}S_i):正如上面“从切向量到伴随”所说,程序里的扇入在反向扫描里变成扇出,所以它们的伴随只读取 Rˉ\bar R,不把它清零;⑧、⑨ 对 Sˉi\bar S_i 与 Qˉ\bar Q 也是如此。只有覆盖赋值——本页的 ⑪——在反传后把种子清零。把累加当成覆盖会丢掉路径:例如处理完源项就把 Rˉi,m\bar R_{i,m} 清零,面通量就再也得不到这一行的种子;把覆盖当成累加,则会把已擦除的守恒行重新算进来。格点页的 ⑪ 就是上面共享推导里的覆盖语句(5),它的反向语句(5′)最先执行:三条被替换的行的种子已送进约束并清零,后面的 ⑩ 与 ⑥ 读到的就是掩码后的种子 Rˉraw=MbcRˉ\bar R_{raw}=M_{\mathrm{bc}}\bar R。所以节点 0 的源项得到 Sˉ0=−Rˉraw,0,m=0\bar S_0=-\bar R_{raw,0,m}=0(它的动量行是被替换的第 1 行),面 0 的 Hˉf\bar H_f 只剩节点 0 种子的能量分量(质量与动量分量已被 ⑪ 清零)。

The table has two kinds of statement. ⑥ and ⑩ accumulate (RL+=HfR_L\mathrel{\text{+=}}H_f, Ri,m-=SiR_{i,m}\mathrel{\text{-=}}S_i): as “From tangent to adjoint” above explains, a fan-in of the program becomes a fan-out of the reverse sweep, so their adjoints only read Rˉ\bar R and leave it; the same holds for Sˉi\bar S_i and Qˉ\bar Q in ⑧ and ⑨. Only an overwrite, ⑪ on this page, zeroes its seed after the reverse step. Treating an accumulation as an overwrite loses paths: zeroing Rˉi,m\bar R_{i,m} after the source, for instance, would leave the face fluxes without the seed of that row; treating an overwrite as an accumulation brings erased balance rows back. On the node page ⑪ is the overwrite (5) of the shared derivation above, so its reverse statement (5′) runs first: the seeds of the three replaced rows have been sent into the constraints and zeroed, so ⑩ and ⑥ read the masked seed Rˉraw=MbcRˉ\bar R_{raw}=M_{\mathrm{bc}}\bar R. The source of node 0 therefore gets Sˉ0=−Rˉraw,0,m=0\bar S_0=-\bar R_{raw,0,m}=0 (its momentum row is the replaced row 1), and Hˉf\bar H_f of face 0 keeps only the energy component of the seed of node 0 (the mass and momentum components were zeroed by ⑪).

算例:种子 Rˉ=ψ\bar R=\psi(第 11 节的伴随解)、Qˉ=1\bar Q=1,在第 3 节的面 f=6f=6 上。上面的推导(“同一个面的反向扫描”)用同样的残差种子反传这个面,列出的伴随和下面相同;它的语句(4′)、(3′)、(2′)至(1′)就是本表的 ⑥、⑤、③–①。第 ⑥ 句给出 Hˉf=ψ5−ψ6\bar H_f=\psi_{5}-\psi_{6},第 ⑤ 句给出 Aˉf=H^THˉf=−0.002076\bar A_f=\widehat H^T\bar H_f=-0.002076,以及

Worked example: the seeds Rˉ=ψ\bar R=\psi (the adjoint solution of Section 11) and Qˉ=1\bar Q=1, at the face f=6f=6 of Section 3. The derivation above (“The reverse sweep of the same face”) reverses this face with the same residual seed and lists the same adjoints; its statements (4′), (3′) and (2′)–(1′) are ⑥, ⑤ and ③–① of this table. Statement ⑥ gives Hˉf=ψ5−ψ6\bar H_f=\psi_{5}-\psi_{6} and statement ⑤ gives Aˉf=H^THˉf=−0.002076\bar A_f=\widehat H^T\bar H_f=-0.002076 and

Hˉf=(0.003538, −0.005283, 9.935×10−4)T,Uˉf−=(0.002292, −0.003336, 2.82×10−4)T,Uˉf+=(−0.00243, 0.003706, −0.001043)T.\bar H_f=\bigl(0.003538,\ -0.005283,\ 9.935\times10^{-4}\bigr)^T,\qquad\bar U_f^-=\bigl(0.002292,\ -0.003336,\ 2.82\times10^{-4}\bigr)^T,\qquad\bar U_f^+=\bigl(-0.00243,\ 0.003706,\ -0.001043\bigr)^T.

经第 ②① 句的重构权重散回存储状态后,它们与运算记录对这个面的反传结果相差 8.7×10−198.7\times10^{-19}(面积分量相差 00)。所有语句执行完,𝐔ˉ=JTψ+QUT\bar{\mathbf U}=J^T\psi+Q_U^T 的范数为 1.2×10−121.2\times10^{-12},这是伴随方程的残差;Dˉ\bar D 与梯度 gg 相差 7.8×10−187.8\times10^{-18}。边界行的伴随(⑪)见第 4 节:它直接给出 gp0=−ψ0g_{p_0}=-\psi_0 等三个分量。

Scattered to the stored states by the reconstruction weights of ② and ①, they differ from the tape’s reverse of this face by 8.7×10−198.7\times10^{-19} (the area entries by 00). After all statements 𝐔ˉ=JTψ+QUT\bar{\mathbf U}=J^T\psi+Q_U^T has norm 1.2×10−121.2\times10^{-12}, the residual of the adjoint equation, and Dˉ\bar D differs from the gradient gg by 7.8×10−187.8\times10^{-18}. The adjoint of the boundary rows (⑪) is in Section 4: it gives gp0=−ψ0g_{p_0}=-\psi_0 and the two other components directly.

两个种子同时出现时,上面的对偶恒等式扩展为

With both seeds present, the duality identity above extends to

RˉTR˙+Qˉ Q˙=UˉTU˙+DˉTD˙.\bar R^T\dot R+\bar Q\,\dot Q=\bar U^T\dot U+\bar D^T\dot D.

它逐句成立,证明与上面“对偶恒等式的逐步证明”相同,只是这里对本页两张表的每一条语句都适用。图 8 用随机种子把两边逐项累加:两条折线的路径不同,终点相同。图 7 逐句播放 Rˉ=ψ\bar R=\psi、Qˉ=1\bar Q=1 的反向扫描:Uˉ\bar U 结束于伴随方程的残差(大小由线性求解的容差决定,不是舍入误差,数值见上),Dˉ\bar D 结束于梯度,并按路径着色。

It holds statement by statement, by the proof of “The duality identity step by step” above, here applied to every statement of the two tables of this page. Figure 8 accumulates the two sides term by term for random seeds: the two polylines take different paths and end at the same value. Figure 7 plays the reverse sweep of Rˉ=ψ\bar R=\psi, Qˉ=1\bar Q=1 statement by statement: Uˉ\bar U ends at the residual of the adjoint equation (its size is set by the tolerance of the linear solve, not by rounding; the value is given above) and Dˉ\bar D at the gradient, coloured by path.

图 7 · 反向扫描:种子 Rˉ=ψ\bar R=\psi、Qˉ=1\bar Q=1 按与原始计算相反的顺序逐句传播(播放、暂停、单步、调速)。上:当前语句所在的面或控制体;中:Uˉ\bar U 的三个分量;下:Dˉ\bar D 的全部分量,按路径着色(面面积、源项斜率、目标、边界数据)。扫描结束时 Uˉ\bar U 等于伴随方程的残差,Dˉ\bar D 等于梯度 gg。Figure 7 · The reverse sweep: the seeds Rˉ=ψ\bar R=\psi, Qˉ=1\bar Q=1 travel through the statements in the reverse of the primal order (play, pause, step, speed). Top: the face or volume of the current statement; middle: the three components of Uˉ\bar U; bottom: every component of Dˉ\bar D, coloured by path (face areas, source slopes, objective, boundary data). At the end Uˉ\bar U is the residual of the adjoint equation and Dˉ\bar D the gradient gg.
图 8 · 对偶恒等式两边的逐项累加(本页代码的前向与伴随,一组随机种子,可以换一组)。左边 RˉTR˙+QˉQ˙\bar R^T\dot R+\bar Q\dot Q 按残差行累加,右边 UˉTU˙+DˉTD˙\bar U^T\dot U+\bar D^T\dot D 先按状态、再按设计变量累加;两条折线的路径不同,终点相同。Figure 8 · The two sides of the duality identity accumulated term by term (this page’s tangent and adjoint code, one set of random seeds, which can be redrawn). The left side RˉTR˙+QˉQ˙\bar R^T\dot R+\bar Q\dot Q accumulates over the residual rows, the right side UˉTU˙+DˉTD˙\bar U^T\dot U+\bar D^T\dot D over the states and then the design variables; the two polylines take different paths and end at the same value.
第 10 节Section 10

前向方程的求解:一个设计方向一次线性求解

Tangent solve: one linear system per design direction

收敛方程 R(U(D),D)=0 对参数求导,得到 J UDU_D=−RDR_D。这里 J 是收敛状态处的精确算子,不是预处理矩阵 P。解出 UDU_D 后再计算 dQ/dD=QDQ_D+QUQ_U UDU_D。图中状态敏感度是一个解向量,和直接在任意 v 上做一次前向传播是两件不同的事。对一个设计方向 D˙\dot D,解出的状态切向量是 U˙=UDD˙\dot U=U_D\dot D;下面的推导记作 v,伪代码记作 x,即 v≡U˙v\equiv\dot U。

Differentiate the converged equation R(U(D),D)=0 to obtain J UDU_D=−RDR_D. J is the exact operator at the converged state, not preconditioner P. Then dQ/dD=QDQ_D+QUQ_U UDU_D. The plotted state sensitivity is a solved vector, distinct from a single tangent application to an arbitrary v. For one design direction D˙\dot D the solved state tangent is U˙=UDD˙\dot U=U_D\dot D; the derivation below calls it v and the pseudo-code x, so v≡U˙v\equiv\dot U.

JU˙=−RDD˙,r(k)=−RDD˙−JU˙(k),U˙(k+1)=U˙(k)+0.7Br(k).J\dot U=-R_D\dot D,\quad r^{(k)}=-R_D\dot D-J\dot U^{(k)},\quad\dot U^{(k+1)}=\dot U^{(k)}+0.7Br^{(k)}.
# D_d: design tangent; x = v = U_d: state tangent
rhs = -residual_d(U, U_d=0, D_d)
x = 0
repeat until norm(rhs-J_times(x)) < tolerance:
    x += 0.7 * twelve_GS_sweeps(P, rhs-J_times(x))
Q_d = objective_d(U, U_d=x, D_d)

把隐函数求导展开成可求解的系统

Expand implicit differentiation into a solvable system

设原始残差已经收敛,且当前光滑分支上的 JJ 非奇异。取设计种子(方向)D˙\dot D,令 D(t)=D+t D˙D(t)=D+t\,\dot D;前向求解得到的状态切向量是 v≡𝐔˙=ddt𝐔(D+t D˙)∣t=0v\equiv\dot{\mathbf U}=\frac{d}{dt}\mathbf U(D+t\,\dot D)\big|_{t=0}。对每一个残差分量逐项求导,零残差的切向量仍为零:

Assume the primal residual is converged and JJ is nonsingular on the current smooth branch. Choose a design seed (direction) D˙\dot D and set D(t)=D+t D˙D(t)=D+t\,\dot D; the state tangent returned by the tangent solve is v≡𝐔˙=ddt𝐔(D+t D˙)∣t=0v\equiv\dot{\mathbf U}=\frac{d}{dt}\mathbf U(D+t\,\dot D)\big|_{t=0}. Differentiate each residual component: the tangent of a zero residual is zero.

0=R˙r=∑j∂Rr∂Ujvj0=\dot R_r=\sum_j\frac{\partial R_r}{\partial U_j}v_j
+∑k∂Rr∂DkD˙k(1≤r≤nu)+\sum_k\frac{\partial R_r}{\partial D_k}\dot D_k\quad(1\le r\le n_u)
⟹Jv=−RD D˙.\Longrightarrow\quad Jv=-R_D\,\dot D.
Q^(D)=Q(𝐔(D),D),\widehat{Q}(D)=Q(\mathbf U(D),D),
Q˙=ddtQ^(D+t D˙)∣t=0=QUv+QD D˙.\dot Q=\frac{d}{dt}\widehat{Q}(D+t\,\dot D)\Big|_{t=0}=Q_Uv+Q_D\,\dot D.

实现分三步:状态种子 𝐔˙=0\dot{\mathbf U}=0、设计种子 D˙\dot D,前向求值残差得到 R˙=RD D˙\dot R=R_D\,\dot D;用精确 JJ 的乘积求解 vv;再以 𝐔˙=v\dot{\mathbf U}=v 和 D˙\dot D 为种子对目标前向求导,得到 Q˙\dot Q。一次 JvJv 只是算子调用(一次前向扫描),不是求解 vv。稳态导数的对象是收敛残差方程,不是 块 GS/GMRES 的迭代次数。在不可微点(如 limiter 的切换或分支判别处)以上普通导数需要另外讨论,不能无条件套用。

Implementation has three steps: seed the state with 𝐔˙=0\dot{\mathbf U}=0 and the design with D˙\dot D and evaluate the residual tangent R˙=RD D˙\dot R=R_D\,\dot D; solve for vv with exact JJ products; then differentiate the objective forward with the seeds 𝐔˙=v\dot{\mathbf U}=v and D˙\dot D to obtain Q˙\dot Q. One JvJv is an operator application (one forward sweep), not the solution vv. The steady derivative concerns the converged residual equation, not the block GS/GMRES iteration count. At nondifferentiable points (a limiter switch or a branch test) these ordinary derivatives need separate treatment.

用面循环的语句来看:右端 RDD˙R_D\dot D 是 𝐔˙=0\dot{\mathbf U}=0 时的一次前向扫描。它在保留的行上是面循环的 MbcRD,rawD˙M_{\mathrm{bc}}R_{D,raw}\dot D,只有 KfD˙K_f\dot D 与源项两条路径起作用;在被替换的行上是约束自己的设计路径,即第 0、1、3N3N 行里的 −p˙0-\dot p_0、−T˙0-\dot T_0、−p˙b-\dot p_b,任何面循环里都没有它:对边界数据 p0p_0、T0T_0 与 pbp_b,整个右端都来自这条路径(没有任何面读到它们,这三个分量的 RD,raw ek=0R_{D,raw}\,e_k=0)。迭代中的每个 JvJv 是 D˙=0\dot D=0 时的一次前向扫描:保留的行是面循环的,被替换的行是约束对状态的路径。一次前向求解的全部工作就是这段面循环与覆盖以不同的种子反复执行。对 nan_a 个设计分量逐个求导,要做 nan_a 次这样的求解。

In terms of the face loop's statements: the right-hand side RDD˙R_D\dot D is one forward sweep with 𝐔˙=0\dot{\mathbf U}=0. In the retained rows it is the face loop's MbcRD,rawD˙M_{\mathrm{bc}}R_{D,raw}\dot D, in which only the paths KfD˙K_f\dot D and the source are active; in the replaced rows it is the constraint's own design path, −p˙0-\dot p_0, −T˙0-\dot T_0 and −p˙b-\dot p_b in rows 0, 1 and 3N3N, which no face loop contains: for the boundary data p0p_0, T0T_0 and pbp_b the whole right-hand side comes from that path (no face reads them: RD,raw ek=0R_{D,raw}\,e_k=0 for these three components). Every JvJv of the iteration is one forward sweep with D˙=0\dot D=0: the face loop in the retained rows and the constraint's state path in the replaced rows. All the work of a tangent solve is this face loop and overwrite run again and again with different seeds, and the derivatives with respect to all nan_a design components take nan_a such solves.

一个设计种子的右端项

The right-hand side of one design seed

图 6 在 U˙=0\dot U=0 时算出的正是这里的 RDD˙R_D\dot D。截面积 AkA_k 只出现在读取它的面面积(节点 kk 两侧的面)、区间 k−1k-1 与 kk 的源项斜率和目标积分中,所以 RD ekR_D\,e_k 只有这些面和控制体的行非零:默认设置下 A6A_{6} 的列在 39 行中只有 7 行非零,属于控制体 5、6、7。pbp_b、p0p_0、T0T_0 只出现在三条边界行。右端项是稀疏的,解 U˙=−J−1RDD˙\dot U=-J^{-1}R_D\dot D 却在整个流道上都不为零:一个局部的设计变化会改变全部流动,所以每个设计分量都要一次完整的线性求解。

Figure 6 computes exactly the RDD˙R_D\dot D of this section when U˙=0\dot U=0. The area AkA_k appears only in the face areas that read it (the faces on either side of node kk), in the source slopes of the intervals k−1k-1 and kk, and in the objective integrals there, so only the rows of those faces and volumes are nonzero in RD ekR_D\,e_k: at the defaults the column of A6A_{6} has 7 nonzero rows out of 39, in volumes 5, 6, 7. pbp_b, p0p_0 and T0T_0 appear only in the three boundary rows. The right-hand side is sparse, yet the solution U˙=−J−1RDD˙\dot U=-J^{-1}R_D\dot D is nonzero along the whole duct: a local change of the design changes the whole flow, so every design component needs a complete linear solve.

第 11 节Section 11

伴随方程的求解:一次求解,全部设计变量

Adjoint solve: one solution for all design variables

采用离散 Lagrangian ℒ=Q+ψTR。令状态扰动系数为零,得到 JTψ=−QUTQ_U^T,然后 g=QDTQ_D^T+RDTR_D^Tψ。这个负号来自 ℒ 的约定,不能凭记忆同采用 Q−λR 的连续公式直接对照。附录 A 讨论连续参考;其内部对应还依赖变量、符号及残差尺度,不能把有限网格上的乘子直接等同于连续场。

Use the discrete Lagrangian ℒ=Q+ψTR. Cancelling the coefficient of the state tangent gives JTψ=−QUTQ_U^T and g=QDTQ_D^T+RDTR_D^Tψ. The minus sign follows from this convention. It cannot be compared blindly with a continuous Q−λR convention; Appendix A discusses the continuous reference; an interior correspondence also depends on variables, sign and residual scaling, not pointwise equality on a finite grid.

JTψ=−QUT,ra=−QUT−JTψ,ψ←ψ+0.7BTra,g=QDT+RDTψ.J^T\psi=-Q_U^T,\quad r_a=-Q_U^T-J^T\psi,\quad \psi\leftarrow\psi+0.7B^Tr_a,\quad g=Q_D^T+R_D^T\psi.

用第 9 节的记号:以 Rˉ=ψ\bar R=\psi、Qˉ=1\bar Q=1 为种子做一次反向传播,得到 Uˉ=JTψ+QUT\bar U=J^T\psi+Q_U^T,它正是伴随方程的真残差,解对时为零;同时得到 Dˉ=RDTψ+QDT=g\bar D=R_D^T\psi+Q_D^T=g,就是总梯度。

In the notation of Section 9, one reverse sweep with seeds Rˉ=ψ\bar R=\psi and Qˉ=1\bar Q=1 returns Uˉ=JTψ+QUT\bar U=J^T\psi+Q_U^T, the true residual of the adjoint equation, which vanishes at the solution, and Dˉ=RDTψ+QDT=g\bar D=R_D^T\psi+Q_D^T=g, the total gradient.

转置预处理从出口向入口逆序处理块,并转置各块及所有累加路径。每次迭代用精确 JT 重新计算真残差;只监控近似系统 PT 的残差不能证明已解对伴随方程。

The transposed preconditioner visits blocks from outlet to inlet and transposes blocks and accumulation paths. Every iteration recomputes the true residual using exact JT. A residual for PT alone cannot establish convergence of the adjoint equation.

从有限维 Lagrangian 逐步消去状态敏感度

Eliminate state sensitivity from a finite-dimensional Lagrangian

ℒ(𝐔,D,ψ)=Q(𝐔,D)+ψTR(𝐔,D).\mathcal L(\mathbf U,D,\psi)=Q(\mathbf U,D)+\psi^TR(\mathbf U,D).

在满足 R=0R=0 的解上 ℒ=Q\mathcal L=Q。先把 ψ\psi 固定,沿切向量 𝐔˙\dot{\mathbf U}、D˙\dot D 对 ℒ\mathcal L 前向求导,再按 𝐔˙\dot{\mathbf U} 和 D˙\dot D 收集系数。即使 ψ\psi 随设计改变,额外的 ψ˙TR\dot\psi^TR 项也在收敛解上为零。

At a feasible state R=0R=0, ℒ=Q\mathcal L=Q. Hold ψ\psi fixed, differentiate ℒ\mathcal L forward along the tangents 𝐔˙\dot{\mathbf U} and D˙\dot D, then collect the coefficients of 𝐔˙\dot{\mathbf U} and D˙\dot D. Even if ψ\psi varies with the design, the extra ψ˙TR\dot\psi^TR term vanishes at the converged state.

ℒ˙=QU𝐔˙+QDD˙+ψT(J𝐔˙+RDD˙)\dot{\mathcal L}=Q_U\dot{\mathbf U}+Q_D\dot D+\psi^T(J\dot{\mathbf U}+R_D\dot D)
=(QUT+JTψ)T𝐔˙+(QDT+RDTψ)TD˙.=(Q_U^T+J^T\psi)^T\dot{\mathbf U}+(Q_D^T+R_D^T\psi)^T\dot D.

选择 ψ\psi 使未知的状态敏感度前面的系数为零。于是一个目标只需求解一次伴随线性系统,再进行设计反传;总设计梯度是列向量 gg。注意符号来自这里的加号约定,并不是因为“伴随一定带负号”。

Choose ψ\psi to cancel the coefficient of the unknown state sensitivity. One objective then requires one adjoint solve followed by design reverse accumulation; the total design gradient gg is a column. Its sign follows from the plus convention in this Lagrangian, not from a universal negative sign for adjoints.

JTψ=−QUT,g=QDT+RDTψ,\boxed{J^T\psi=-Q_U^T},\qquad\boxed{g=Q_D^T+R_D^T\psi},
gk=∂Q∂Dk+∑rψr∂Rr∂Dk.g_k=\frac{\partial Q}{\partial D_k}+\sum_r\psi_r\frac{\partial R_r}{\partial D_k}.

用反向模式的横线记号:以 Rˉ=ψ\bar R=\psi 为种子运行残差的反向扫描,得到 𝐔ˉ=JTψ=−QUT\bar{\mathbf U}=J^T\psi=-Q_U^T,这就是伴随方程。再把目标自己的反向语句(种子 Qˉ=1\bar Q=1)累加进同一组横线量:它们给 𝐔ˉ\bar{\mathbf U} 加上 QUTQ_U^T,给 Dˉ\bar D 加上 QDTQ_D^T,于是 𝐔ˉ=QUT+JTψ=0\bar{\mathbf U}=Q_U^T+J^T\psi=0。伴随方程说的正是状态的横线量总和为零:状态不再把敏感度往下传,同一次扫描的设计伴随 Dˉ=QDT+RDTψ\bar D=Q_D^T+R_D^T\psi 就是梯度 gg。也可以直接检查与 tangent 的等价性:利用 Jv=−RD D˙Jv=-R_D\,\dot D,QUv=−ψTJv=ψTRD D˙Q_Uv=-\psi^TJv=\psi^TR_D\,\dot D,所以两条路线给出同一个 Q˙=DˉTD˙\dot Q=\bar D^T\dot D。更换为 ℒ=Q−λTR\mathcal L=Q-\lambda^TR 会得到 λ=−ψ\lambda=-\psi,但这只是两个离散乘子的符号约定,不能据此认定连续场 λ(x)\lambda(x) 与离散向量逐点相等。

In the bar notation of reverse mode: the residual's reverse sweep seeded with Rˉ=ψ\bar R=\psi returns 𝐔ˉ=JTψ=−QUT\bar{\mathbf U}=J^T\psi=-Q_U^T, which is the adjoint equation. Accumulate the objective's own reverse statements (seeded with Qˉ=1\bar Q=1) into the same bars: they add QUTQ_U^T to 𝐔ˉ\bar{\mathbf U} and QDTQ_D^T to Dˉ\bar D, so 𝐔ˉ=QUT+JTψ=0\bar{\mathbf U}=Q_U^T+J^T\psi=0. The adjoint equation says exactly that the state's total bar vanishes: the state passes no sensitivity on, and the design adjoint of the same sweep, Dˉ=QDT+RDTψ\bar D=Q_D^T+R_D^T\psi, is the gradient gg. Check equivalence with the tangent: Jv=−RD D˙Jv=-R_D\,\dot D gives QUv=−ψTJv=ψTRD D˙Q_Uv=-\psi^TJv=\psi^TR_D\,\dot D, so both routes give the same Q˙=DˉTD˙\dot Q=\bar D^T\dot D. Changing to ℒ=Q−λTR\mathcal L=Q-\lambda^TR yields λ=−ψ\lambda=-\psi, but that relates two discrete sign conventions; it does not prove pointwise equality with a continuous field λ(x)\lambda(x).

精确算子决定求解哪个方程;块 GS 固定点、GS 预处理 GMRES 与 LU(附录 C)决定如何求解。较便宜的近似 Jacobian(例如一阶或冻结部分系数的 Jacobian)可以用于迭代修正或预处理,但不能替代精确转置。应监控真残差 rψ=JTψ+QUTr_\psi=J^T\psi+Q_U^T。若其不为零,当前伴随与精确 tangent 的方向导数差为 −rψTv-r_\psi^Tv,因此只看迭代次数或预处理残差不够。

The exact operator determines the equation; the block-GS fixed point, GS-preconditioned GMRES and LU (Appendix C) determine how it is solved. A cheaper approximate Jacobian (first order, or with some coefficients frozen) may supply corrections or preconditioning, but cannot replace the exact transpose. Monitor the true residual rψ=JTψ+QUTr_\psi=J^T\psi+Q_U^T. If it is nonzero, the current adjoint directional derivative minus the exact tangent derivative is −rψTv-r_\psi^Tv; an iteration count or a preconditioned residual alone is insufficient.

为什么一次伴随求解能代替 nan_a 次前向求解

Why one adjoint solve replaces nan_a tangent solves

同样的结论也可以不经过 Lagrangian,直接由前向求解的两个关系得到:

The same result follows without a Lagrangian, directly from the two relations of the tangent solve:

Q˙=QU𝐔˙+QDD˙,𝐔˙=−J−1RDD˙\dot Q=Q_U\dot{\mathbf U}+Q_D\dot D,\qquad \dot{\mathbf U}=-J^{-1}R_D\dot D
⟹Q˙=QDD˙−QU J−1 RDD˙.\Longrightarrow\qquad \dot Q=Q_D\dot D-Q_U\,J^{-1}\,R_D\dot D.

最后一项是三个因子 QUQ_U、J−1J^{-1}、RDD˙R_D\dot D 的乘积,括号可以从两边加。前向路线先算 J−1(RDD˙)J^{-1}(R_D\dot D):它依赖方向 D˙\dot D,每个设计分量都要解一次以 JJ 为系数的方程,全部分量要解 nan_a 次。伴随路线先算行向量 QUJ−1Q_UJ^{-1}:它与 D˙\dot D 无关,只要解一次。把这个行向量记作 −ψT-\psi^T:

The last term is the product of three factors, QUQ_U, J−1J^{-1} and RDD˙R_D\dot D, which can be bracketed from either side. The tangent route computes J−1(RDD˙)J^{-1}(R_D\dot D) first: it depends on the direction D˙\dot D, so every design component needs one solve with JJ, nan_a solves for all of them. The adjoint route computes the row QUJ−1Q_UJ^{-1} first: it does not depend on D˙\dot D, so it is solved once. Call this row −ψT-\psi^T:

ψT=−QU J−1⟺JTψ=−QUT,\psi^T=-Q_U\,J^{-1}\quad\Longleftrightarrow\quad J^T\psi=-Q_U^T,
Q˙=(QDT+RDTψ)TD˙=DˉTD˙.\dot Q=\big(Q_D^T+R_D^T\psi\big)^T\dot D=\bar D^T\dot D.

这对每个方向 D˙\dot D 都成立,所以梯度就是上面方框里的 g=Dˉg=\bar D:一次伴随求解之后,再以 Rˉ=ψ\bar R=\psi 为种子做一次反向扫描,就得到全部分量。两条路线的计算量(本页的 nan_a 与 nun_u 由本页代码数出,与页首路线图相同):

This holds for every direction D˙\dot D, so the gradient is the boxed g=Dˉg=\bar D above: after one adjoint solve, one reverse sweep seeded with Rˉ=ψ\bar R=\psi gives every component. The cost of the two routes (nan_a and nun_u of this page, counted by its code, as in the page roadmap at the top):

前向(tangent)路线Tangent route伴随(adjoint)路线Adjoint route
每个目标的线性求解Linear solves per objectivena=16n_a=16 次,系数 JJna=16n_a=16, with JJ1 次,系数 JTJ^T1, with JTJ^T
右端Right-hand sides−RD ek,-R_D\,e_k, k=0,…,na−1k=0,\ldots,n_a-1−QUT-Q_U^T
求解之后After the solvesnan_a 次前向扫描,各得一个 Q˙\dot Qnan_a forward sweeps, one Q˙\dot Q each1 次反向扫描,得到整个 Dˉ\bar D1 reverse sweep, the whole Dˉ\bar D
一次求解给出One solve gives一个分量 dQ^/dDkd\widehat{Q}/dD_kone component dQ^/dDkd\widehat{Q}/dD_k全部 nan_a 个分量all nan_a components
nJn_J 个目标时With nJn_J objectives仍是 nan_a 次(vv 与目标无关)still nan_a (vv does not depend on the objective)nJn_J 次nJn_J

本页 na=16n_a=16、nu=39n_u=39:前向路线要解 16 个 39 阶的线性方程组,伴随路线只解 1 个。设计变量多而目标少时(形状优化通常如此)伴随路线便宜得多;设计变量少而目标多时,前向路线更合适。

This page has na=16n_a=16 and nu=39n_u=39: the tangent route solves 16 linear systems of order 39, the adjoint route one. With many design variables and few objectives (the usual case in shape optimisation) the adjoint route is far cheaper; with few design variables and many objectives the tangent route is the better choice.

伴随沿哪个方向传播

Which way the adjoint travels

原始问题的 uu、u+cu+c 两族向下游传播,u−cu-c 向上游;伴随沿同样的特征反向传播(附录 A),所以它从原始问题离开的地方进入:入口一族(c−uc-u),出口两族(−u-u、−(u+c)-(u+c))。图 9 用本页离散解的 u−cu-c、uu、u+cu+c 画出两者的特征线,并让扰动沿它们移动。这也解释了两种扫描的方向:第 6 节的原始 GS 从入口扫到出口,顺着 uu 与 u+cu+c;本节的转置预处理从出口扫到入口,顺着伴随的 −u-u 与 −(u+c)-(u+c)——一次前向扫描精确倒转下三角部分 diag⁡(Pii)+L\operatorname{diag}(P_{ii})+L,它的转置是上三角的,只能从最后一块倒着解,正好是一次反向扫描。逆着扫描方向传播的第三族(原始的 u−cu-c、伴随的 c−uc-u)要靠 12 次内扫描与外层迭代来处理。

The primal families uu and u+cu+c travel downstream and u−cu-c upstream; the adjoint travels along the same characteristics in the opposite direction (Appendix A), so it enters where the primal leaves: one family at the inlet (c−uc-u) and two at the outlet (−u-u, −(u+c)-(u+c)). Figure 9 draws the characteristics of both from this page’s discrete u−cu-c, uu and u+cu+c and lets disturbances move along them. This also explains the direction of the two sweeps: the primal GS of Section 6 sweeps from the inlet to the outlet, along uu and u+cu+c; the transposed preconditioner of this section sweeps from the outlet to the inlet, along the adjoint’s −u-u and −(u+c)-(u+c). A forward sweep inverts the lower part diag⁡(Pii)+L\operatorname{diag}(P_{ii})+L exactly; its transpose is upper triangular and can only be solved from the last block backwards, which is a reverse sweep. The third family, which runs against each sweep (the primal u−cu-c, the adjoint c−uc-u), is left to the 12 inner sweeps and the outer iteration.

图 9 · 原始问题(上)与伴随(下)的特征:横轴为 xx,纵轴为时间,速度取本页当前离散解的 u−cu-c、uu、u+cu+c(伴随取相反数)。细线是特征线,圆点是沿它们移动的扰动(播放、暂停、单步、调速);两端标出进入计算域的族数:原始问题入口 2、出口 1,伴随入口 1、出口 2。Figure 9 · The characteristics of the primal (top) and of the adjoint (bottom): xx across, time upwards, the speeds u−cu-c, uu, u+cu+c of this page’s current discrete solution (negated for the adjoint). Thin lines are characteristics, dots are disturbances moving along them (play, pause, step, speed); each end counts the families that enter the domain there: primal 2 at the inlet and 1 at the outlet, adjoint 1 and 2.

多次前向求解与一次伴随求解

Many tangent solves against one adjoint solve

图 10 在本页上度量上面代价表里的两条路线:每个设计分量一次前向求解 JU˙=−RDD˙J\dot U=-R_D\dot D(第 10 节),对比一次伴随求解 JTψ=−QUTJ^T\psi=-Q_U^T 加一次反向扫描。页首默认设置下,16 次前向求解共用 468 次 GMRES 迭代,伴随求解用 27 次(第 1 节)。每次迭代做两次精确算子乘积(一次用于 Krylov 步,一次重新计算真残差,第 17 节)和一次 GS 预处理(12 次块扫描),每个求解结束时再乘一次;按调用计数,前向路线共 952 次 JJ 乘向量,伴随求解 55 次 JTJ^T 乘向量。迭代数之比 17.3 与调用数之比 17.3 几乎相同,因为两条路线每次迭代做的事一样;每次迭代的代价只是近似相同,Krylov 正交化的代价随迭代序号增长,没有计入。两条路线的梯度相差至多 8.3×10−138.3\times10^{-13},来自两个线性求解的容差。设计分量越多,差距越大,如上面的表所计。

Figure 10 measures on this page the two routes of the cost table above: one tangent solve JU˙=−RDD˙J\dot U=-R_D\dot D per design component (Section 10) against one adjoint solve JTψ=−QUTJ^T\psi=-Q_U^T and one reverse sweep. At the defaults the 16 tangent solves take 468 GMRES iterations in total and the adjoint solve 27 (Section 1). Every iteration makes two exact operator products (one for the Krylov step, one to recompute the true residual, Section 17) and applies the GS preconditioner once (12 block sweeps), and every solve multiplies once more at the end; counted by calls, the tangent route makes 952 products with JJ and the adjoint solve 55 with JTJ^T. The ratio of the iteration counts, 17.3, and the ratio of the call counts, 17.3, are nearly the same because both routes do the same in an iteration; the cost of an iteration is only approximately the same, since the cost of the Krylov orthogonalisation grows with the iteration index and is not counted. The gradients of the two routes differ by at most 8.3×10−138.3\times10^{-13}, the tolerance of the two linear solves. The more design components, the larger the gap, as the table above counts.

图 10 · 两条路线的工作量(当前设置)。横轴为累计的迭代次数:GMRES 的每次迭代含两次精确算子乘积和一次 GS 预处理(12 次块扫描),选 GS 固定点时每次迭代含一次乘积和一次预处理;两条路线做的事相同,标题里的调用数是精确算子乘积的计数。上面是前向路线:每个设计分量一次求解,解完点亮梯度的一格;下面是伴随路线:一次求解加一次反向扫描后,全部格子同时点亮(播放、暂停、单步、调速)。下方的柱是两条路线得到的梯度。Figure 10 · The work of the two routes (current settings). Across: the cumulative number of iterations: a GMRES iteration makes two exact operator products and applies the GS preconditioner once (12 block sweeps), with the GS fixed point selected an iteration makes one product and one preconditioner application; the two routes do the same, and the call counts in the headings are counted exact operator products. Top: the tangent route, one solve per design component, each lighting one cell of the gradient; bottom: the adjoint route, one solve and one reverse sweep, after which all cells light at once (play, pause, step, speed). The bars below are the gradients of the two routes.
第 12 节Section 12

设计导数:面积、入口总量和出口压力

Design derivatives: area, inlet totals and exit pressure

设计向量包含 N+1 个面积值以及 pbp_b、p₀、T₀。面积变化同时影响面通量、源项中的 A′ 和反设计目标的积分权重。漏掉其中任意一项,状态导数点积测试仍可能通过,但全链路几何梯度会错。节点坐标固定,因此本页不包含移动节点导致的 h、重构权重或积分点导数。

The design vector contains N+1 area values plus pbp_b, p₀ and T₀. Area changes affect face flux, source A′ and inverse-objective integration weights. Omitting one may still pass a state dot-product test while corrupting the full geometry gradient. Node coordinates are fixed; derivatives of spacing, reconstruction weights or moving quadrature points are outside this example.

Q˙=gATA˙+gpbp˙b+gp0p˙0+gT0T˙0,A˙′=A˙k+1−A˙kh.\dot Q=g_A^T\dot A+g_{p_b}\dot p_b+g_{p_0}\dot p_0+g_{T_0}\dot T_0,\quad\dot A'=\frac{\dot A_{k+1}-\dot A_k}{h}.

选择一个设计变量,比较前向与伴随得到的同一个标量梯度。面积分量是对节点面积值的偏导,不是连续梯度密度;比较连续结果时,应沿同一光滑方向:把附录 A 的连续变分 δA(x)\delta A(x) 在面积节点 xkx_k 上取样作离散种子 A˙k=δA(xk)\dot A_k=\delta A(x_k),再比较 gATA˙g_A^T\dot A 与连续的 δQ\delta Q,而不能直接比较数组元素大小。

Select a design variable and compare tangent and adjoint evaluations of the same scalar gradient. Area entries differentiate nodal area values, not a continuous gradient density. Compare continuous and discrete results along the same smooth direction: sample the continuous variation δA(x)\delta A(x) of Appendix A at the area nodes xkx_k as the discrete seed A˙k=δA(xk)\dot A_k=\delta A(x_k), then compare gATA˙g_A^T\dot A with the continuous δQ\delta Q, rather than matching array magnitudes.

在 T₀>0 的精确离散根处,若实际状态 Jacobian 非奇异,则同一光滑局部分支上,两个压力目标(压力参考固定)对 T₀ 的导数均为零。取 λ>0,把 T₀ 换成 λT₀,同时取 ρ→ρ/λ、u→√λu、p 不变。格点未被替换的守恒残差分量分别乘以 1/√λ、1、√λ;入口总压和出口背压替换行保持不变,入口总温替换行乘以 λ。因此完整零残差解映为完整零残差解,压力及两个目标保持不变。数值表中的总温分量不为零,只是因为线性求解只算到容差(量级约 10⁻¹³ 或更小;稠密求解只剩舍入量级),也可受原始方程残差影响;第 13、14 节检查它时只看绝对误差。

At an exact discrete root with T₀>0 and a nonsingular actual state Jacobian, both pressure objectives, with the reference pressure field fixed, have zero T₀ derivative on the same smooth local branch. For λ>0, replace T₀ by λT₀ and take ρ→ρ/λ, u→√λu with p unchanged. The unreplaced node conservation residual components scale by 1/√λ, 1 and √λ. The inlet total-pressure and outlet back-pressure replacement rows are unchanged; the inlet total-temperature replacement row scales by λ. Thus a complete zero-residual solution maps to another complete zero-residual solution with unchanged pressures and objectives. That temperature entry in the numerical table is not zero only because the linear solve stops at its tolerance (about 10⁻¹³ or smaller; a dense solve leaves only rounding level), and it can also reflect the primal equation residual; use absolute error for this component in Sections 13 and 14.

面积梯度的三条路径

The three paths of an area gradient

每个截面积 AkA_k 在残差和目标里出现在三处:面面积 AfA_f(第 3 节)、源项斜率 GqG_q(第 5 节)和目标的积分项(第 7 节)。第 9 节的反向扫描沿这三条路径分别累加 Aˉk\bar A_k;图 11 把它们分开画出(页首默认设置)。反设计目标下,面路径与源项路径各自比它们的和大得多,而且几乎相互抵消:节点 k=12k=12 处面路径为 0.01660.0166、源项路径为 −0.0183-0.0183、目标路径为 2.2×10−82.2\times10^{-8},三者之和 gA12=−0.00179g_{A_{12}}=-0.00179;整个流道上面路径的最大值是梯度最大值的 2.1 倍,源项路径是 2.3 倍。漏掉面路径或源项路径,误差比梯度本身还大;漏掉目标路径的误差小得多(最大 3.5×10−53.5\times10^{-5},约为梯度最大值的 0.44%),但同样是错的。这就是上面“漏掉其中任意一项……几何梯度会错”的具体含义。推力目标下两端由目标路径主导,那里的梯度为 gA0=−0.878g_{A_0}=-0.878、gA12=0.892g_{A_{12}}=0.892。边界数据各只有一条路径:被替换行的乘子(第 4 节)。

Every area AkA_k enters the residual and the objective in three places: the face areas AfA_f (Section 3), the source slopes GqG_q (Section 5) and the objective integrals (Section 7). The reverse sweep of Section 9 accumulates Aˉk\bar A_k along each of the three paths; Figure 11 draws them separately (the defaults at the top of the page). For the inverse design the face path and the source path are each much larger than their sum and nearly cancel: at node k=12k=12 the face path is 0.01660.0166, the source path −0.0183-0.0183 and the objective path 2.2×10−82.2\times10^{-8}, which sum to gA12=−0.00179g_{A_{12}}=-0.00179; along the duct the largest face path is 2.1 times the largest gradient entry, and the largest source path 2.3 times. Omitting the face path or the source path makes an error larger than the gradient itself; omitting the objective path makes a much smaller error (at most 3.5×10−53.5\times10^{-5}, about 0.44% of the largest gradient entry), but still an error. This is what “omitting one … corrupting the full geometry gradient” above means in numbers. For the thrust the objective path dominates at the two ends, where the gradient is gA0=−0.878g_{A_0}=-0.878 and gA12=0.892g_{A_{12}}=0.892. The boundary data have one path each: the multipliers of the replaced rows (Section 4).

图 11 · 面积梯度的三条路径:面面积、源项斜率与目标−0.02−0.010.010.0200123456789101112节点 k压力反设计 QI−1−0.50.5100123456789101112节点 k壁面推力 QT面面积经由 Af源项斜率经由 Gq目标经由 Q三者之和 gAk​
图 11 · 面积梯度 gAkg_{A_k} 的三条路径(页首默认设置,N=12N=12):面面积(蓝)、源项斜率(琥珀)与目标(紫)三部分,黑点是三者之和。上:压力反设计;下:壁面推力。纵轴刻度按各自的数据选取。
Figure 11 · The three paths of the area gradient: face areas, source slopes and objective−0.02−0.010.010.0200123456789101112node kpressure inverse design QI−1−0.50.5100123456789101112node kwall thrust QTface areasthrough Afsource slopesthrough Gqobjectivethrough Qtheir sum gAk​
Figure 11 · The three paths of the area gradient gAkg_{A_k} (the defaults at the top of the page, N=12N=12): face areas (blue), source slopes (amber) and objective (violet); the black dot is their sum. Top: pressure inverse design; bottom: wall thrust. Each panel has its own scale.

网格加密:从节点导数到梯度密度

Refinement: from nodal derivatives to a gradient density

gAkg_{A_k} 是对一个节点值的导数,随网格加密而变小。除以节点 kk 的帽函数面积 hkh_k(内部为 hh,两端为 h/2h/2),得到的梯度密度在加密时趋于附录 A.4 的连续密度 d(x)=jA+λ′TF−(jA′+λ2p)′d(x)=j_A+\lambda'^TF-\bigl(j_{A'}+\lambda_2p\bigr)'(推力 jA′=pj_{A'}=p 时括号内是 (1+λ2)p(1+\lambda_2)p;离散与连续之间 λ≈−ψ\lambda\approx-\psi)。图 12 用 N=12, 24, 48, 96N=12,\ 24,\ 48,\ 96 的离线计算说明这一点:反设计的内部密度逐渐贴近 d(x)d(x),在喉部 x=0.5x=0.5 依次为 0.0080248, 0.024035, 0.028094, 0.0285510.0080248,\ 0.024035,\ 0.028094,\ 0.028551,连续值为 0.0285420.028542。N=96N=96 时它与连续值相差 0.032%;再加密到 N=192N=192 得 0.028560.02856,相差 0.062%:第四位以后数列不再单调地趋于连续值,这一列只支持前三位数字。两端附近的几个节点另外承担 A.4 的端点项,不形成密度。推力的连续密度恒为零(A.4),管道中段(0.25≤x≤0.750.25\le x\le0.75)的离散密度,其最大绝对值从 N=12N=12 的 0.470.47 降到 N=96N=96 的 0.00360.0036(两端附近的节点带着端点项,不算在内),而两端的导数趋于 −p(0)=−0.9-p(0)=-0.9 与 p(1)=0.9p(1)=0.9:N=96N=96 时为 −0.89974-0.89974 与 0.899990.89999。

gAkg_{A_k} is the derivative by one nodal value and shrinks as the mesh is refined. Divided by the area hkh_k of the hat function of node kk (hh inside, h/2h/2 at the two ends), it becomes a gradient density that tends under refinement to the continuous density of Appendix A.4, d(x)=jA+λ′TF−(jA′+λ2p)′d(x)=j_A+\lambda'^TF-\bigl(j_{A'}+\lambda_2p\bigr)' (for the thrust jA′=pj_{A'}=p, and the bracket is (1+λ2)p(1+\lambda_2)p; between discrete and continuous λ≈−ψ\lambda\approx-\psi). Figure 12 shows this with offline runs at N=12, 24, 48, 96N=12,\ 24,\ 48,\ 96: the interior density of the inverse design approaches d(x)d(x), at the throat x=0.5x=0.5 it takes the values 0.0080248, 0.024035, 0.028094, 0.0285510.0080248,\ 0.024035,\ 0.028094,\ 0.028551 against the continuous 0.0285420.028542. At N=96N=96 it differs from the continuous value by 0.032%; one grid finer, N=192N=192, it is 0.028560.02856, differing by 0.062%: beyond the third digit the sequence no longer approaches the continuous value monotonically, so this column supports the first three digits only. A few nodes near each end also carry the end terms of A.4 and do not form a density. The continuous density of the thrust is identically zero (A.4); on the middle of the duct (0.25≤x≤0.750.25\le x\le0.75) the largest absolute discrete density falls from 0.470.47 at N=12N=12 to 0.00360.0036 at N=96N=96 (the nodes near the ends carry the end terms and are not counted), and the two end derivatives tend to −p(0)=−0.9-p(0)=-0.9 and p(1)=0.9p(1)=0.9: −0.89974-0.89974 and 0.899990.89999 at N=96N=96.

图 12 · 网格加密下的面积梯度密度与连续密度−0.012500.01250.0250.03750.0500.250.50.751压力反设计:gAk​ / hk​ 与连续密度 d(x)−1−0.500.5100.250.50.751壁面推力:连续密度 d(x) ≡ 0连续 d(x)附录 A.4N = 12N = 24N = 48N = 96▲▼ 超出纵轴的节点(都在两端附近)
图 12 · 网格加密下的面积梯度密度 gAk/hkg_{A_k}/h_k(hkh_k 为节点 kk 的帽函数面积;离线计算 N=12, 24, 48, 96N=12,\ 24,\ 48,\ 96,二阶,默认流道)。上:压力反设计,实线是附录 A.4 的连续密度 d(x)d(x);超出纵轴范围的节点(都在两端附近)画成边框上的三角。下:壁面推力,连续密度恒为零。
Figure 12 · The area gradient density under refinement and the continuous density−0.012500.01250.0250.03750.0500.250.50.751pressure inverse design: gAk​ / hk​ and the continuous density d(x)−1−0.500.5100.250.50.751wall thrust: continuous density d(x) ≡ 0continuous d(x)Appendix A.4N = 12N = 24N = 48N = 96▲▼ nodes off scale(all near the ends)
Figure 12 · The area gradient density gAk/hkg_{A_k}/h_k under refinement (hkh_k is the area of the hat function of node kk; offline runs at N=12, 24, 48, 96N=12,\ 24,\ 48,\ 96, second order, the default duct). Top: pressure inverse design, with the continuous density d(x)d(x) of Appendix A.4 as the solid line; nodes off the vertical scale (all near the ends) are drawn as triangles on the frame. Bottom: wall thrust, whose continuous density is identically zero.
第 13 节Section 13

有限差分:改变变量、改变步长、重新收敛

Finite differences: change variables and steps, reconverge

差分验证必须重新求解扰动后的完整流场。中心差分误差含 O(h²) 截断误差与约 O(ε/h) 的舍入及求解误差:步长越小不一定越准确。下面保留全部面积和边界变量、前向/中心/后向差分,以及从 10⁻¹ 到 10⁻¹² 的扫描。若扰动离开当前亚声速分支,显示失败而不伪造数值。

Each finite-difference sample reconverges the full perturbed flow. Central differences combine O(h²) truncation with approximately O(ε/h) roundoff and solver error; smaller steps are not always better. Select any area or boundary variable, forward/central/backward differences, and scan steps from 10⁻¹ to 10⁻¹². Perturbations leaving the subsonic branch are reported as failures.

gkCD(h)=Q(U(D+hek),D+hek)−Q(U(D−hek),D−hek)2h.g_k^{\mathrm{CD}}(h)=\frac{Q(U(D+h e_k),D+h e_k)-Q(U(D-h e_k),D-h e_k)}{2h}.
target = fixed_target_from_baseline()
for h in steps:
    Uplus  = reconverge(D+h*ek, start=Ustar)
    Uminus = reconverge(D-h*ek, start=Ustar)
    fd = (Q(Uplus,D+h*ek)-Q(Uminus,D-h*ek))/(2*h)
    report(fd, abs(fd-adjoint_gradient[k]), true_primal_residuals)

四种误差要分开:空间离散误差由网格加密检查;非线性求解误差由原始残差检查;线性求解误差由 tangent/adjoint 真残差检查;求导实现由转置、差分与复数检查。点积相等只证明两条传播互为转置;共同遗漏同一项也可能通过。近零梯度分量请同时查看绝对误差。

Separate four errors: spatial error through refinement; nonlinear solve error through primal residuals; linear solve error through true tangent/adjoint residuals; derivative implementation through transpose, finite differences and complex checks. A dot test alone only establishes paired transposes; jointly omitting a term may still pass. Inspect absolute errors for near-zero gradient components.

Taylor 实验使用固定设计方向(种子)Ḋ,每个步长重新收敛原始问题,并保持目标场不变。E₀=|Q(D+hḊ)−Q(D)|,E₁=|Q(D+hḊ)−Q(D)−h gTḊ|。光滑且 gTḊ≠0 时,预期 E₀ 为一阶、E₁ 为二阶;驻点或特殊方向可能有更高阶。步长过小后,求解误差与浮点消减会破坏这些斜率,不能把这一段当作空间精度。

The Taylor experiment reconverges each perturbed primal along a fixed design direction (seed) Ḋ with a fixed target. E₀=|Q(D+hḊ)−Q(D)| and E₁=|Q(D+hḊ)−Q(D)−h gTḊ|. For a smooth problem and nonzero gTḊ, expect first- and second-order behaviour; stationary points or special directions can give higher orders. At tiny steps, solve errors and cancellation spoil the slopes. These slopes are not spatial accuracy.

容差实验固定当前设计、目标与差分步长,分别放宽扰动原始问题的容差、伴随线性系统的容差。表中差分误差与梯度误差以严格收敛的当前梯度为参照;残差是代数求解指标,不是空间误差。

The tolerance experiment fixes design, target and finite-difference step, then independently relaxes perturbed-primal and adjoint linear tolerances. Differences are measured against the tightly converged current gradient; algebraic residuals are not spatial errors.

第 14 节Section 14

复数步长:独立复数残差验证

Complex step: an independent complex-residual check

把同一原始代数运算换为复数运算,令 DkD_k 增加 ih,h=10⁻²⁵;不做两个近似数的相减。程序从已收敛实部出发,用精确实部 Jacobian 校正复数残差的虚部,最后检查 Im(R)/h 的范数。输出来自复数目标函数,不是直接返回前向导数。

Execute the primal algebra with independent complex arithmetic and perturb DkD_k by ih, h=10⁻²⁵. Starting from the converged real state, correct the imaginary residual using the real exact Jacobian, then check the norm of Im(R)/h. The result comes from the complex objective, not from returning a tangent derivative.

gkCS=Im⁡Q(U(D+ihek),D+ihek)h+O(h2).g_k^{\mathrm{CS}}=\frac{\operatorname{Im}Q(U(D+ih e_k),D+ih e_k)}h+O(h^2).

光滑分支的开方、幂和边界选择都保持解析延拓。状态有效性判断只决定是否接受实部基准,不能在复数残差内对虚部取绝对值。激波限制器中的 min/max 分支必须另行分析,附录 B 解释其局限。

Square roots, powers and boundary choices follow analytic continuation on the smooth branch. Admissibility concerns the real baseline, not absolute values of imaginary quantities inside the residual. Limiter min/max branches in shock problems require a separate analysis, discussed in Appendix B.

第 15 节Section 15

精度阶:同一物理问题上的网格加密

Accuracy order: refinement of the same physical problem

本节实验:线性重构精确、PDE 解达到二阶、目标或梯度达到二阶,是三种不同的结论。本节检查重构和加密解,第 16 节比较梯度的加密。

Experiment: Exact linear reconstruction, second-order PDE solutions and second-order functionals or gradients are distinct claims. This section checks reconstruction and refined solutions; Section 16 compares gradients under refinement.

精确参考不是把细网格离散解当作真解,而是独立求解等熵面积—马赫数关系。格心比较精确单元平均值,格点比较精确节点值。面积仍由越来越密的节点作分片线性逼近;对数图和实测相邻阶数显示真实误差,不把“二阶重构”直接当作二阶收敛证据。

The reference independently solves the isentropic area–Mach relation rather than treating a fine discrete solution as exact. Compare cell averages on the cell page and nodal values on the node page. Piecewise-linear area is refined with the grid. Log plots and measured adjacent-grid rates report observed error rather than equating reconstruction order with demonstrated convergence.

qm=ρuA=AγM(1+0.2M2)−3,Me=5(pb−2/7−1),EN=13m∑i,k(Uik−Uikref)2,q=log⁡2(EN/E2N).q_m=\rho uA=A\sqrt\gamma M(1+0.2M^2)^{-3},\quad M_e=\sqrt{5(p_b^{-2/7}-1)},\quad E_N=\sqrt{\frac1{3m}\sum_{i,k}(U_{ik}-U_{ik}^{ref})^2},\quad q=\log_2(E_N/E_{2N}).

上式中 qmq_m 是质量流量;各式按默认的 p₀=T₀=1、R=1 写出,第 15、16 节的加密表都用这组参数。

Here qmq_m is the mass flow; the formulas are written for the default p₀=T₀=1 and R=1, which the refinement tables of Sections 15 and 16 use.

第 16 节Section 16

伴随一致性:离散正确与连续极限分开检查

Adjoint consistency: discrete correctness and continuum limits

点积、前向/伴随、差分及复数步长检查的是当前离散程序。连续伴随则从微分方程独立推导与求解,用来观察网格加密后的极限。两者不是互相替代的验证。完整推导、边界条件、独立 ODE 求解和图形位于本页附录 A。

Dot products, tangent/adjoint agreement, finite differences and complex step test the discrete program. The continuous adjoint is independently derived and solved from the differential equation to study the refinement limit. Neither replaces the other. Appendix A contains the complete derivation, boundary conditions, independent ODE solution and plots.

光滑、正密度正压力、远离声速点以及可逆的两端边界算子,是本页连续参考的前提。在声速点,通量 Jacobian 有零特征值;在激波处,平滑 ODE 和普通分部积分不再覆盖全部变化。出现不一致时先区分离散实现错误、网格未收敛与连续问题不适定。

The reference assumes smooth positive states, separation from sonic points and an invertible two-end boundary operator. A sonic point makes the flux Jacobian singular; shocks add variations absent from a smooth ODE. Disagreement must be classified as an implementation defect, unresolved mesh error or ill-posed continuous problem.

下方加密图是固定压力反设计基准的离线数据;顶部目标切换不会改写这组实验。附录 A 按当前目标独立积分连续伴随。两处实验必须按各自的目标、几何和参考数据解释。

The refinement plot below contains offline data for the fixed pressure-inverse-design benchmark. Changing the top objective does not change that experiment. Appendix A independently integrates the continuous adjoint for the current objective. Interpret each comparison with its own objective, geometry and reference.

离散乘子与连续场为什么不能直接混用

Why a discrete multiplier is not automatically a continuous field

正文使用普通数组点积定义 JTJ^T。若另选带权内积,算子的伴随就会改变:状态权矩阵为 WUW_U、残差权矩阵为 WRW_R 时,带权伴随是 WU−1JTWRW_U^{-1}J^TW_R。代码的普通转置不能在没有换元说明的情况下被写成该带权算子。

The main derivation defines JTJ^T using ordinary array dot products. A weighted inner product changes the adjoint: with state and residual weights WUW_U and WRW_R it is WU−1JTWRW_U^{-1}J^TW_R. The coded ordinary transpose cannot be relabelled as that weighted operator without a change of variables.

⟨Jv,Rˉ⟩WR=vTJTWRRˉ=⟨v,J†Rˉ⟩WU,\langle Jv,\bar R\rangle_{W_R}=v^TJ^TW_R\bar R=\langle v,J^\dagger \bar R\rangle_{W_U},
J†=WU−1JTWR.J^\dagger=W_U^{-1}J^TW_R.

残差的归一化也会改变乘子的数值。若改为 R𝒩=𝒩−1RR_{\mathcal N}={\mathcal N}^{-1}R,𝒩{\mathcal N} 为可逆的行尺度矩阵(例如各控制体的体积),则在收敛解上 J𝒩=𝒩−1JJ_{\mathcal N}={\mathcal N}^{-1}J,ψ𝒩=𝒩Tψ\psi_{\mathcal N}={\mathcal N}^T\psi;目标总梯度保持不变。即使 𝒩{\mathcal N} 随设计改变,由 𝒩˙\dot {\mathcal N} 产生的项也乘以 RR,在精确收敛时为零。把控制体积分残差改成体积平均残差后,不能仍直接比较原来的乘子曲线。

Residual normalization also changes multiplier values. For R𝒩=𝒩−1RR_{\mathcal N}={\mathcal N}^{-1}R with an invertible row-scaling matrix 𝒩{\mathcal N} (for example the control-volume sizes), at convergence J𝒩=𝒩−1JJ_{\mathcal N}={\mathcal N}^{-1}J and ψ𝒩=𝒩Tψ\psi_{\mathcal N}={\mathcal N}^T\psi, while the total objective gradient is unchanged. Even when 𝒩{\mathcal N} depends on the design, the terms from 𝒩˙\dot {\mathcal N} multiply RR and vanish at exact convergence. Multipliers of integrated and volume-normalized residuals cannot be compared without rescaling.

连续附录从微分方程、积分目标和分部积分出发;正文从实际离散残差及其有限维链式法则出发。两者可以比较同一设计变量的目标总导数与加密极限,但还须匹配符号、变量、积分权重和边界约束。被替换的边界行的乘子尤其不能直接解释为连续场采样值。

The continuous appendix starts from differential equations, integral objectives and integration by parts; the main text starts from actual discrete residuals and finite-dimensional chain rules. Compare total derivatives of the same objective and design variable and their refinement limits, after matching signs, variables, weights and boundary constraints. Multipliers of replaced boundary rows especially are not direct samples of a continuous field.

第 17 节Section 17

传统固定点与 GS 预处理 GMRES

Fixed-point iteration and GS-preconditioned GMRES

两种方法解同一个精确系统 Jx=b。传统缺陷校正每步应用 B 后按固定系数更新,收敛要求谱半径 ρ(I−ωBJ)<1;这个条件不是“P 很像 J”就自动满足的。GMRES 则在 Krylov 子空间中选组合系数,减少真残差。这里使用右预处理:解 JB y=b,再取 x=B y。

Both methods solve the same exact Jx=b. Fixed-point defect correction applies B with a prescribed relaxation; convergence requires ρ(I−ωBJ)<1, which is not guaranteed merely because P resembles J. GMRES chooses combinations in a Krylov subspace to reduce the residual. Right preconditioning solves JB y=b and recovers x=B y.

𝒦k(JB,r0)=span⁡{r0,JBr0,…,(JB)k−1r0},yk=arg⁡min⁡y∈𝒦k∥b−JBy∥2.\mathcal K_k(JB,r_0)=\operatorname{span}\{r_0,JBr_0,\ldots,(JB)^{k-1}r_0\},\qquad y_k=\arg\min_{y\in\mathcal K_k}\|b-JBy\|_2.

本页 GMRES 采用两次正交化,每一步都用精确算子重新计算真残差,返回前再算一次。一次 GMRES 步含两次精确算子乘积(一次用于 Krylov 步,一次是真残差)和一次 GS 预处理(12 次内扫描),不能仅凭迭代次数宣布性能优势。下面在同一收敛流场上独立求解伴随系统,比较解差与真残差历史;固定点的转置预处理同样经过点积检查。

GMRES uses two orthogonalisation passes, recomputes the true residual with the exact operator at every step and once more before returning. Each Krylov step includes two exact operator products (one for the Krylov step, one for the true residual) and one application of the GS preconditioner (12 inner sweeps), so iteration count alone is not a timing comparison. The panel solves the same adjoint system independently with both methods and compares solutions and true residual histories. The transposed preconditioner also has a dot-product test.

第 18 节Section 18

总结:从重构到经过验证的设计梯度

Summary: from reconstruction to verified design gradients

Euler 的新增内容不是把 Burgers 的标量换成长度为三的数组就结束了:热力学使三个分量耦合,特征边界限制允许的扰动,面积同时进入面和体积分。二阶重构还要求源项、目标函数、前向与反向路径一起升级。

Euler requires more than replacing a Burgers scalar by a three-entry array: thermodynamics couples components, characteristic boundaries constrain perturbations, and area enters both face and volume terms. Second-order reconstruction also requires consistent source, objective, tangent and reverse paths.

本页实现光滑亚声速的一阶/二阶离散、matrix-free 精确导数、低阶 GS 固定点和 GS 预处理 GMRES、全变量差分与复数验证,并用独立连续伴随作对比。附录 B 进一步分析激波跳跃与位置变化,提供局部跳跃计算,并推出激波处的内部伴随条件;B.5 用整条流道的激波拟合给出约化伴随,B.6 给出激波捕捉格式的离散 tangent 与 adjoint 及其加密研究。这些都是独立实验;当前稳态流道求解器仍限于光滑分支,尚未把限制器或激波移动混入已验证的光滑实现。

This page implements first/second-order smooth subsonic discretisations, exact matrix-free derivatives, low-order GS defect correction, GS-preconditioned GMRES, all-variable finite differences and complex checks, with an independent continuous reference. Appendix B analyses shock jumps and location variations, provides a local jump calculator and derives the internal adjoint conditions at the shock; B.5 gives a reduced adjoint by fitting the shock over the whole duct, and B.6 the discrete tangent and adjoint of a shock-capturing scheme with a refinement study. These are independent experiments; the steady duct solver remains on the smooth branch, and limiter and moving-shock solves are not part of that verified implementation.

展开完整离散数值实现Full discrete numerical implementation
(function(root){'use strict';
const gamma=1.4,alpha=2,zeros=n=>Array(n).fill(0),dot=(a,b)=>a.reduce((s,x,i)=>s+x*b[i],0),norm=a=>Math.sqrt(dot(a,a));
const real={c:x=>x,add:(a,b)=>a+b,sub:(a,b)=>a-b,mul:(a,b)=>a*b,div:(a,b)=>a/b,pow:(a,p)=>a**p,sin:Math.sin,cos:Math.cos};
function tape(){const val=[],pa=[],pb=[],da=[],db=[],inputs=[];function node(v,a=-1,b=-1,x=0,y=0){const i=val.length;val.push(v);pa.push(a);pb.push(b);da.push(x);db.push(y);return i;}const op={c:x=>node(x),input:x=>{const i=node(x);inputs.push(i);return i;},add:(a,b)=>node(val[a]+val[b],a,b,1,1),sub:(a,b)=>node(val[a]-val[b],a,b,1,-1),mul:(a,b)=>node(val[a]*val[b],a,b,val[b],val[a]),div:(a,b)=>node(val[a]/val[b],a,b,1/val[b],-val[a]/val[b]**2),pow:(a,p)=>node(val[a]**p,a,-1,p*val[a]**(p-1)),sin:a=>node(Math.sin(val[a]),a,-1,Math.cos(val[a])),cos:a=>node(Math.cos(val[a]),a,-1,-Math.sin(val[a]))};
function forward(seed){const d=new Float64Array(val.length);inputs.forEach((i,k)=>d[i]=seed[k]||0);for(let i=0;i<val.length;i++){if(pa[i]>=0)d[i]+=da[i]*d[pa[i]];if(pb[i]>=0)d[i]+=db[i]*d[pb[i]];}return d;}
function reverse(outputs,seeds){const w=new Float64Array(val.length);outputs.forEach((i,k)=>w[i]+=seeds[k]);for(let i=val.length-1;i>=0;i--){if(pa[i]>=0)w[pa[i]]+=da[i]*w[i];if(pb[i]>=0)w[pb[i]]+=db[i]*w[i];}return inputs.map(i=>w[i]);}
return {op,val,forward,reverse};}
const complex={c:x=>[x,0],add:(a,b)=>[a[0]+b[0],a[1]+b[1]],sub:(a,b)=>[a[0]-b[0],a[1]-b[1]],mul:(a,b)=>[a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]],div:(a,b)=>{const d=b[0]**2+b[1]**2;return [(a[0]*b[0]+a[1]*b[1])/d,(a[1]*b[0]-a[0]*b[1])/d];},pow:(a,p)=>{const r=Math.hypot(...a)**p,t=p*Math.atan2(a[1],a[0]);return [r*Math.cos(t),r*Math.sin(t)];},sin:a=>[Math.sin(a[0])*Math.cosh(a[1]),Math.cos(a[0])*Math.sinh(a[1])],cos:a=>[Math.cos(a[0])*Math.cosh(a[1]),-Math.sin(a[0])*Math.sinh(a[1])]};
function gmres(A,b,P=x=>x.slice(),tol=1e-11,max=b.length){const n=b.length,beta=norm(b);if(beta<=tol)return {x:zeros(n),residual:beta,history:[beta],states:[zeros(n)],converged:true};const V=[b.map(x=>x/beta)],Z=[],H=Array.from({length:max+1},()=>zeros(max)),cs=zeros(max),sn=zeros(max),g=zeros(max+1),history=[beta],estimates=[beta],states=[zeros(n)];g[0]=beta;let m=0;
 for(let j=0;j<max;j++){Z.push(P(V[j]));let w=A(Z[j]);for(let pass=0;pass<2;pass++)for(let i=0;i<=j;i++){const h=dot(V[i],w);H[i][j]+=h;w=w.map((x,k)=>x-h*V[i][k]);}H[j+1][j]=norm(w);const nextnorm=H[j+1][j];if(nextnorm>1e-16)V.push(w.map(x=>x/nextnorm));for(let i=0;i<j;i++){const a=H[i][j],b=H[i+1][j];H[i][j]=cs[i]*a+sn[i]*b;H[i+1][j]=-sn[i]*a+cs[i]*b;}const a=H[j][j],bb=H[j+1][j],r=Math.hypot(a,bb);cs[j]=r?a/r:1;sn[j]=r?bb/r:0;H[j][j]=r;H[j+1][j]=0;g[j+1]=-sn[j]*g[j];g[j]*=cs[j];m=j+1;estimates.push(Math.abs(g[j+1]));const yy=zeros(m);for(let ii=m-1;ii>=0;ii--){let a=g[ii];for(let kk=ii+1;kk<m;kk++)a-=H[ii][kk]*yy[kk];yy[ii]=a/H[ii][ii];}const xx=zeros(n);for(let ii=0;ii<m;ii++)for(let kk=0;kk<n;kk++)xx[kk]+=Z[ii][kk]*yy[ii];states.push(xx);history.push(norm(A(xx).map((v,i)=>v-b[i])));if(history.at(-1)<tol||nextnorm<=1e-16)break;}
 const y=zeros(m);for(let i=m-1;i>=0;i--){let a=g[i];for(let j=i+1;j<m;j++)a-=H[i][j]*y[j];y[i]=a/H[i][i];}const x=zeros(n);for(let i=0;i<m;i++)for(let j=0;j<n;j++)x[j]+=Z[i][j]*y[i];const residual=norm(A(x).map((z,i)=>z-b[i]));return {x,residual,history,estimates,states,converged:residual<=Math.max(tol*5,1e-13)};}
const api={real,complex,tape,gmres,dot,norm};if(typeof module!=='undefined')module.exports=api;else root.QMath=api;
})(typeof window!=='undefined'?window:globalThis);

/* Smooth quasi-1D Euler with exact reconstruction, volume sources and boundary rows. */
(function(root){'use strict';const Q=typeof module!=='undefined'?require('./math'):root.QMath,gamma=1.4,zeros=n=>Array(n).fill(0),{real,complex,tape,gmres,dot,norm}=Q;
function solve3(A,b){const a=A.map((r,i)=>[...r,b[i]]);for(let k=0;k<3;k++){let p=k;for(let i=k+1;i<3;i++)if(Math.abs(a[i][k])>Math.abs(a[p][k]))p=i;if(Math.abs(a[p][k])<1e-15)throw Error('Singular 3x3 block');[a[p],a[k]]=[a[k],a[p]];for(let i=k+1;i<3;i++){const s=a[i][k]/a[k][k];for(let j=k;j<4;j++)a[i][j]-=s*a[k][j];}}const x=zeros(3);for(let i=2;i>=0;i--){let s=a[i][3];for(let j=i+1;j<3;j++)s-=a[i][j]*x[j];x[i]=s/a[i][i];}return x;}
function model({node=false,n=12,order=1,solver='gmres',objective='thrust',pb=.9,p0=1,T0=1,omega=.7,depth=.3}={}){
 const m=node?n+1:n,nd=3*m,h=1/n,x=Array.from({length:m},(_,i)=>(i+(node?0:.5))*h),ax=Array.from({length:n+1},(_,i)=>i*h),baseA=ax.map(x=>1-depth*Math.sin(Math.PI*x)**2),params=[...baseA,pb,p0,T0],faceX=node?[0,...Array.from({length:n},(_,i)=>(i+.5)*h),1]:ax.slice(),faces=faceX.map((xf,i)=>({x:xf,L:i-1,R:i===m?-1:i})),constraints=node?[0,1,3*(m-1)]:[];let targetU;
 function weights(i,xx,q=order){const d=(xx-x[i])/h;if(q===1)return [[i,1]];if(i===0)return [[0,1-d],[1,d]];if(i===m-1)return [[i,1+d],[i-1,-d]];return [[i,1],[i-1,-.5*d],[i+1,.5*d]];}
 function physics(o){const {c,add,sub,mul,div,pow}=o;
 const prim=U=>{const rho=U[0],u=div(U[1],rho),p=mul(c(.4),sub(U[2],mul(c(.5),mul(U[1],u)))),sound=pow(div(mul(c(gamma),p),rho),.5);return {rho,u,p,a:sound,T:div(p,rho)};};
 const cons=(r,u,p)=>[r,mul(r,u),add(div(p,c(.4)),mul(mul(c(.5),r),mul(u,u)))];
 const flux=U=>{const q=prim(U);return [U[1],add(mul(U[1],q.u),q.p),mul(add(U[2],q.p),q.u)];};
 const totals=U=>{const q=prim(U),factor=add(c(1),mul(c(.2),div(mul(q.u,q.u),mul(q.a,q.a))));return {p0:mul(q.p,pow(factor,3.5)),T0:mul(q.T,factor)};};
 function inlet(U,pt,tt){const q=prim(U),rm=sub(q.u,mul(c(5),q.a)),a1=mul(c(5),rm),a0=sub(mul(c(.5),mul(rm,rm)),mul(c(3.5),tt)),sound=div(add(mul(c(-1),a1),pow(sub(mul(a1,a1),mul(c(60),a0)),.5)),c(30)),u=add(rm,mul(c(5),sound)),M=div(u,sound),p=mul(pt,pow(add(c(1),mul(c(.2),mul(M,M))),-3.5)),rho=div(mul(c(gamma),p),mul(sound,sound));return cons(rho,u,p);}
 function outlet(U,back){const q=prim(U),entropy=div(q.p,pow(q.rho,gamma)),rho=pow(div(back,entropy),1/gamma),sound=pow(div(mul(c(gamma),back),rho),.5),u=sub(add(q.u,mul(c(5),q.a)),mul(c(5),sound));return cons(rho,u,back);}
 function numerical(L,R,A){const l=prim(L),r=prim(R),lam=mul(c(.5),add(add(l.u,l.a),add(r.u,r.a))),fl=flux(L),fr=flux(R);return {H:fl.map((v,k)=>mul(A,sub(mul(c(.5),add(v,fr[k])),mul(mul(c(.5),lam),sub(R[k],L[k]))))),lam};}
 return {prim,cons,flux,totals,inlet,outlet,numerical};}
 function evaluate(o,U,D,q=order,withObjective=true){const {c,add,sub,mul}=o,ph=physics(o),A=D.slice(0,n+1),back=D[n+1],pt=D[n+2],tt=D[n+3],R=Array.from({length:nd},()=>c(0)),local=[],sources=[],quadrature=[];
 const reconstruct=(V,i,xx)=>[0,1,2].map(k=>weights(i,xx,q).reduce((s,[j,w])=>add(s,mul(c(w),V[3*j+k])),c(0))),area=xx=>{const k=Math.min(n-1,Math.floor(xx/h)),t=(xx-ax[k])/h;return add(mul(c(1-t),A[k]),mul(c(t),A[k+1]));};
 for(const f of faces){let L=f.L>=0?reconstruct(U,f.L,f.x):null,Rr=f.R>=0?reconstruct(U,f.R,f.x):null;const af=area(f.x);let z;if(node&&(f.L<0||f.R<0)){const state=f.L<0?Rr:L;z={H:ph.flux(state).map(v=>mul(af,v)),lam:c(0)};L=state;Rr=state;}else{if(f.L<0)L=ph.inlet(Rr,pt,tt);if(f.R<0)Rr=ph.outlet(L,back);z=ph.numerical(L,Rr,af);}z.H.forEach((v,k)=>{if(f.L>=0)R[3*f.L+k]=add(R[3*f.L+k],v);if(f.R>=0)R[3*f.R+k]=sub(R[3*f.R+k],v);});local.push({...z,L,R:Rr,A:af});}
 let J=c(0);const target=targetU?targetU.map(c):null;
 for(let i=0;i<m;i++){let source=c(0),inv=c(0);const lo=faceX[i],hi=faceX[i+1],breaks=node&&i>0&&i<m-1?[lo,x[i],hi]:[lo,hi],qs=[];
 for(let k=1;k<breaks.length;k++){const left=breaks[k-1],right=breaks[k],mid=(left+right)/2,half=(right-left)/2,ai=Math.min(n-1,Math.floor(mid/h)),dA=mul(c(1/h),sub(A[ai+1],A[ai]));for(const z of [-1/Math.sqrt(3),1/Math.sqrt(3)]){const xx=mid+half*z,u=reconstruct(U,i,xx),p=ph.prim(u).p,af=area(xx),sc=mul(c(half),mul(p,dA));source=add(source,sc);let pref=null,objectiveContribution=objective==='thrust'?sc:c(0);if(target&&withObjective){pref=ph.prim(reconstruct(target,i,xx)).p;const delta=sub(p,pref);const term=mul(c(.5*half),mul(af,mul(delta,delta)));inv=add(inv,term);if(objective==='inverse')objectiveContribution=term;}qs.push({x:xx,w:half,p,A:af,dA,contribution:sc,pref,objectiveContribution});}}
 R[3*i+1]=sub(R[3*i+1],source);sources.push(source);quadrature.push(qs);if(withObjective)J=add(J,objective==='thrust'?source:inv);}
 const raw=R.slice();if(node){const q0=ph.totals(U.slice(0,3)),qe=ph.prim(U.slice(-3));R[0]=sub(q0.p0,pt);R[1]=sub(q0.T0,tt);R[3*(m-1)]=sub(qe.p,back);}return {R,J,raw,local,sources,quadrature};}
 function linearize(U,D=params,q=order,withObjective=true){const t=tape(),u=U.map(t.op.input),p=D.map(t.op.input),out=evaluate(t.op,u,p,q,withObjective),rv=ids=>ids.map(i=>t.val[i]);return {R:rv(out.R),J:t.val[out.J],jvp:(v,dp=zeros(D.length))=>{const d=t.forward([...v,...dp]);return {R:out.R.map(i=>d[i]),raw:out.raw.map(i=>d[i]),J:d[out.J],faces:out.local.map(f=>f.H.map(i=>d[i])),local:out.local.map(f=>({L:f.L.map(i=>d[i]),R:f.R.map(i=>d[i]),A:d[f.A],lam:d[f.lam],H:f.H.map(i=>d[i])})),sources:out.sources.map(i=>d[i])};},vjp:(w,bJ=0)=>{const v=t.reverse([...out.R,out.J],[...w,bJ]);return {U:v.slice(0,nd),p:v.slice(nd)};},faceVjp:(i,w)=>{const z=t.reverse(out.local[i].H,w);return {U:z.slice(0,nd),p:z.slice(nd)};},sourceVjp:(i,w)=>{const z=t.reverse([out.sources[i]],[w]);return {U:z.slice(0,nd),p:z.slice(nd)};}};}
 function firstOrderMatrix(U,D=params){const L=linearize(U,D,1,false),rows=Array.from({length:nd},()=>zeros(nd));for(let j=0;j<nd;j++){const e=zeros(nd);e[j]=1;const v=L.jvp(e).R;v.forEach((x,i)=>rows[i][j]=x);}return rows;}
 function sweep(P,rhs,transpose=false){const out=zeros(nd),get=(i,j)=>transpose?P[j][i]:P[i][j];for(let it=0;it<m;it++){const i=transpose?m-1-it:it,b=rhs.slice(3*i,3*i+3),block=Array.from({length:3},(_,k)=>[0,1,2].map(l=>get(3*i+k,3*i+l)));for(let j=transpose?i+1:0;transpose?j<m:j<i;j++)for(let k=0;k<3;k++)for(let l=0;l<3;l++)b[k]-=get(3*i+k,3*j+l)*out[3*j+l];const v=solve3(block,b);v.forEach((x,k)=>out[3*i+k]=x);}return out;}
 function GS(P,rhs,transpose=false){let v=zeros(nd);for(let it=0;it<12;it++){const r=rhs.map((x,i)=>x-P.reduce((s,row,j)=>s+(transpose?row[i]:P[i][j])*v[j],0)),d=sweep(P,r,transpose);v=v.map((x,i)=>x+d[i]);}return v;}
 function valid(U,D=params){const out=evaluate(real,U,D,order,false),states=Array.from({length:m},(_,i)=>U.slice(3*i,3*i+3)).concat(out.local.flatMap(f=>[f.L,f.R]));return D.slice(0,n+1).every(x=>x>.2)&&states.every(U=>{const q=physics(real).prim(U);return q.rho>.05&&q.p>.05&&q.u>0&&q.u/q.a<.95;});}
 function initial(D=params){const M=.3,T=D[n+3]/(1+.2*M*M),p=D[n+2]*(T/D[n+3])**3.5;return Array.from({length:m},()=>physics(real).cons(p/T,M*Math.sqrt(gamma*T),p)).flat();}
 function linearSolve(L,rhs,P,trans=false,method=solver,tol=2e-12){const op=v=>trans?L.vjp(v).U:L.jvp(v).R,pre=v=>GS(P,v,trans);if(method==='gmres')return gmres(op,rhs,pre,tol,nd);let v=zeros(nd),history=[],states=[];for(let it=0;it<12000;it++){const av=op(v),r=rhs.map((x,i)=>x-av[i]),rn=norm(r);history.push(rn);states.push(v.slice());if(rn<tol)return {x:v,residual:rn,history,states,converged:true};const d=pre(r);v=v.map((x,i)=>x+omega*d[i]);if(!Number.isFinite(norm(v))||norm(v)>1e10)break;}return {x:v,history,states,residual:history.at(-1),converged:false};}
 function solve(D=params,start,method=solver,tol=8e-13){if(!(tol>0&&Number.isFinite(tol)))throw Error('Invalid primal tolerance');let U=start?start.slice():initial(D),P;const history=[],states=[],max=method==='gs'?6000:35;for(let it=0;it<max;it++){const L=linearize(U,D,order,false),rn=norm(L.R);history.push(rn);if(it<200||it%20===0)states.push({it,U:U.slice(),r:rn});if(rn<tol){if(!valid(U,D))throw Error('Outside positive subsonic branch');return {U,history,states,converged:true};}if(!P||it%10===0||method==='gmres')P=firstOrderMatrix(U,D);let d;if(method==='gs')d=GS(P,L.R.map(x=>-x));else{const v=linearSolve(L,L.R.map(x=>-x),P,false,'gmres',2e-13);if(!v.converged)throw Error('Newton GMRES failed '+v.residual);d=v.x;}let step=method==='gs'?omega:1,next;while(step>1e-8){next=U.map((v,i)=>v+step*d[i]);const rr=norm(evaluate(real,next,D,order,false).R);if(valid(next,D)&&rr<(method==='gs'?1.5:1-1e-4*step)*rn)break;step*=.5;}if(step<=1e-8)throw Error('No admissible residual correction');U=next;}throw Error('Primal '+method+' not converged: '+history.at(-1));}
 function target(){if(!targetU){const D=params.slice();ax.forEach((xx,i)=>D[i]=(1-.3*Math.sin(Math.PI*xx)**2)*(1-.04*Math.sin(Math.PI*xx)));targetU=solve(D,null,'gmres').U;}return targetU;}
 function sensitivities(sol,D=params){if(objective==='inverse')target();const L=linearize(sol.U,D),P=firstOrderMatrix(sol.U,D),g=L.vjp(zeros(nd),1),l=linearSolve(L,g.U.map(x=>-x),P,true);if(!l.converged)throw Error('Adjoint '+solver+' not converged '+l.residual);return {...l,psi:l.x,gradient:L.vjp(l.x,1).p,L,P};}
 function tangent(sol,k,D=params){if(objective==='inverse')target();const L=linearize(sol.U,D),P=firstOrderMatrix(sol.U,D),dp=zeros(D.length);dp[k]=1;const f=L.jvp(zeros(nd),dp),l=linearSolve(L,f.R.map(x=>-x),P);if(!l.converged)throw Error('Tangent '+solver+' not converged '+l.residual);return {...l,gradient:L.jvp(l.x,dp).J};}
 function complexGradient(sol,k,D=params,hc=1e-25){if(objective==='inverse')target();const L=linearize(sol.U,D),P=firstOrderMatrix(sol.U,D),U=sol.U.map(x=>[x,0]),p=D.map((x,i)=>[x,i===k?hc:0]);for(let it=0;it<4;it++){const out=evaluate(complex,U,p),rhs=out.R.map(x=>-x[1]/hc);if(norm(rhs)<2e-12)break;const l=linearSolve(L,rhs,P,false,'gmres',2e-13);U.forEach((x,i)=>x[1]+=hc*l.x[i]);}const out=evaluate(complex,U,p),residual=norm(out.R.map(x=>x[1]/hc));if(residual>2e-10)throw Error('Complex residual not converged');return {gradient:out.J[1]/hc,residual};}
 return {node,n,m,nd,h,depth,x,ax,baseA,params,faceX,faces,order,solver,objective,constraints,weights,physics:physics(real),evaluate:(U,D=params)=>{if(objective==='inverse')target();return evaluate(real,U,D);},linearize,firstOrderMatrix,GS,linearSolve,valid,initial,solve,target,sensitivities,tangent,complexGradient};
}
const api={model,solve3,gamma,...Q};if(typeof module!=='undefined')module.exports=api;else root.QEuler=api;
})(typeof window!=='undefined'?window:globalThis);

附录 A:连续伴随的完整推导与实现

Appendix A: complete continuous-adjoint derivation and implementation

A.1 先明确问题和符号

A.1 Specify the problem and sign convention

取固定区间 [0,1],截面积 A(x)>0,稳态残差 ℛ=(AF)′−A′pe₂=0,目标 Q=∫j(U,A,A′,x)dx。U 是连续状态,B=FUF_U 是 3×3 通量 Jacobian;本附录用 B 表示通量导数,与正文第 6、17 节的 GS 预处理映射不是同一对象。令 ℒ=Q−∫λTℛdx。先只改变 U,再改变面积及边界数据,避免把两类变化混在一起。

On the fixed interval [0,1], let A(x)>0, ℛ=(AF)′−A′pe₂=0 and Q=∫j(U,A,A′,x)dx. U is continuous and B=FUF_U is the 3×3 flux Jacobian; this B is unrelated to the GS preconditioner map of Sections 6 and 17. Set ℒ=Q−∫λTℛdx. Vary U first, then area and boundary data, keeping the two variations distinct.

B=[010−3−γ2u2(3−γ)uγ−1(γ−1)u3−γuEγE−32(γ−1)u2γu],pU=(γ−12u2,−(γ−1)u,γ−1),E=e/ρ.B=\begin{bmatrix}0&1&0\\-\frac{3-\gamma}{2}u^2&(3-\gamma)u&\gamma-1\\(\gamma-1)u^3-\gamma uE&\gamma E-\frac32(\gamma-1)u^2&\gamma u\end{bmatrix},\quad p_U=\left(\frac{\gamma-1}{2}u^2,-(\gamma-1)u,\gamma-1\right),\quad E=e/\rho.

A.2 逐项变分和分部积分

A.2 Variation and integration by parts

δUℛ=(ABδU)′−A′e2pUδU.\delta_U\mathcal R=(AB\delta U)'-A'e_2p_U\delta U.
δUℒ=∫01jUδU dx−∫01λT(ABδU)′dx+∫01A′λ2pUδU dx.\delta_U\mathcal L=\int_0^1j_U\delta U\,dx-\int_0^1\lambda^T(AB\delta U)'dx+\int_0^1 A'\lambda_2p_U\delta U\,dx.
δUℒ=∫01(jUT+ABTλ′+A′λ2pUT)TδU dx−[λTABδU]01.\delta_U\mathcal L=\int_0^1\left(j_U^T+AB^T\lambda'+A'\lambda_2p_U^T\right)^T\delta U\,dx-\left[\lambda^TAB\delta U\right]_0^1.

分部积分作用在整个 ABδU 上。因此体内伴随包含 ABTλ′,不能再额外补一个 (AB)′Tλ;那会重复计算原始保守形式已经包含的系数变化。令任意内部扰动的系数为零,得到以下三分量一阶 ODE。

Integrate the derivative of the entire ABδU product. The volume adjoint contains ABTλ′; adding (AB)′Tλ would double-count coefficient variations already in the conservative primal form. Set the coefficient of arbitrary interior variations to zero to obtain a three-component first-order ODE.

−ABTλ′−A′λ2pUT=jUT,jUT={A′pUTQT,A(p−p⋆)pUTQI.-AB^T\lambda'-A'\lambda_2p_U^T=j_U^T,\qquad j_U^T=\begin{cases}A'p_U^T&Q_T,\\A(p-p^\star)p_U^T&Q_I.\end{cases}

A.3 边界条件来自允许的状态扰动

A.3 Boundary conditions follow admissible state variations

固定入口总压和总温时,C₀,UδU(0)=0,其中 C₀=(p₀,T₀)T;固定出口静压时,C₁,UδU(1)=0,其中 C₁=p。入口零空间是一维,出口零空间是二维。取它们的列基 N₀、N₁,把 δU=Nξ 代入边界项。只有对所有允许的 ξ 消去边界项,才得到正确的伴随条件,而不是随意令三个 λ 都为零。

For fixed inlet totals, C₀,UδU(0)=0 with C₀=(p₀,T₀)T. At fixed exit pressure, C₁,UδU(1)=0 with C₁=p. The inlet null space is one-dimensional and the outlet null space two-dimensional. Let N₀,N₁ span them and substitute δU=Nξ into the boundary term. Cancel it for every admissible ξ; setting all three λ values to zero arbitrarily is incorrect.

N0TABTλ(0)=0,N1TABTλ(1)=0.N_0^TAB^T\lambda(0)=0,\qquad N_1^TAB^T\lambda(1)=0.

本实现显式求总量的梯度。由 T=p/ρ、T₀=T+u²/7 得 dT₀=dT+2u du/7,再由 p₀=p(T₀/T)³·⁵ 得 dp₀。入口零空间用 ∇p₀×∇T₀ 归一化;出口可取 (1,0,−pρ/pe)T 与 (0,1,−pm/pe)T。这些公式不调用离散残差 Jacobian。

The implementation differentiates total quantities explicitly: T=p/ρ, T₀=T+u²/7, dT₀=dT+2u du/7 and p₀=p(T₀/T)³·⁵. Normalise ∇p₀×∇T₀ for the inlet null vector. At the outlet use (1,0,−pρ/pe)T and (0,1,−pm/pe)T. These formulas never call the discrete residual Jacobian.

逐步推导:边界条件的个数与形式

Step by step: how many boundary conditions, and in which form

亚声速拟一维 Euler 有三族特征速度 u−c<0<u<u+cu-c<0<u<u+c。原始问题在一端给定的量,恰好等于从这一端进入的特征数:入口进入 uu 与 u+cu+c,给定 p0p_0、T0T_0;出口只进入 u−cu-c,给定 pbp_b。伴随方程沿同样的特征反向传递信息,所以个数对调。

Subsonic quasi-1-D Euler has three characteristic speeds, u−c<0<u<u+cu-c<0<u<u+c. The primal problem prescribes at an end exactly as many data as characteristics enter there: uu and u+cu+c enter at the inlet, which receives p0p_0 and T0T_0; only u−cu-c enters at the outlet, which receives pbp_b. The adjoint carries information along the same characteristics in reverse, so the counts swap.

图 A.1 · 亚声速流道中的三族特征:u + c 与 u 向右、u − c 向左。原始问题在每一端给定“进入”的特征数:入口 2 个(p₀、T₀),出口 1 个(pb)。伴随沿同样的特征反向传播,个数正好对调:入口 1 个,出口 2 个。原始u + cuu − c入口 2 个条件出口 1 个条件p₀, T₀pb伴随u + cuu − c入口 1 个条件出口 2 个条件n₀T​A₀B₀T​λ(0) = 0N₁T​A₁B₁T​λ(1) = 0
图 A.1 · 亚声速流道中的三族特征:u + c 与 u 向右、u − c 向左。原始问题在每一端给定“进入”的特征数:入口 2 个(p₀、T₀),出口 1 个(pb)。伴随沿同样的特征反向传播,个数正好对调:入口 1 个,出口 2 个。
Figure A.1 · The three characteristic families in the subsonic duct: u + c and u run right, u − c runs left. The primal problem prescribes at each end as many data as characteristics enter: 2 at the inlet (p₀, T₀) and 1 at the outlet (pb). The adjoint travels along the same characteristics in reverse, so the counts swap: 1 at the inlet, 2 at the outlet.primalu + cuu − cinlet: 2outlet: 1p₀, T₀pbadjointu + cuu − cinlet: 1outlet: 2n₀T​A₀B₀T​λ(0) = 0N₁T​A₁B₁T​λ(1) = 0
Figure A.1 · The three characteristic families in the subsonic duct: u + c and u run right, u − c runs left. The primal problem prescribes at each end as many data as characteristics enter: 2 at the inlet (p₀, T₀) and 1 at the outlet (pb). The adjoint travels along the same characteristics in reverse, so the counts swap: 1 at the inlet, 2 at the outlet.

形式由 A.2 的边界项决定。出口只约束静压:允许的 δU(1)\delta U(1) 是 ker⁡pU\ker p_U 中任意向量,写成 δU(1)=N1ξ1\delta U(1)=N_1\xi_1,N1∈ℝ3×2N_1\in\mathbb R^{3\times2}。边界项对所有 ξ1\xi_1 为零:

The form follows from the boundary term of A.2. The outlet constrains only the static pressure: admissible δU(1)\delta U(1) are arbitrary vectors of ker⁡pU\ker p_U, written δU(1)=N1ξ1\delta U(1)=N_1\xi_1 with N1∈ℝ3×2N_1\in\mathbb R^{3\times2}. The boundary term must vanish for every ξ1\xi_1:

λ(1)TA1B1 N1ξ1=0∀ξ1⟺N1TA1B1Tλ(1)=0⟺A1B1Tλ(1)=pUT μ1.\begin{aligned}&\lambda(1)^TA_1B_1\,N_1\xi_1=0\quad\forall\xi_1\\ \Longleftrightarrow\quad&N_1^TA_1B_1^T\lambda(1)=0\\ \Longleftrightarrow\quad&A_1B_1^T\lambda(1)=p_U^T\,\mu_1.\end{aligned}

最后一式说的是:向量 q1=A1B1Tλ(1)q_1=A_1B_1^T\lambda(1) 与整个允许平面正交,只能平行于约束梯度 pUTp_U^T,差一个标量 μ1\mu_1。入口有两个约束,允许的扰动只剩一条直线 n0∝∇p0×∇T0n_0\propto\nabla p_0\times\nabla T_0,于是只得一个条件:

The last form says that the vector q1=A1B1Tλ(1)q_1=A_1B_1^T\lambda(1) is orthogonal to the whole admissible plane and can only be parallel to the constraint gradient pUTp_U^T, up to a scalar μ1\mu_1. The inlet has two constraints, the admissible variations reduce to the line n0∝∇p0×∇T0n_0\propto\nabla p_0\times\nabla T_0, and a single condition results:

n0TA0B0Tλ(0)=0⟺A0B0Tλ(0)=C0,UTμ0,μ0∈ℝ2.n_0^TA_0B_0^T\lambda(0)=0\quad\Longleftrightarrow\quad A_0B_0^T\lambda(0)=C_{0,U}^T\mu_0,\qquad \mu_0\in\mathbb R^2.
图 A.2 · 边界条件的几何意义(三维状态扰动空间的示意)。出口只约束静压,允许的扰动 δU(1) 构成与 pU 正交的平面,伴随条件要求 q₁ = A₁B₁ᵀλ(1) 垂直于这个平面,即平行于 pUᵀ。入口约束 p₀、T₀,允许的扰动只剩一条直线 n₀ ∝ ∇p₀ × ∇T₀,只需 q₀ 与 n₀ 正交。(a) 出口:一个约束 p = pb允许的 δU(1)(二维)q₁ = A₁B₁T​λ(1) ∥ pUT​(b) 入口:两个约束 p₀、T₀q₀ 可以在这个平面内n₀ ∝ ∇p₀ × ∇T₀允许的 δU(0) 只有这一条线q₀
图 A.2 · 边界条件的几何意义(三维状态扰动空间的示意)。出口只约束静压,允许的扰动 δU(1) 构成与 pU 正交的平面,伴随条件要求 q₁ = A₁B₁Tλ(1) 垂直于这个平面,即平行于 pUT。入口约束 p₀、T₀,允许的扰动只剩一条直线 n₀ ∝ ∇p₀ × ∇T₀,只需 q₀ 与 n₀ 正交。
Figure A.2 · What the boundary conditions mean geometrically (a sketch in the three-dimensional space of state variations). The outlet constrains only the static pressure, so admissible δU(1) form the plane orthogonal to pU, and the adjoint condition requires q₁ = A₁B₁ᵀλ(1) to be perpendicular to that plane, i.e. parallel to pUᵀ. The inlet constrains p₀ and T₀, leaving a single admissible line n₀ ∝ ∇p₀ × ∇T₀, and q₀ need only be orthogonal to n₀.(a) outlet: one constraint p = pbadmissible δU(1) (2-D)q₁ = A₁B₁T​λ(1) ∥ pUT​(b) inlet: two constraints p₀, T₀q₀ may lie in this planen₀ ∝ ∇p₀ × ∇T₀the only admissible δU(0)q₀
Figure A.2 · What the boundary conditions mean geometrically (a sketch in the three-dimensional space of state variations). The outlet constrains only the static pressure, so admissible δU(1) form the plane orthogonal to pU, and the adjoint condition requires q₁ = A₁B₁Tλ(1) to be perpendicular to that plane, i.e. parallel to pUT. The inlet constrains p₀ and T₀, leaving a single admissible line n₀ ∝ ∇p₀ × ∇T₀, and q₀ need only be orthogonal to n₀.

两端合计 2+1=32+1=3 个条件,与三分量一阶伴随 ODE 的阶数相同。μ0\mu_0、μ1\mu_1 正是 A.4 中边界数据导数用到的乘子:改变 p0p_0、T0T_0 或 pbp_b 时,梯度由这些乘子给出,(p0,T0)(p_0,T_0) 的梯度是 μ0\mu_0,pbp_b 的梯度是 −μ1-\mu_1。

The ends give 2+1=32+1=3 conditions, the order of the three-component first-order adjoint ODE. μ0\mu_0 and μ1\mu_1 are exactly the multipliers used for the boundary-data derivatives in A.4: when p0p_0, T0T_0 or pbp_b change, the gradients are given by these multipliers: that with respect to (p0,T0)(p_0,T_0) is μ0\mu_0 and that with respect to pbp_b is −μ1-\mu_1.

A.4 面积导数和边界数据导数

A.4 Area and boundary-data derivatives

δAℛ=(δAF)′−δA′pe2,δQ=∫01[(jA+λ′TF)δA+(jA′+λ2p)δA′]dx−[λTFδA]01.\delta_A\mathcal R=(\delta A F)'-\delta A'pe_2,\quad\delta Q=\int_0^1\left[(j_A+\lambda'^TF)\delta A+(j_{A'}+\lambda_2p)\delta A'\right]dx-\left[\lambda^TF\delta A\right]_0^1.

推力有 jAj_A=0、jA′j_{A'}=p;反设计有 jAj_A=(p−p⋆)²/2、jA′j_{A'}=0。若希望只剩 δA,再对第二项分部积分,但必须同时保留新产生的端点项。本页验证方向 δA=−0.02sin²(πx) 在两端为零,所以上式最后一项消失;这不是对任意面积设计变量都成立的假设。

For thrust, jAj_A=0 and jA′j_{A'}=p. For inverse design, jAj_A=(p−p⋆)²/2 and jA′j_{A'}=0. One may integrate the δA′ term by parts again, but must retain the resulting endpoint terms. The tested direction δA=−0.02sin²(πx) vanishes at both ends, cancelling the displayed endpoint term. Arbitrary area variables need not do so.

q0=A0B0Tλ0=C0,UTμ0,q1=A1B1Tλ1=C1,UTμ1,δQboundary=μ0Tδ(p0,T0)−μ1δpb.q_0=A_0B_0^T\lambda_0=C_{0,U}^T\mu_0,\quad q_1=A_1B_1^T\lambda_1=C_{1,U}^T\mu_1,\qquad\delta Q_{boundary}=\mu_0^T\delta(p_0,T_0)-\mu_1\delta p_b.

上式利用边界伴随条件把 q 放入约束梯度的列空间。固定状态边界数据时这一项为零;改变入口或出口数据时必须保留。若目标还含端点函数,其状态导数也应并入边界条件,本页两个积分目标没有该项。

The adjoint boundary conditions place q in the range of the constraint-gradient transpose. This contribution vanishes for fixed boundary data, but remains when inlet totals or exit pressure change. Endpoint objective terms would also modify boundary conditions; the two integral objectives here have none.

逐步推导:推力为什么对内部面积没有导数

Step by step: why the thrust has no interior area derivative

推力可以不借助伴随直接分析。第一步,动量方程是 (AF2)′=A′p(AF_2)'=A'p,积分一次,推力等于两端动量通量之差:

The thrust can be analysed without the adjoint. First, the momentum equation is (AF2)′=A′p(AF_2)'=A'p; one integration shows that the thrust equals the difference of the end momentum fluxes:

QT=∫01pA′ dx=[AF2]01=A(1)F2(1)−A(0)F2(0).Q_T=\int_0^1pA'\,dx=\bigl[AF_2\bigr]_0^1=A(1)F_2(1)-A(0)F_2(0).

第二步,等熵、亚声速、不阻塞时,总压 p0p_0、总温 T0T_0 沿程不变,截面状态只由当地马赫数 MM 决定。对 γ=1.4\gamma=1.4,静压、动量通量和单位面积质量流量分别是

Second, for isentropic, subsonic, unchoked flow the total pressure p0p_0 and total temperature T0T_0 are constant and the state of a section is fixed by its Mach number MM. For γ=1.4\gamma=1.4 the static pressure, the momentum flux and the mass flow per unit area are

p=p0(1+0.2M2)3.5,F2=ρu2+p=p (1+γM2),ρu=p0T0 m(M),m(M)=γ M(1+0.2M2)3.\begin{gathered}p=\frac{p_0}{(1+0.2M^2)^{3.5}},\qquad F_2=\rho u^2+p=p\,(1+\gamma M^2),\\ \rho u=\frac{p_0}{\sqrt{T_0}}\,m(M),\qquad m(M)=\frac{\sqrt\gamma\,M}{(1+0.2M^2)^3}.\end{gathered}

第三步,记 M0=M(0)M_0=M(0)、M1=M(1)M_1=M(1)。出口马赫数由出口静压决定,质量守恒再把入口马赫数与两端面积联系起来:

Third, write M0=M(0)M_0=M(0) and M1=M(1)M_1=M(1). The exit Mach number is set by the outlet static pressure, and conservation of mass ties the inlet Mach number to the two end areas:

(1+0.2M12)3.5=p0pb,m(M0) A(0)=m(M1) A(1).\Bigl(1+0.2M_1^2\Bigr)^{3.5}=\frac{p_0}{p_b},\qquad m(M_0)\,A(0)=m(M_1)\,A(1).

所以 QT=A(1)F2(M1)−A(0)F2(M0)Q_T=A(1)F_2(M_1)-A(0)F_2(M_0) 只依赖 A(0)A(0)、A(1)A(1) 和边界数据,与中间形状无关。本页 A(0)=A(1)=1A(0)=A(1)=1、p0=1p_0=1、pb=0.9p_b=0.9:M0=M1=0.3909M_0=M_1=0.3909,两端状态完全相同,QT=0Q_T=0,任何保持两端的面积扰动都不改变它。

Hence QT=A(1)F2(M1)−A(0)F2(M0)Q_T=A(1)F_2(M_1)-A(0)F_2(M_0) depends only on A(0)A(0), A(1)A(1) and the boundary data, not on the shape in between. On this page A(0)=A(1)=1A(0)=A(1)=1, p0=1p_0=1 and pb=0.9p_b=0.9: M0=M1=0.3909M_0=M_1=0.3909, the two end states coincide, QT=0Q_T=0, and no perturbation that keeps both ends changes it.

第四步,对两端面积求导。把 F2F_2、mm 看成 MM 的函数,取对数导数;两者相除再乘以 F2F_2,得到第二行:

Fourth, differentiate with respect to the end areas. With F2F_2 and mm as functions of MM, take the logarithmic derivatives; their ratio times F2F_2 gives the second line:

F2′F2=2γM1+γM2−γM1+0.2M2,m′m=1−M2M (1+0.2M2),F2′ mm′=γM2p.\begin{gathered}\frac{F_2'}{F_2}=\frac{2\gamma M}{1+\gamma M^2}-\frac{\gamma M}{1+0.2M^2},\qquad \frac{m'}{m}=\frac{1-M^2}{M\,(1+0.2M^2)},\\ F_2'\,\frac{m}{m'}=\gamma M^2p.\end{gathered}

质量守恒给出 ∂M0/∂A(0)=−m(M0)/(A(0) m′(M0))\partial M_0/\partial A(0)=-m(M_0)/\bigl(A(0)\,m'(M_0)\bigr) 与 ∂M0/∂A(1)=m(M1)/(A(0) m′(M0))\partial M_0/\partial A(1)=m(M_1)/\bigl(A(0)\,m'(M_0)\bigr),M1M_1 不随面积变化。代入并用 m(M1)/m(M0)=A(0)/A(1)m(M_1)/m(M_0)=A(0)/A(1):

Conservation of mass gives ∂M0/∂A(0)=−m(M0)/(A(0) m′(M0))\partial M_0/\partial A(0)=-m(M_0)/\bigl(A(0)\,m'(M_0)\bigr) and ∂M0/∂A(1)=m(M1)/(A(0) m′(M0))\partial M_0/\partial A(1)=m(M_1)/\bigl(A(0)\,m'(M_0)\bigr), while M1M_1 does not depend on the areas. Substituting, with m(M1)/m(M0)=A(0)/A(1)m(M_1)/m(M_0)=A(0)/A(1):

∂QT∂A(0)=−F2(M0)+F2′(M0)m(M0)m′(M0)=−p(0),∂QT∂A(1)=F2(M1)−γM02 p(0) A(0)A(1).\begin{aligned}\frac{\partial Q_T}{\partial A(0)}&=-F_2(M_0)+F_2'(M_0)\frac{m(M_0)}{m'(M_0)}=-p(0),\\ \frac{\partial Q_T}{\partial A(1)}&=F_2(M_1)-\gamma M_0^2\,p(0)\,\frac{A(0)}{A(1)}.\end{aligned}

入口一式对任何流道都成立。出口一式在本页 M0=M1=MM_0=M_1=M、A(0)=A(1)A(0)=A(1) 时化为 p(1)(1+γM2)−γM2p(1)=p(1)p(1)(1+\gamma M^2)-\gamma M^2p(1)=p(1)。

The inlet formula holds for any duct. On this page, with M0=M1=MM_0=M_1=M and A(0)=A(1)A(0)=A(1), the outlet formula reduces to p(1)(1+γM2)−γM2p(1)=p(1)p(1)(1+\gamma M^2)-\gamma M^2p(1)=p(1).

伴随给出同样的结论。把 A.4 中 δA′\delta A' 一项再分部积分一次:

The adjoint agrees. Integrating the δA′\delta A' term of A.4 by parts once more:

δQT=∫01[λ′TF−((1+λ2)p)′]⏟d(x)δA dx+[((1+λ2)p−λTF)δA]01.\delta Q_T=\int_0^1\underbrace{\Bigl[\lambda'^TF-\bigl((1+\lambda_2)p\bigr)'\Bigr]}_{d(x)}\delta A\,dx+\Bigl[\bigl((1+\lambda_2)p-\lambda^TF\bigr)\delta A\Bigr]_0^1.

图 A.3(b) 中推力的 λ 在两端为零,端点系数就是 p(1)p(1) 与 −p(0)-p(0),与上式一致。体内密度 dd 恒为零:数值上 max⁡∣d∣\max|d| 只有 max⁡∣λ′TF∣\max|\lambda'^TF| 的 7.2×10−107.2\times10^{-10},是 RK4 与差商的误差。所以本页验证方向 δA=−0.02sin⁡2(πx)\delta A=-0.02\sin^2(\pi x) 的伴随梯度是 −2.5×10−15-2.5\times10^{-15},连续差分是 −1.6×10−13-1.6\times10^{-13},都只是舍入误差。

The thrust λ of Figure A.3(b) vanishes at both ends, so the end coefficients are p(1)p(1) and −p(0)-p(0), as above. The interior density dd is identically zero: numerically max⁡∣d∣\max|d| is only 7.2×10−107.2\times10^{-10} of max⁡∣λ′TF∣\max|\lambda'^TF|, the error of RK4 and the difference quotient. Hence the adjoint gradient for this page's test direction δA=−0.02sin⁡2(πx)\delta A=-0.02\sin^2(\pi x) is −2.5×10−15-2.5\times10^{-15} and the continuous difference −1.6×10−13-1.6\times10^{-13}, both rounding.

两端 λ 为零也可以直接推出。由 A.4,μ0\mu_0 与 −μ1-\mu_1 就是 QTQ_T 对 (p0,T0)(p_0,T_0) 与 pbp_b 的导数。对称流道的两端状态对任何边界数据都相同,QTQ_T 恒为零,所以 μ0=0\mu_0=0、μ1=0\mu_1=0。亚声速时 det⁡B≠0\det B\ne0(A.5),由 A0B0Tλ(0)=C0,UTμ0=0A_0B_0^T\lambda(0)=C_{0,U}^T\mu_0=0 得 λ(0)=0\lambda(0)=0,同理 λ(1)=0\lambda(1)=0。λ 在内部不为零:它描述残差(源项)扰动的影响,而面积扰动沿解流形移动时,各项贡献恰好抵消。

That λ vanishes at both ends also follows directly. By A.4, μ0\mu_0 and −μ1-\mu_1 are the derivatives of QTQ_T with respect to (p0,T0)(p_0,T_0) and pbp_b. On a symmetric duct the two end states coincide for any boundary data, QTQ_T is identically zero, and so μ0=0\mu_0=0 and μ1=0\mu_1=0. In subsonic flow det⁡B≠0\det B\ne0 (A.5), so A0B0Tλ(0)=C0,UTμ0=0A_0B_0^T\lambda(0)=C_{0,U}^T\mu_0=0 gives λ(0)=0\lambda(0)=0, and likewise λ(1)=0\lambda(1)=0. Inside, λ is not zero: it measures the effect of residual (source) perturbations, while for area perturbations moving along the solution manifold the contributions cancel exactly.

A.5 独立原始参考、射击法和可解性

A.5 Independent primal reference, shooting and solvability

先由出口压力求 MeM_e,再用守恒质量流量在每个 x 上二分求 0<M<1 的等熵根。得到 U(x) 后,连续伴随是非齐次线性 ODE。积分一个零初值特解和三个单位初值齐次解,均使用 RK4;由左端一条、右端两条约束组成 3×3 系统,求线性组合系数。这与转置离散 Euler 矩阵是完全不同的计算。

Compute exit Mach from pressure, then use conserved mass flow and bisection to find the subsonic isentropic root at each x. Given U(x), integrate the affine linear adjoint ODE using RK4: one particular solution with zero initial value and three homogeneous solutions with unit initial values. One inlet and two outlet conditions determine three combination coefficients through a 3×3 solve. This is independent of transposing the discrete Euler matrix.

λ(x)=z(x)+Y(x)c,[N0TA0B0TN1TA1B1TY(1)]c=[0−N1TA1B1Tz(1)],Y(0)=I,  z(0)=0.\lambda(x)=z(x)+Y(x)c,\quad \begin{bmatrix}N_0^TA_0B_0^T\\N_1^TA_1B_1^TY(1)\end{bmatrix}c=\begin{bmatrix}0\\-N_1^TA_1B_1^Tz(1)\end{bmatrix},\quad Y(0)=I,\;z(0)=0.
U(x) = subsonic_isentropic_root(area(x), outlet_pressure)
f(x,lambda) = inverse(transpose(F_U(U(x)))) *
              (-j_U(x)-area_prime(x)*pressure_U(U(x))*lambda[1])/area(x)
z = RK4(f, initial=0)
Y[:,k] = RK4(homogeneous_part(f), initial=unit(k))
c = solve_3x3(left_and_right_boundary_conditions)
lambda(x) = z(x)+Y(x)*c
g = Simpson(j_explicit + dot(lambda_prime,F)*dA + lambda[1]*p*dA_prime)

局部条件 det B≠0 排除了 u=0 和 u=±c;还需上述射击矩阵可逆才能得到唯一两端解。接近阻塞、跨声速或激波时,不能靠把 ODE 步长变小解决适定性问题。下方可选 64/128/256/512 个步,检查解和梯度是否随 ODE 步长收敛,并与独立等熵流场中心差分比较。

Local invertibility det B≠0 excludes u=0 and u=±c. The shooting matrix must also be invertible for a unique two-end solution. Near choking, transonic transitions or shocks, reducing the ODE step does not repair ill-posedness. Select 64/128/256/512 steps below to test ODE and gradient convergence against independently recomputed isentropic finite differences.

图 A.3 · 连续伴随 λ(x) = (λ₁, λ₂, λ₃),由 A.5 的独立射击法求得(pb = 0.9,512 步 RK4)。(a) 反设计:两端都不为零,满足图 A.2 的条件。(b) 推力:两端 λ 为零,内部不为零;它描述残差扰动的影响,而面积扰动下的净贡献为零(见 A.4 末尾的推导)。(a) 反设计 QI−0.04−0.020.000.0200.250.50.751xλ₁λ₂λ₃(b) 推力 QT−0.4−0.20.00.20.40.600.250.50.751xλ₁λ₂λ₃
图 A.3 · 连续伴随 λ(x) = (λ₁, λ₂, λ₃),由 A.5 的独立射击法求得(pb = 0.9,512 步 RK4)。(a) 反设计:两端都不为零,满足图 A.2 的条件。(b) 推力:两端 λ 为零,内部不为零;它描述残差扰动的影响,而面积扰动下的净贡献为零(见 A.4 末尾的推导)。
Figure A.3 · The continuous adjoint λ(x) = (λ₁, λ₂, λ₃) from the independent shooting of A.5 (pb = 0.9, 512 RK4 steps). (a) Inverse design: nonzero at both ends, satisfying the conditions of Figure A.2. (b) Thrust: λ vanishes at both ends but not inside; it measures the effect of residual perturbations, while its net contribution for area changes is zero (derivation at the end of A.4).(a) inverse design QI−0.04−0.020.000.0200.250.50.751xλ₁λ₂λ₃(b) thrust QT−0.4−0.20.00.20.40.600.250.50.751xλ₁λ₂λ₃
Figure A.3 · The continuous adjoint λ(x) = (λ₁, λ₂, λ₃) from the independent shooting of A.5 (pb = 0.9, 512 RK4 steps). (a) Inverse design: nonzero at both ends, satisfying the conditions of Figure A.2. (b) Thrust: λ vanishes at both ends but not inside; it measures the effect of residual perturbations, while its net contribution for area changes is zero (derivation at the end of A.4).

A.6 与离散乘子比较时的约定

A.6 Conventions when comparing discrete multipliers

正文用积分残差和 Q+ψTR,所以内部有 λ≈−ψ;若残差除以体积,乘子还必须作相应缩放。格点端点的被替换行是边界约束,其乘子不是同位置的连续场分量。图中仅比较内部完整守恒块,并通过同一 δA 的总方向导数作更稳健的比较。目标压力在离散问题中固定为各阶各网格的离散目标,在连续问题中固定为等熵目标;两者只在加密极限趋于同一函数。

The main text uses integrated residuals and Q+ψTR, giving λ≈−ψ in the interior. Volume-normalised residuals require rescaled multipliers. Replaced endpoint rows on the node page carry constraint multipliers, not continuous field components. Compare only interior intact conservation blocks and also compare total derivatives along the same δA. Discrete targets use each grid/order’s fixed discrete flow; the continuous target is isentropic. They approach the same target only under refinement.

展开完整连续参考实现Complete continuous-reference implementation
/* Independent smooth isentropic flow and continuous-adjoint ODE shooting. */
(function(root){'use strict';const Q=typeof module!=='undefined'?require('./core'):root.QEuler,{solve3,dot,norm}=Q;
const area=x=>1-.3*Math.sin(Math.PI*x)**2,areaD=x=>-.3*Math.PI*Math.sin(2*Math.PI*x),direction=x=>-.02*Math.sin(Math.PI*x)**2,directionD=x=>-.02*Math.PI*Math.sin(2*Math.PI*x),targetArea=x=>area(x)*(1-.04*Math.sin(Math.PI*x));
function flow(x,af=area,pb=.9,p0=1,T0=1){if(!(pb>0&&pb<p0&&T0>0))throw Error("Require 0 < p_b < p0 and T0 > 0");const me=Math.sqrt(5*((p0/pb)**(2/7)-1));if(!(me>0&&me<1))throw Error("Require strictly subsonic exit Mach: p0 < p_b*(6/5)^(7/2)");const ax=af(x),ae=af(1);if(!(ax>0&&ae>0))throw Error("Require positive flow and exit areas");const mass=m=>Math.sqrt(1.4)*m/(1+.2*m*m)**3,mdot=ae*mass(me);if(mdot/ax>=mass(1))throw Error('Choked flow: smooth subsonic reference unavailable');let lo=0,hi=1;for(let k=0;k<55;k++){const mid=(lo+hi)/2;if(mass(mid)*af(x)>mdot)hi=mid;else lo=mid;}const M=(lo+hi)/2,T=T0/(1+.2*M*M),p=p0*(T/T0)**3.5,rho=p/T,u=M*Math.sqrt(1.4*T);return [rho,rho*u,p/.4+.5*rho*u*u];}
function coeff(U){const rho=U[0],u=U[1]/rho,E=U[2]/rho,p=.4*(U[2]-.5*U[1]*u),dp=[.2*u*u,-.4*u,.4],B=[[0,1,0],[-.8*u*u,1.6*u,.4],[.4*u**3-1.4*u*E,1.4*E-.6*u*u,1.4*u]],F=[U[1],U[1]*u+p,(U[2]+p)*u];return {rho,u,p,dp,B,F};}
function bcJac(U){const {rho,u,p,dp}=coeff(U),du=[-u/rho,1/rho,0],T=p/rho,T0=T+u*u/7,dT=dp.map((v,k)=>v/rho-(k===0?p/(rho*rho):0)),dT0=dT.map((v,k)=>v+2*u*du[k]/7),p0=p*(T0/T)**3.5,dp0=dp.map((v,k)=>p0*(v/p+3.5*(dT0[k]/T0-dT[k]/T)));return [dp0,dT0,dp];}
const cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]],transpose=A=>A[0].map((_,k)=>A.map(r=>r[k]));
const baseFlow=flow;
function reference(kind='inverse',steps=256,config={}){const {depth=.3,pb=.9,p0=1,T0=1}=config,area=x=>1-depth*Math.sin(Math.PI*x)**2,areaD=x=>-depth*Math.PI*Math.sin(2*Math.PI*x),flow=(x,af=area)=>baseFlow(x,af,pb,p0,T0);const U0=flow(0),U1=flow(1),c0=bcJac(U0),pe=bcJac(U1)[2],n0=cross(c0[0],c0[1]),ns=[n0.map(x=>x/norm(n0)),[1,0,-pe[0]/pe[2]],[0,1,-pe[1]/pe[2]]],bs=[coeff(U0).B,...Array(2).fill(coeff(U1).B)],rows=ns.map((n,i)=>bs[i].map(r=>dot(r,n)));
function rhs(x,y,forcing=true){const {B,dp,p}=coeff(flow(x)),pt=coeff(flow(x,targetArea)).p,j=kind==='thrust'?areaD(x):area(x)*(p-pt),b=dp.map(v=>-v*((forcing?j:0)+areaD(x)*y[1]));return solve3(transpose(B),b).map(v=>v/area(x));}
function integrate(y0,forcing){let y=y0.slice(),ys=[y.slice()];const h=1/steps;for(let i=0;i<steps;i++){const x=i*h,k1=rhs(x,y,forcing),k2=rhs(x+h/2,y.map((v,j)=>v+h*k1[j]/2),forcing),k3=rhs(x+h/2,y.map((v,j)=>v+h*k2[j]/2),forcing),k4=rhs(x+h,y.map((v,j)=>v+h*k3[j]),forcing);y=y.map((v,j)=>v+h*(k1[j]+2*k2[j]+2*k3[j]+k4[j])/6);ys.push(y.slice());}return ys;}
const part=integrate([0,0,0],true),basis=[0,1,2].map(k=>integrate([0,1,2].map(j=>+(j===k)),false)),M=[rows[0],...rows.slice(1).map(r=>basis.map(b=>dot(r,b.at(-1))))],b=[0,...rows.slice(1).map(r=>-dot(r,part.at(-1)))],initial=solve3(M,b),lambda=part.map((p,i)=>p.map((v,k)=>v+basis.reduce((s,z,j)=>s+initial[j]*z[i][k],0))),x=Array.from({length:steps+1},(_,i)=>i/steps),U=x.map(xx=>flow(xx));
const integrand=x.map((xx,i)=>{const {p,F}=coeff(U[i]),pt=coeff(flow(xx,targetArea)).p,lp=rhs(xx,lambda[i]);return (kind==='thrust'?p*directionD(xx):.5*(p-pt)**2*direction(xx))+dot(lp,F)*direction(xx)+lambda[i][1]*p*directionD(xx);});const simpson=v=>v.reduce((s,a,i)=>s+a*(i===0||i===steps?1:i%2?4:2),0)/(3*steps),gradient=simpson(integrand),objective=t=>simpson(x.map(xx=>{const p=coeff(flow(xx,x=>area(x)+t*direction(x))).p,pt=coeff(flow(xx,targetArea)).p;return kind==='thrust'?p*(areaD(xx)+t*directionD(xx)):.5*(p-pt)**2*(area(xx)+t*direction(xx));})),fd=(objective(1e-4)-objective(-1e-4))/2e-4;
return {x,U,lambda,gradient,fd,boundaryResidual:norm([dot(rows[0],lambda[0]),...rows.slice(1).map(r=>dot(r,lambda.at(-1)))]),rhs,initial};}
const api={area,areaD,direction,directionD,targetArea,flow,coeff,bcJac,reference};if(typeof module!=='undefined')module.exports=api;else root.QContinuous=api;
})(typeof window!=='undefined'?window:globalThis);

附录 B:从光滑解走向间断与激波

Appendix B: from smooth solutions to discontinuities and shocks

B.1 先区分光滑流道与激波问题

B.1 Separate smooth duct flow from shock problems

主求解器检查全部状态保持亚声速,因此不能用降低出口压力的方式偷偷引入激波。激波扩展需要新的原始分支、熵条件、适用于间断的数值通量及限制器,还需要明确激波位置如何改变。这里先给出 Euler 激波的独立局部计算和伴随推导入口;B.1–B.4 的局部计算不是完整激波流道求解器;B.5 另给出独立的全流道位置拟合与约化伴随,B.6 提供独立的激波捕捉有限体积实验。

The main solver enforces subsonic states; lowering exit pressure cannot silently turn it into a shock solver. Shocks require a new primal branch, entropy selection, discontinuity-compatible flux/limiting and a treatment of shock-position changes. This appendix provides an independent local Euler shock calculation and the starting adjoint derivation, not a completed shock-containing duct solver in B.1–B.4; B.5 adds an independent whole-duct shock fit and reduced adjoint; B.6 provides an independent shock-capturing finite-volume experiment.

[AF]−s[AU]=0,[q]=q+−q−,s=0, A continuous ⟹ [F]=0.[AF]-s[A U]=0,\quad [q]=q^+-q^-,\qquad s=0,\ A\text{ continuous}\ \Longrightarrow\ [F]=0.

把守恒方程在激波两侧的小区间积分,区间长度趋零时有界几何源项消失,留下 Rankine–Hugoniot 跳跃条件。静止正激波的三条条件是质量、动量和总焓连续;满足它们还不够,需选择熵增加、超声速转为亚声速的物理解。

Integrate conservation over a shrinking interval surrounding the shock. The bounded geometry source vanishes, leaving the Rankine–Hugoniot condition. A stationary normal shock conserves mass, momentum and total enthalpy. Select the entropy-increasing supersonic-to-subsonic branch in addition to these conservation conditions.

ρ+ρ−=(γ+1)M−22+(γ−1)M−2,p+p−=1+2γγ+1(M−2−1),M+2=1+γ−12M−2γM−2−γ−12.\frac{\rho^+}{\rho^-}=\frac{(\gamma+1)M_-^2}{2+(\gamma-1)M_-^2},\quad\frac{p^+}{p^-}=1+\frac{2\gamma}{\gamma+1}(M_-^2-1),\quad M_+^2=\frac{1+\frac{\gamma-1}{2}M_-^2}{\gamma M_-^2-\frac{\gamma-1}{2}}.
亚声速subsonic超声速supersonic亚声速subsonic喉部 M = 1throat, M = 1正激波 x = ξnormal shock, x = ξp₀ = T₀ = 1p_b00.511.5MM⁻ = 1.439M⁺ = 0.7240.20.40.60.8100.250.50.751xp, p₀[p] = p⁺ − p⁻p₀,₋ = 1p₀,₊ = 0.9478实线 p,虚线 p₀solid p, dashed p₀
图 B.1 · B.5 的激波喷管在 pbp_b = 0.8289(ξ = 0.75)时的流场。上:面积 A(x) = 1 + 2.2(x − 0.5)²,喉部 x = 0.5 为声速,激波位于扩张段 x = ξ。中:马赫数;上游按等熵关系 A/A* 取超声速分支,在 ξ 处按正激波关系由 M⁻ 跳到 M⁺。下:静压(实线)与总压(虚线)。激波两侧 [F] = 0,质量、动量、能量通量连续,但总压下降 p₀,₊ < p₀,₋,因此下游的声速面积变为 A*,₊ = A*,₋ p₀,₋/p₀,₊,出口压力 P(ξ) 由它决定。曲线由与 B.5 相同的关系逐点计算。Figure B.1 · The shocked nozzle of B.5 at pbp_b = 0.8289 (ξ = 0.75). Top: area A(x) = 1 + 2.2(x − 0.5)²; the throat x = 0.5 is sonic and the shock sits in the divergent part at x = ξ. Middle: Mach number; upstream it follows the isentropic A/A* relation on the supersonic branch, and at ξ the normal-shock relations take M⁻ to M⁺. Bottom: static pressure (solid) and total pressure (dashed). Across the shock [F] = 0, so mass, momentum and energy fluxes are continuous, but total pressure drops, p₀,₊ < p₀,₋, so the downstream sonic area becomes A*,₊ = A*,₋ p₀,₋/p₀,₊ and fixes the outlet pressure P(ξ). The curves are computed pointwise from the same relations as B.5.

B.2 固定界面跳跃的 tangent 和 adjoint

B.2 Tangent and adjoint of a fixed jump interface

先固定激波位置与速度。在非声速上下游状态处,线性化 [F]=0 得 B+U˙+=B−U˙−B^+\dot U^+=B^-\dot U^-,于是跳跃映射 T=(B⁺)⁻¹B⁻。反向传播不是把上游下游简单交换,而是 TT。下方计算器同时检查跳跃守恒与这个点积恒等式。这只证明固定界面局部映射,不包括激波移动的导数。

First fix shock position and speed. Away from sonic states, linearising [F]=0 gives B+U˙+=B−U˙−B^+\dot U^+=B^-\dot U^-, so T=(B⁺)⁻¹B⁻. Reverse propagation is TT, not a simple swap of upstream and downstream states. The calculator checks jump conservation and this dot-product identity. This verifies only the local fixed-interface map, not shock motion.

U˙+=TU˙−,T=(B+)−1B−,Uˉ−=(B−)T(B+)−TUˉ+,(Uˉ+)TU˙+=(Uˉ−)TU˙−.\dot U^+=T\dot U^-,\quad T=(B^+)^{-1}B^-,\quad\bar U^-=(B^-)^T(B^+)^{-T}\bar U^+,\quad(\bar U^+)^T\dot U^+=(\bar U^-)^T\dot U^-.

B.3 移动界面不能忽略

B.3 The interface-position variation cannot be omitted

δ[AF]+[(AF)′]δξ=0,δ[AF]=[ABδU+FδA],δQ=∫0ξδj−dx+∫ξ1δj+dx+(j−−j+)δξ.\delta[AF]+[(AF)']\delta\xi=0,\quad\delta[AF]=[AB\delta U+F\delta A],\quad\delta Q=\int_0^\xi\delta j^-dx+\int_\xi^1\delta j^+dx+(j^--j^+)\delta\xi.

δU 表示固定坐标处的扰动;移动界面还要加 U′δξ 的迹变化。分别在两段做分部积分,界面贡献为 λ+TA⁺B⁺δU⁺−λ−TA⁻B⁻δU⁻,并叠加位置项与跳跃约束乘子。消去允许的两侧迹和 δξ 后,才得到完整内部伴随传递条件。若把 δξ=0 强行固定,算出的只是固定激波位置的问题。

Here δU is an Eulerian perturbation; moving traces also contain U′δξ. Integration by parts on each side produces λ+TA⁺B⁺δU⁺−λ−TA⁻B⁻δU⁻ at the interface, together with location and jump-constraint multiplier terms. Eliminate admissible traces and δξ to derive the full internal adjoint conditions. Imposing δξ=0 instead differentiates a fixed-shock problem.

B.3(续)内部伴随条件:消去 δU⁺ 与 δξ

B.3 (continued) Internal adjoint conditions: eliminating δU⁺ and δξ

沿用附录 A 的约定 ℒ = Q − ∫λTℛ dx、ℛ = (AF)′ − A′p e₂,在 (0, ξ) 与 (ξ, 1) 上分别分部积分。两侧的体积项给出与附录 A 相同的伴随方程,激波处剩下:

Keep the convention of Appendix A, ℒ = Q − ∫λTℛ dx with ℛ = (AF)′ − A′p e₂, and integrate by parts on (0, ξ) and (ξ, 1) separately. The volume terms give the adjoint equation of Appendix A on each side; at the shock what remains is:

δℒ∣x=ξ=[λTAB δU]+(j−−j+) δξ,[q]=q+−q−.\delta\mathcal L\big|_{x=\xi}=\big[\lambda^TAB\,\delta U\big]+(j^--j^+)\,\delta\xi,\qquad [q]=q^+-q^-.

A 在激波处连续且 [F] = 0,所以线化跳跃条件中的 δA[F] 为零;两侧的原始方程给出 (AF)′ = A′p e₂,于是 [(AF)′] = A′(ξ)[p]e₂。B.3 的线化条件成为:

A is continuous at the shock and [F] = 0, so the δA[F] term of the linearised jump vanishes; the primal equation on each side gives (AF)′ = A′p e₂, hence [(AF)′] = A′(ξ)[p]e₂. The linearised condition of B.3 becomes:

A(B+δU+−B−δU−)+A′(ξ) [p] e2 δξ=0.A\big(B^+\delta U^+-B^-\delta U^-\big)+A'(\xi)\,[p]\,e_2\,\delta\xi=0.

用它消去 B⁺δU⁺,激波处的项整理为:

Using it to eliminate B⁺δU⁺, the terms at the shock become:

δℒ∣x=ξ=(λ+−λ−)TA B−δU−+(j−−j+−A′(ξ) [p] λ2) δξ.\delta\mathcal L\big|_{x=\xi}=(\lambda^+-\lambda^-)^TA\,B^-\delta U^-+\big(j^--j^+-A'(\xi)\,[p]\,\lambda_2\big)\,\delta\xi.

上游是超声速流:三条特征都从上游到达激波,δU⁻ 由上游数据决定、可以取任意值,且 B⁻ 可逆;δξ 由整个问题决定,不能预先给定。两个系数都必须为零,这就是激波处的内部伴随条件:

Upstream the flow is supersonic: all three characteristics reach the shock from upstream, so δU⁻ is set by upstream data and can take any value, and B⁻ is invertible; δξ is decided by the whole problem and cannot be prescribed. Both coefficients must vanish, and these are the internal adjoint conditions at the shock:

[λ]=0,A′(ξ) [p] λ2(ξ)=j−(ξ)−j+(ξ).\boxed{[\lambda]=0},\qquad\boxed{A'(\xi)\,[p]\,\lambda_2(\xi)=j^-(\xi)-j^+(\xi)}.

对 j = w(x)p(压力积分 w = 1,壁面推力 w = A′),第二式给出 λ₂(ξ) = −w(ξ)/A′(ξ):压力积分为 −1/A′(ξ),推力为 −1,都与激波强度无关。代回两侧的伴随方程,右端恰好为零,所以伴随在激波两侧的导数也为零:

For j = w(x)p (pressure integral w = 1, wall thrust w = A′) the second condition gives λ₂(ξ) = −w(ξ)/A′(ξ): −1/A′(ξ) for the pressure integral and −1 for thrust, independent of the shock strength. Substituted into the adjoint equation on either side it makes the right-hand side vanish, so the adjoint also has zero slope at the shock:

λ2(ξ)=−w(ξ)A′(ξ),−ABTλ′∣ξ±=(w+A′λ2) pUT∣ξ±=0 ⟹ λ′(ξ±)=0.\lambda_2(\xi)=-\frac{w(\xi)}{A'(\xi)},\qquad -AB^T\lambda'\big|_{\xi^\pm}=\big(w+A'\lambda_2\big)\,p_U^T\big|_{\xi^\pm}=0\ \Longrightarrow\ \lambda'(\xi^\pm)=0.

条件的个数与特征方向一致。伴随特征的方向与原始流动相反:下游亚声速区有两条从出口进入,对应附录 A.3 的两个出口条件,一条从激波进入,对应上面的 λ₂ 条件;上游超声速区三条都从激波进入,由连续性 [λ] = 0 给出。反过来,若强行令 δξ = 0(B.2 的固定界面),下游扰动要同时满足三个跳跃条件和出口条件,一般无解:激波位置正是那个多出来的未知量。背压导数仍按附录 A.4 读出:

The number of conditions matches the characteristic directions. Adjoint characteristics run opposite to the flow’s: in the subsonic downstream region two enter from the outlet, matching the two outlet conditions of Appendix A.3, and one enters from the shock, matching the λ₂ condition above; in the supersonic upstream region all three enter from the shock and come from continuity [λ] = 0. Conversely, forcing δξ = 0 (the fixed interface of B.2) would ask the downstream perturbation to meet three jump conditions and the outlet condition at once, which in general has no solution: the shock position is exactly the extra unknown. The back-pressure derivative is read off as in Appendix A.4:

A1B1Tλ(1)=μ1 pU(1)T,dQdpb=−μ1.A_1B_1^T\lambda(1)=\mu_1\,p_U(1)^T,\qquad\frac{dQ}{dp_b}=-\mu_1.

图 B.3 的 λ(x) 就按这些条件计算:出口的两个条件留下一个未知数 μ₁,由激波条件确定,再以连续性为初值向喉部积分。它与 B.5 相互独立(B.5 把流场消元后只剩一个标量 ψ),两者应给出同一个导数。ξ = 0.75 时,压力积分的 −μ₁ = 0.915331474650869,B.5(每段 192 面板)为 0.915331474626099,与本页代码无共享的 30 位高精度参考为 0.915331474650867;推力为 1.156988045808,B.5 给出 1.156988045795。所以 B.5 的约化乘子 ψ 正是连续伴随的出口乘子 μ₁。另把 cos⁴ 形的面积凸起分别放在超声速区、下游区和跨过激波处,用这个伴随算出的梯度与重新求解激波位置的高精度差分相差不到 10⁻⁹(压力积分)。

The λ(x) of Figure B.3 is computed exactly this way: the two outlet conditions leave one unknown μ₁, the shock condition fixes it, and continuity starts the integration towards the throat. It is independent of B.5, which eliminates the flow and keeps a single scalar ψ, and the two must give the same derivative. At ξ = 0.75 the pressure integral gives −μ₁ = 0.915331474650869; B.5 (192 panels per segment) gives 0.915331474626099, and a 30-digit reference sharing no code with this page gives 0.915331474650867. For thrust: 1.156988045808 against 1.156988045795 from B.5. So the reduced multiplier ψ of B.5 is exactly the outlet multiplier μ₁ of the continuous adjoint. Area bumps of cos⁴ shape placed in the supersonic region, downstream, and straddling the shock give adjoint gradients within 10⁻⁹ of high-precision differences that re-solve the shock position (pressure integral).

图中喉部下游的推力伴随是常数:在这一段 λ₂ ≡ −1,λ = (λ₁, −1, λ₃) 由出口条件决定。完整收扩喷管还须补上喉部上游亚声速段;默认等端面积、非零压缩激波下,该段 λ₂ 严格大于 −1,并非常数场的延拓。这对应一个守恒事实:推力 ∫pA′dx 等于两端动量通量之差。喉部阻塞时质量流量 qmq_m(即 ρuA)正比于喉部面积 A*;入口状态由 A(0)/A* 决定,出口状态由 qmq_m、pbp_b 和总温决定。所以只要面积扰动不碰两端、也不改变喉部面积,两端状态就不变,推力也不变;伴随对上述三个凸起(都离开喉部)给出的梯度都在 10⁻¹³ 以内(精确值为 0)。改变喉部面积则改变 qmq_m 和两端状态,推力随之改变。这一贡献集中在声速喉部,本页在喉部截断的光滑伴随积分不包含它,喉部面积的导数需要单独处理,推导见下。压力积分的伴随则在喉部有对数奇点:x − 0.5 每缩小十倍,λ 约增加 (0.918, −0.992, 0.262)。激波处连续、导数为零、需要一个内部条件,以及声速喉部的对数奇点,都与 Giles 和 Pierce 对拟一维 Euler 伴随解析解的结论一致(J. Fluid Mech. 426, 2001)。

The plotted thrust adjoint is constant downstream of the throat: λ₂ ≡ −1 there, and λ = (λ₁, −1, λ₃) is fixed by the outlet conditions. The complete converging–diverging duct also needs the upstream subsonic part; for the default equal end areas and a nonzero compressive shock, its λ₂ is strictly greater than −1 and is not an extension of that constant field. This reflects a conservation fact: the thrust ∫pA′dx equals the difference of the end momentum fluxes. With the throat choked, the mass flow qmq_m (that is, ρuA) is proportional to the throat area A*; the inlet state is set by A(0)/A*, the outlet state by qmq_m, pbp_b and the total temperature. So an area change that leaves the ends and the throat area unchanged leaves both end states, and the thrust, unchanged; for the three bumps above, all away from the throat, the adjoint gradients are below 10⁻¹³ (exact value 0). Changing the throat area changes qmq_m and both end states, and so the thrust. That contribution is concentrated at the sonic throat: the smooth adjoint integral on this page, truncated at the throat, does not contain it, and the throat-area derivative needs a separate treatment, derived below. The pressure-integral adjoint, by contrast, has a logarithmic singularity at the throat: λ grows by about (0.918, −0.992, 0.262) for every tenfold decrease of x − 0.5. Continuity with zero slope at the shock, the need for one internal condition, and the logarithmic singularity at the sonic throat all agree with the analytic quasi-one-dimensional Euler adjoint solutions of Giles and Pierce (J. Fluid Mech. 426, 2001).

逐步推导:喉部面积的推力导数

Step by step: the thrust derivative with respect to the throat area

推力等于两端动量通量之差,F=(ρu2+p)A=qmu+pAF=(\rho u^2+p)A=q_mu+pA。两端面积与 pbp_b 固定时,两端状态只通过喉部面积 A∗A^* 变化:入口在 A(0)/A∗=Φ(M0)A(0)/A^*=\Phi(M_0) 的亚声速根上取等熵状态;出口由阻塞质量流量 qm=A∗ρ∗u∗q_m=A^*\rho^*u^*、p(1)=pbp(1)=p_b 与总温 T0=1T_0=1 决定(本页 p0=T0=R=1p_0=T_0=R=1,γ=1.4\gamma=1.4)。

The thrust is the difference of the end momentum fluxes, F=(ρu2+p)A=qmu+pAF=(\rho u^2+p)A=q_mu+pA. With both end areas and pbp_b fixed, the end states change only through the throat area A∗A^*: the inlet takes the isentropic state on the subsonic root of A(0)/A∗=Φ(M0)A(0)/A^*=\Phi(M_0); the outlet is set by the choked mass flow qm=A∗ρ∗u∗q_m=A^*\rho^*u^*, p(1)=pbp(1)=p_b and the total temperature T0=1T_0=1 (on this page p0=T0=R=1p_0=T_0=R=1 and γ=1.4\gamma=1.4).

QT=F(1)−F(0),F(1)=qm u1+pbA(1),F(0)=p(0)A(0)(1+γM02),Φ(M)=1M(1+0.2M21.2)3.\begin{gathered}Q_T=F(1)-F(0),\qquad F(1)=q_m\,u_1+p_bA(1),\\F(0)=p(0)A(0)\bigl(1+\gamma M_0^2\bigr),\qquad \Phi(M)=\frac1M\Bigl(\frac{1+0.2M^2}{1.2}\Bigr)^3.\end{gathered}

入口:由 Φ′(M)=Φ(M)(M2−1)/(M(1+0.2M2))\Phi'(M)=\Phi(M)(M^2-1)/\bigl(M(1+0.2M^2)\bigr) 得 dM0/dA∗dM_0/dA^*,再对 p(0)=(1+0.2M02)−3.5p(0)=(1+0.2M_0^2)^{-3.5} 与 F(0)F(0) 求导,两个因子 1−M021-M_0^2 恰好相消:

Inlet: Φ′(M)=Φ(M)(M2−1)/(M(1+0.2M2))\Phi'(M)=\Phi(M)(M^2-1)/\bigl(M(1+0.2M^2)\bigr) gives dM0/dA∗dM_0/dA^*; differentiating p(0)=(1+0.2M02)−3.5p(0)=(1+0.2M_0^2)^{-3.5} and F(0)F(0), the two factors 1−M021-M_0^2 cancel:

dM0dA∗=M0(1+0.2M02)(1−M02)A∗,dF(0)dM0=γA(0)p(0)M0(1−M02)1+0.2M02⟹dF(0)dA∗=γp(0)A(0)M02A∗.\begin{gathered}\frac{dM_0}{dA^*}=\frac{M_0(1+0.2M_0^2)}{(1-M_0^2)A^*},\qquad \frac{dF(0)}{dM_0}=\frac{\gamma A(0)p(0)M_0(1-M_0^2)}{1+0.2M_0^2}\\\Longrightarrow\quad \frac{dF(0)}{dA^*}=\frac{\gamma p(0)A(0)M_0^2}{A^*}.\end{gathered}

出口:R=1R=1 时 u1=qmT1/(pbA(1))u_1=q_mT_1/\bigl(p_bA(1)\bigr),总温不变给出 dT1=−(u1/cp) du1dT_1=-(u_1/c_p)\,du_1;消去 dT1dT_1,并用 qmu1/(cppbA(1))=(γ−1)M12q_mu_1/\bigl(c_pp_bA(1)\bigr)=(\gamma-1)M_1^2 与 dqm/dA∗=qm/A∗dq_m/dA^*=q_m/A^*:

Outlet: with R=1R=1, u1=qmT1/(pbA(1))u_1=q_mT_1/\bigl(p_bA(1)\bigr), and the constant total temperature gives dT1=−(u1/cp) du1dT_1=-(u_1/c_p)\,du_1; eliminate dT1dT_1 and use qmu1/(cppbA(1))=(γ−1)M12q_mu_1/\bigl(c_pp_bA(1)\bigr)=(\gamma-1)M_1^2 and dqm/dA∗=qm/A∗dq_m/dA^*=q_m/A^*:

du1dqm=u1/qm1+(γ−1)M12,dF(1)dA∗=qmA∗ d(qmu1)dqm=qmu1A∗ 2+(γ−1)M121+(γ−1)M12.\frac{du_1}{dq_m}=\frac{u_1/q_m}{1+(\gamma-1)M_1^2},\qquad \frac{dF(1)}{dA^*}=\frac{q_m}{A^*}\,\frac{d(q_m u_1)}{dq_m}=\frac{q_m u_1}{A^*}\,\frac{2+(\gamma-1)M_1^2}{1+(\gamma-1)M_1^2}.
dQTdA∗=qmu1A∗ 2+(γ−1)M121+(γ−1)M12−γp(0)A(0)M02A∗\boxed{\frac{dQ_T}{dA^*}=\frac{q_m u_1}{A^*}\,\frac{2+(\gamma-1)M_1^2}{1+(\gamma-1)M_1^2}-\frac{\gamma p(0)A(0)M_0^2}{A^*}}

本页 pb=0.8289p_b=0.8289(ξ=0.75\xi=0.75):M0=0.4129M_0=0.4129,M1=0.4419M_1=0.4419,qm=0.6847q_m=0.6847,u1=0.5129u_1=0.5129;dF(1)/dA∗=0.6770dF(1)/dA^*=0.6770,dF(0)/dA∗=0.3289dF(0)/dA^*=0.3289,所以 dQT/dA∗=0.3480dQ_T/dA^*=0.3480。独立核对:在同样的激波拟合关系下把面积换成 A+ϵβA+\epsilon\beta(β\beta 是以喉部为中心的 cos⁡4\cos^4 凸起,β(0.5)=1\beta(0.5)=1,两端为零),每个 ϵ\epsilon 重新求激波位置,直接对 ∫pA′dx\int pA'dx 作中心差分,与上式之差小于 10−710^{-7}。

On this page pb=0.8289p_b=0.8289 (ξ=0.75\xi=0.75): M0=0.4129M_0=0.4129, M1=0.4419M_1=0.4419, qm=0.6847q_m=0.6847, u1=0.5129u_1=0.5129; dF(1)/dA∗=0.6770dF(1)/dA^*=0.6770 and dF(0)/dA∗=0.3289dF(0)/dA^*=0.3289, so dQT/dA∗=0.3480dQ_T/dA^*=0.3480. An independent check replaces the area by A+ϵβA+\epsilon\beta (β\beta a cos⁡4\cos^4 bump centred on the throat, β(0.5)=1\beta(0.5)=1, zero at both ends), re-solves the shock position for each ϵ\epsilon, and takes central differences of ∫pA′dx\int pA'dx directly; the difference from the formula is below 10−710^{-7}.

因此对任何保持两端面积不变的扰动都有 δQT=(dQT/dA∗) δA(0.5)\delta Q_T=(dQ_T/dA^*)\,\delta A(0.5):A′(0.5)=0A'(0.5)=0,喉部面积的一阶变化就是 δA\delta A 在喉部的值。推力对面积的全部灵敏度集中在声速喉部这一点,喉部以外的光滑伴随积分部分为零,这正是上面三个凸起得到 0 的原因。增大喉部面积使 qmq_m 增大,出口动量通量的增量大于入口,推力增大。

Hence for every perturbation that keeps both end areas, δQT=(dQT/dA∗) δA(0.5)\delta Q_T=(dQ_T/dA^*)\,\delta A(0.5): since A′(0.5)=0A'(0.5)=0, the first-order change of the throat area is the value of δA\delta A at the throat. The whole area sensitivity of the thrust sits at the single sonic point, and the smooth adjoint integral away from the throat contributes zero, which is why the three bumps above give 0. Enlarging the throat raises qmq_m; the outlet momentum flux grows more than the inlet flux, and the thrust increases.

0.20.40.60.80.50.60.70.80.91pp_b = 0.8289(ξ = 0.7500)p_b = 0.8289 (ξ = 0.7500)p_b + 0.02(ξ′ = 0.7130)p_b + 0.02 (ξ′ = 0.7130)δξ-101230.50.60.70.80.91xΔp / Δp_b扫过区:高约 [p]/Δp_b ≈ 18.6(截断)swept strip: height ≈ [p]/Δp_b ≈ 18.6 (cut off)Δp_b → 0:−[p] δξ δ(x − ξ)Δp_b → 0: −[p] δξ δ(x − ξ)上游:扰动为零(喉部阻塞)upstream: no perturbation (choked throat)下游:光滑的欧拉扰动 δpdownstream: smooth Eulerian δp
图 B.2 · 移动界面项从哪里来。上:两个背压下的静压,背压升高使激波向上游移动 δξ(紫色带)。下:差商 [p(x; pbp_b + Δpbp_b) − p(x; pbp_b)]/Δpbp_b。两侧的欧拉扰动光滑有界;在被激波扫过的窄带里,差值是 O(1) 的跳跃 [p],差商高约 [p]/Δpbp_b,带宽 |δξ| 与 Δpbp_b 成正比,面积趋于 −[p] dξ/dpb\mathrm dξ/\mathrm dp_b。所以 Δpbp_b → 0 时扰动含 −[p]δξ δ(x − ξ) 这一项,它就是 B.3 中 δ[AF] + [(AF)′]δξ = 0 与 δQ 里 (j⁻ − j⁺)δξ 的来源;只对光滑部分求导,会漏掉它。Figure B.2 · Where the moving-interface term comes from. Top: static pressure at two back pressures; raising pbp_b moves the shock upstream by δξ (violet strip). Bottom: the difference quotient [p(x; pbp_b + Δpbp_b) − p(x; pbp_b)]/Δpbp_b. The Eulerian perturbation is smooth and bounded on each side; in the narrow strip the shock sweeps the difference is the O(1) jump [p], the quotient is about [p]/Δpbp_b high, and the strip width |δξ| is proportional to Δpbp_b, so its area tends to −[p] dξ/dpb\mathrm dξ/\mathrm dp_b. As Δpbp_b → 0 the perturbation therefore contains −[p]δξ δ(x − ξ): this is where δ[AF] + [(AF)′]δξ = 0 and the (j⁻ − j⁺)δξ in δQ come from, and differentiating only the smooth parts misses it.
-2-10120.50.60.70.80.91xλλ₁λ₂λ₃λ₂(ξ) = −1/A′(ξ) = -0.9091[λ] = 0,λ′(ξ±) = 0[λ] = 0, λ′(ξ±) = 0ξ = 0.75← 喉部:对数奇点← throat: logarithmic singularity出口两个条件two outlet conditions激波条件shock condition连续性 [λ] = 0continuity [λ] = 0实线:压力积分;虚线:壁面推力(λ 为常数)solid: pressure integral; dashed: wall thrust (λ constant)
图 B.3 · B.5 激波喷管(ξ = 0.75)的连续伴随 λ(x),按上文的内部条件计算。实线为压力积分,λ₂ 在激波处等于 −1/A′(ξ),λ 连续且两侧导数为零,向声速喉部对数增长(图中在 x − 0.5 = 1.5×10⁻³ 截断)。虚线为壁面推力:λ 是常数,λ₂ ≡ −1。箭头是伴随特征的方向,与原始流动相反:下游亚声速区两个条件来自出口,一个来自激波;上游超声速区三个条件都由激波处的连续性给出。由此得到 dQ/dpb\mathrm dQ/\mathrm dp_b = −μ₁ = 0.915331474651,与 B.5 约化伴随相同(见正文)。Figure B.3 · The continuous adjoint λ(x) of the B.5 shocked nozzle (ξ = 0.75), computed with the internal conditions derived above. Solid: pressure integral; λ₂ equals −1/A′(ξ) at the shock, λ is continuous with zero slope on both sides, and it grows logarithmically towards the sonic throat (cut at x − 0.5 = 1.5×10⁻³). Dashed: wall thrust; λ is constant with λ₂ ≡ −1. The arrows are adjoint characteristic directions, opposite to the flow's: downstream, in the subsonic region, two conditions come from the outlet and one from the shock; upstream, in the supersonic region, continuity at the shock gives all three. This yields dQ/dpb\mathrm dQ/\mathrm dp_b = −μ₁ = 0.915331474651, the same as the reduced adjoint of B.5 (see the text).
展开带内部激波条件的连续伴随的完整实现Complete implementation of the continuous adjoint with internal shock conditions
/* Continuous adjoint of the shock-fitted nozzle of Appendix B.5, with the
   internal shock conditions obtained by finishing the elimination of B.3.

   Convention of Appendix A: L = Q - int lambda^T R dx with (Q the objective,
   J the residual Jacobian on these pages), R = (AF)' - A' p e2, and
   [q] = q+ - q- across the shock at x = xi.
     interior:  -A B^T lambda' - A' lambda_2 p_U^T = j_U^T      (each side)
     shock:     [lambda] = 0,   A'(xi) [p] lambda_2(xi) = j- - j+
     outlet:    A B^T lambda(1) = mu_1 p_U(1)^T,   dQ/dp_b = -mu_1
   Objectives j = w(x) p with w = 1 (pressure integral) or w = A' (thrust).

   The flow is the isentropic / normal-shock solution of B.5 (same area,
   gamma = 1.4, inlet p0 = T0 = 1, R = 1). Downstream of the shock the
   linear adjoint ODE is integrated with RK4 in x from the outlet, as a
   particular solution plus mu_1 times a homogeneous one, and the shock
   condition fixes mu_1. Upstream of the shock, in the supersonic branch,
   continuity alone starts the integration; it runs in s = log(x - 1/2)
   because the pressure-integral adjoint has a logarithmic singularity at
   the sonic throat. Area-bump gradients are integrated alongside:
     dQ = int (j_A + lambda'^T F) dA + (j_A' + lambda_2 p) dA' dx. */
(function (root) {
  'use strict';
  const G = 1.4;
  const area = x => 1 + 2.2 * (x - 0.5) ** 2, areaX = x => 4.4 * (x - 0.5);
  const ratio = M => ((5 / 6) * (1 + 0.2 * M * M)) ** 3 / M;

  function mach(r, sup) {
    if (!(r >= 1 - 1e-14)) throw Error('Area below the sonic area');
    if (Math.abs(r - 1) < 1e-15) return 1;
    let lo = sup ? 1 : 1e-7, hi = sup ? 20 : 1;
    for (let k = 0; k < 60; k++) {
      const m = (lo + hi) / 2;
      if ((ratio(m) < r) === sup) lo = m; else hi = m;
    }
    let M = (lo + hi) / 2;
    for (let k = 0; k < 4; k++) {
      const f = ratio(M) - r, d = ratio(M) * (1.2 * M / (1 + 0.2 * M * M) - 1 / M);
      M -= f / d;
    }
    return M;
  }
  function state(M, p0) {
    const T = 1 / (1 + 0.2 * M * M), p = p0 * T ** 3.5, rho = p / T, u = M * Math.sqrt(G * T);
    return { M, p, rho, u, E: p / (G - 1) / rho + 0.5 * u * u };
  }
  function shock(xi) {
    const M1 = mach(area(xi), true), p1 = state(M1, 1).p;
    const M2 = Math.sqrt((1 + 0.2 * M1 * M1) / (G * M1 * M1 - 0.2));
    const p2 = p1 * (1 + 7 / 6 * (M1 * M1 - 1)), p02 = p2 * (1 + 0.2 * M2 * M2) ** 3.5;
    return { xi, M1, M2, p1, p2, p02 };          // downstream sonic area A*,+ = 1 / p02
  }
  const outletPressure = xi => { const s = shock(xi); return state(mach(area(1) * s.p02, false), s.p02).p; };
  function position(pb) {
    let a = 0.52, b = 0.98;
    const fa = outletPressure(a) - pb, fb = outletPressure(b) - pb;
    if (!(fa * fb < 0)) throw Error('Back pressure is outside the interior-shock bracket');
    let flo = fa;
    for (let k = 0; k < 200 && b - a > 1e-16; k++) {
      const m = (a + b) / 2, fm = outletPressure(m) - pb;
      if (fm * flo > 0) { a = m; flo = fm; } else b = m;
    }
    return (a + b) / 2;
  }
  /* side is explicit: at x = xi the two branches meet, and a rounded x must
     not pick the wrong one */
  const flow = (x, s, side) => side === 'up' ? state(mach(area(x), x > 0.5), 1) : state(mach(area(x) * s.p02, false), s.p02);

  function solve3(A, b) {
    const a = A.map((r, i) => [...r, b[i]]);
    for (let k = 0; k < 3; k++) {
      let p = k;
      for (let i = k + 1; i < 3; i++) if (Math.abs(a[i][k]) > Math.abs(a[p][k])) p = i;
      [a[p], a[k]] = [a[k], a[p]];
      for (let i = k + 1; i < 3; i++) { const f = a[i][k] / a[k][k]; for (let j = k; j < 4; j++) a[i][j] -= f * a[k][j]; }
    }
    const x = [0, 0, 0];
    for (let i = 2; i >= 0; i--) { let t = a[i][3]; for (let j = i + 1; j < 3; j++) t -= a[i][j] * x[j]; x[i] = t / a[i][i]; }
    return x;
  }
  /* A B^T and p_U^T at a state (B = F_U as in Appendix A.1, E = e / rho) */
  function ABt(q, A) {
    const { u, E } = q;
    return [[0, -(3 - G) / 2 * u * u, (G - 1) * u ** 3 - G * u * E],
            [1, (3 - G) * u, G * E - 1.5 * (G - 1) * u * u],
            [0, G - 1, G * u]].map(r => r.map(v => A * v));
  }
  const pU = q => [(G - 1) / 2 * q.u * q.u, -(G - 1) * q.u, G - 1];
  const fluxF = q => [q.rho * q.u, q.rho * q.u * q.u + q.p, (q.rho * q.E + q.p) * q.u];
  const weight = (kind, x) => kind === 'pressure' ? 1 : areaX(x);

  /* lambda' for the particular (with source) or homogeneous part */
  function slope(x, lam, s, side, kind, withSource) {
    const q = flow(x, s, side), P = pU(q), c = (withSource ? weight(kind, x) : 0) + areaX(x) * lam[1];
    return solve3(ABt(q, area(x)), P.map(v => -c * v));
  }
  /* a C3 bump: cos^4 has three continuous derivatives at its edges, so the
     RK4 quadrature of the gradient integrals keeps its order */
  function bump(c, w) {
    return {
      c, w,
      f: x => Math.abs(x - c) < w ? Math.cos(Math.PI * (x - c) / (2 * w)) ** 4 : 0,
      df: x => Math.abs(x - c) < w ? -4 * Math.cos(Math.PI * (x - c) / (2 * w)) ** 3 * Math.sin(Math.PI * (x - c) / (2 * w)) * Math.PI / (2 * w) : 0
    };
  }
  /* integrand of the area-bump gradient for the particular or homogeneous part */
  function bumpDensity(x, lam, s, side, kind, b, withSource) {
    const q = flow(x, s, side), dl = slope(x, lam, s, side, kind, withSource), F = fluxF(q);
    const jAp = withSource && kind === 'thrust' ? q.p : 0;
    return (dl[0] * F[0] + dl[1] * F[1] + dl[2] * F[2]) * b.f(x) + (jAp + lam[1] * q.p) * b.df(x);
  }

  /* RK4 for the pair (z, y) plus the bump integrals, in a variable t with
     x = X(t) and dx/dt = Xt(t) */
  function march(t0, t1, n, X, Xt, z0, y0, s, side, kind, bumps, keep) {
    const h = (t1 - t0) / n, out = [];
    let z = z0.slice(), y = y0.slice(), Iz = bumps.map(() => 0), Iy = bumps.map(() => 0);
    const deriv = (t, z, y) => {
      const x = X(t), j = Xt(t);
      return {
        z: slope(x, z, s, side, kind, true).map(v => v * j),
        y: slope(x, y, s, side, kind, false).map(v => v * j),
        Iz: bumps.map(b => bumpDensity(x, z, s, side, kind, b, true) * j),
        Iy: bumps.map(b => bumpDensity(x, y, s, side, kind, b, false) * j)
      };
    };
    const axpy = (a, v, k) => a.map((x, i) => x + k * v[i]);
    if (keep) out.push([X(t0), z.slice(), y.slice()]);
    for (let k = 0; k < n; k++) {
      const t = t0 + k * h;
      const k1 = deriv(t, z, y);
      const k2 = deriv(t + h / 2, axpy(z, k1.z, h / 2), axpy(y, k1.y, h / 2));
      const k3 = deriv(t + h / 2, axpy(z, k2.z, h / 2), axpy(y, k2.y, h / 2));
      const k4 = deriv(t + h, axpy(z, k3.z, h), axpy(y, k3.y, h));
      const comb = (a, key) => a.map((v, i) => v + h / 6 * (k1[key][i] + 2 * k2[key][i] + 2 * k3[key][i] + k4[key][i]));
      z = comb(z, 'z'); y = comb(y, 'y'); Iz = comb(Iz, 'Iz'); Iy = comb(Iy, 'Iy');
      if (keep) out.push([X(t + h), z.slice(), y.slice()]);
    }
    return { z, y, Iz, Iy, out };
  }

  function field(pb, kind = 'pressure', { steps = 4000, throat = 1e-4, bumps = [] } = {}) {
    if (!['pressure', 'thrust'].includes(kind)) throw Error('Choose pressure or thrust');
    const xi = position(pb), s = shock(xi), bs = bumps.map(([c, w]) => bump(c, w));
    const q1 = flow(1, s, 'down');
    const y1 = solve3(ABt(q1, area(1)), pU(q1));                     // lambda(1) per unit mu_1
    const id = t => t, one = () => 1;
    const down = march(1, xi, steps, id, one, [0, 0, 0], y1, s, 'down', kind, bs, true);
    /* shock condition, written out from the two shock states */
    const jm = weight(kind, xi) * s.p1, jp = weight(kind, xi) * s.p2;
    const lam2 = (jm - jp) / (areaX(xi) * (s.p2 - s.p1));
    const mu1 = (lam2 - down.z[1]) / down.y[1];
    const up = march(Math.log(xi - 0.5), Math.log(throat), steps, t => 0.5 + Math.exp(t), t => Math.exp(t),
      down.z, down.y, s, 'up', kind, bs, true);
    const lam = (z, y) => z.map((v, i) => v + mu1 * y[i]);
    const samples = [...up.out.slice().reverse(), ...down.out.slice().reverse().slice(1)]
      .map(([x, z, y]) => [x, lam(z, y)]);
    const qs = flow(xi, s, 'down'), qm = flow(xi, s, 'up'), lxi = lam(down.z, down.y);
    const slopeAt = q => { const c = weight(kind, xi) + areaX(xi) * lxi[1]; return solve3(ABt(q, area(xi)), pU(q).map(v => -c * v)); };
    return {
      pb, kind, xi, shock: s, mu1, gradient: -mu1, lam2Shock: lam2, lamShock: lxi,
      slopeMinus: slopeAt(qm), slopePlus: slopeAt(qs), samples,
      /* integrals run from 1 down to xi and from xi down to the throat, so
         both carry a minus sign relative to int_throat^1 */
      bumpGradients: bs.map((b, k) => -(down.Iz[k] + mu1 * down.Iy[k]) - (up.Iz[k] + mu1 * up.Iy[k]))
    };
  }

  /* the fitted flow itself, sampled on each side of the shock (Mach number,
     static and total pressure) */
  function profile(pb, n = 160) {
    const xi = position(pb), s = shock(xi), up = [], down = [];
    for (let k = 0; k <= n; k++) {
      const a = xi * k / n, qa = flow(a, s, 'up');
      up.push([a, qa.M, qa.p, 1]);
      const b = xi + (1 - xi) * k / n, qb = flow(b, s, 'down');
      down.push([b, qb.M, qb.p, s.p02]);
    }
    return { pb, xi, shock: s, up, down };
  }

  const api = { field, profile, position, shock, outletPressure, area, areaX };
  if (typeof module !== 'undefined') module.exports = api; else root.QShockAdjoint = api;
})(typeof window !== 'undefined' ? window : globalThis);

B.4 激波离散实现应如何验证

B.4 Verification required for shock discretisation

采用限制器后,必须保存并区分活跃分支:固定分支的离散导数、跨分支的有限差分以及网格极限中的移动激波导数不是同一对象。稳态残差、限制器、波速、边界与源项应进入同一计算图;复数步长只能验证基准分支的解析延拓。B.5–B.6 分别展示位置拟合与固定网格捕捉;应通过逐步长差分与网格加密报告分支变化、误差或不收敛,不能仅凭点积测试声称激波伴随已解决。原 B.6 二阶模型的 N=24 有向证书定位了背压约 0.7391592513074169 的精确稳态切换根:压力目标两侧导数约为 0.5938613640 和 0.6220204651,严格不同。这否定普遍经典可微性,不否定默认背压 0.75 的网格极限。

With limiting, record active branches and distinguish fixed-branch discrete derivatives, finite differences crossing branches and moving-shock derivatives in a mesh limit. Residual, limiter, wave speed, boundary and source must share the differentiated graph. Complex step checks only analytic continuation of the baseline branch. B.5–B.6 distinguish shock fitting from fixed-grid capturing. Step-size sweeps and refinement must report branch changes, errors and failed convergence. A dot-product test alone does not establish a correct moving-shock adjoint. A directed N=24 certificate for the original second-order B.6 model locates an exact steady switch root at back pressure approximately 0.7391592513074169: the pressure objective has unequal one-sided derivatives, about 0.5938613640 and 0.6220204651. This refutes universal classical differentiability; it does not refute the default back-pressure 0.75 mesh limit.

B.5 整个流道的激波位置拟合与约化伴随

B.5 Full-duct shock fitting and a reduced adjoint

下面是与顶部亚声速算例独立的实验。采用 A(x)=1+2.2(x−0.5)²、γ=1.4、入口 p₀=T₀=1,在喉部阻塞。上游由亚声速转为超声速,在扩张段 x=ξ 处发生正激波,下游为亚声速。两段光滑流动用等熵关系消元,激波满足质量、动量和能量跳跃;出口压力决定未知激波位置。它求解整个流道中的移动激波敏感度,不是正文有限体积格式,也不是完整连续伴随场或激波捕捉离散伴随。

This experiment is independent of the subsonic controls above. Use A(x)=1+2.2(x−0.5)², γ=1.4 and inlet p₀=T₀=1, with a choked throat. The upstream flow goes from subsonic to supersonic, a normal shock sits in the divergent branch at ξ, and the downstream flow is subsonic. Isentropic relations eliminate the smooth flow on each side; the shock satisfies mass, momentum and energy jumps. Outlet pressure determines its unknown position. This computes moving-shock sensitivities for a whole duct, but is not the main finite-volume scheme, a full continuous-adjoint field, or a shock-capturing discrete adjoint.

AA∗=1M[2γ+1(1+γ−12M2)]γ+12(γ−1),A∗,+=A∗,−p0,−p0,+.\frac{A}{A_*}=\frac1M\left[\frac{2}{\gamma+1}\left(1+\frac{\gamma-1}{2}M^2\right)\right]^{\frac{\gamma+1}{2(\gamma-1)}},\qquad A_{*,+}=A_{*,-}\frac{p_{0,-}}{p_{0,+}}.

先给 ξ:上游面积关系给 M⁻,正激波关系给 M⁺ 与下游总压 p₀,+,再从下游等熵关系求出口压力 P(ξ)。在 0.52<ξ<0.98 中求 G(ξ,pbp_b)=P(ξ)−pbp_b=0。只接受括区内部的压力;离开此分支不能把端点伪装成收敛激波。

Given ξ, the upstream area relation gives M⁻, the normal-shock relations give M⁺ and downstream total pressure, and downstream isentropic propagation gives outlet pressure P(ξ). Solve G(ξ,pbp_b)=P(ξ)−pbp_b=0 for 0.52<ξ<0.98. Only pressures inside the bracket are accepted; outside this branch an endpoint is not a converged interior shock.

Gξdξdpb=1,Gξψ=−Qξ,dQdpb=Qpb+ψGpb=−ψ.G_\xi\frac{d\xi}{dp_b}=1,\qquad G_\xi\psi=-Q_\xi,\qquad\frac{dQ}{dp_b}=Q_{p_b}+\psi G_{p_b}=-\psi.

这里 Q 没有显式 pbp_b 项,所有依赖经 ξ 以及已消元的下游流场传递。ψ 是一个约化系统乘子,不能画成三个连续 Euler 伴随分量。面积–马赫数方程用二分法求根,其导数通过隐函数关系求出;局部状态、跳跃和积分链式求导不使用差分步长。差分另行重求激波位置,用于检查。

Q has no explicit pbp_b term here; dependence passes through ξ and the eliminated downstream flow. ψ is one reduced-system multiplier, not three continuous Euler adjoint components. The area–Mach equation is solved by bisection and differentiated implicitly; local states, jumps and integration use chain-rule derivatives without finite-difference steps. The independent finite-difference check re-solves shock position.

Q(ξ)=∫0ξj−(x,ξ) dx+∫ξ1j+(x,ξ) dx,Qξ=∫0ξ∂ξj−dx+∫ξ1∂ξj+dx+j−(ξ)−j+(ξ).Q(\xi)=\int_0^\xi j^-(x,\xi)\,dx+\int_\xi^1j^+(x,\xi)\,dx,\quad Q_\xi=\int_0^\xi\partial_\xi j^-dx+\int_\xi^1\partial_\xi j^+dx+j^-(\xi)-j^+(\xi).

可选压力积分 j=p 或壁面推力 j=pA′。表格单独显示界面位移项与光滑区域贡献,并给出漏掉界面项后的错误梯度。积分在喉部和激波处分段;改变积分面板数可区分求积误差与求根误差。质量流率、跳跃残差,以及推力与端面动量通量差提供另外的守恒检查。

Choose pressure integral j=p or wall thrust j=pA′. The table separates interface motion from smooth-region contributions and reports the wrong gradient obtained by omitting the interface term. Quadrature is split at the throat and shock; changing its panel count separates quadrature from root-solving error. Mass flow, jump residuals and thrust versus end momentum flux provide additional conservation checks.

适定性在这里可直接观察:局部唯一位置要求 G_ξ≠0,位置敏感度为 1/G_ξ。把激波推近喉部时,激波变弱,出口压力对位置的区分能力下降,位置导数可能很大。较小差分步长只能减小截断误差,不能消除这种物理条件数;本实验也没有证明分支以外或任意流道中的唯一性。

Well-posedness is visible here: a locally unique position requires G_ξ≠0, and its sensitivity is 1/G_ξ. As the shock approaches the throat it weakens, outlet pressure distinguishes positions less strongly, and position sensitivity can become large. Smaller finite-difference steps reduce truncation error but cannot remove this physical conditioning. The experiment does not establish uniqueness outside this branch or for arbitrary ducts.

0.60.70.80.90.50.751ξP(ξ)G_ξ = -0.5966近喉部 G_ξ → 0near the throat G_ξ → 0(a) 出口压力 P(ξ)(a) outlet pressure P(ξ)01230.50.751ξdQ/dp_b压力积分pressure integral壁面推力wall thrust(b) 实线 −ψ,虚线漏掉 j⁻ − j⁺(b) solid −ψ, dashed without j⁻ − j⁺p_bG(ξ, p_b) = 0Q(ξ)∂G/∂p_b = −1dξ/dp_b = 1/G_ξψ = −Q_ξ/G_ξ,dQ/dp_b = −ψ,Q_ξ = 光滑区贡献 + (j⁻ − j⁺)ψ = −Q_ξ/G_ξ, dQ/dp_b = −ψ, Q_ξ = smooth-region part + (j⁻ − j⁺)
图 B.4 · B.5 的约化问题。(a) 出口压力 P(ξ) 随激波位置单调下降,斜率 G_ξ 就是位置的“刚度”;越靠近喉部越平,1/G_ξ 越大,所以激波位置对背压越敏感。(b) 由约化伴随得到的 dQ/dpb\mathrm dQ/\mathrm dp_b(实线)与漏掉界面项 j⁻ − j⁺ 的错误梯度(虚线)。在 ξ = 0.56,压力积分的正确值为 2.610,漏项后只有 0.665:靠近喉部时导数主要来自激波移动,而不是光滑区域。下方是约化系统的依赖链。数据由 B.5 的代码(每段 96 面板)逐点计算。Figure B.4 · The reduced problem of B.5. (a) The outlet pressure P(ξ) falls monotonically with shock position; its slope G_ξ is the "stiffness" of the position. It flattens towards the throat, where 1/G_ξ grows and the shock position becomes more sensitive to the back pressure. (b) dQ/dpb\mathrm dQ/\mathrm dp_b from the reduced adjoint (solid) and the wrong gradient without the interface term j⁻ − j⁺ (dashed). At ξ = 0.56 the pressure-integral value is 2.610, but only 0.665 without the term: near the throat the derivative comes mostly from the moving shock, not from the smooth regions. Below: the dependency chain of the reduced system. Data computed pointwise by the B.5 code (96 panels per segment).
展开独立激波拟合与约化伴随的完整数值实现Complete numerical implementation of independent shock fitting and reduced adjoint
/* Independent shock-fitted isentropic nozzle. Reduced scalar adjoint after analytic elimination of the smooth flow. */
(function(root){'use strict';
const dual=(v,d=0)=>[v,d],add=(a,b)=>[a[0]+b[0],a[1]+b[1]],neg=a=>[-a[0],-a[1]],sub=(a,b)=>add(a,neg(b)),mul=(a,b)=>[a[0]*b[0],a[1]*b[0]+a[0]*b[1]],div=(a,b)=>[a[0]/b[0],(a[1]*b[0]-a[0]*b[1])/(b[0]*b[0])],pow=(a,k)=>[a[0]**k,k*a[0]**(k-1)*a[1]],scale=(a,k)=>[k*a[0],k*a[1]];
const area=x=>add(dual(1),scale(pow(sub(x,dual(.5)),2),2.2)),areaD=x=>scale(sub(x,dual(.5)),4.4),ratio=m=>(5/6*(1+.2*m*m))**3/m;
function mach(r,superSonic){if(r[0]<1-1e-11)throw Error('Area below sonic area');if(Math.abs(r[0]-1)<1e-13){if(Math.abs(r[1])>1e-10)throw Error('Singular sonic derivative');return dual(1);}let lo=superSonic?1:1e-7,hi=superSonic?20:1;for(let k=0;k<75;k++){const m=(lo+hi)/2;if((ratio(m)<r[0])===superSonic)lo=m;else hi=m;}const m=(lo+hi)/2,der=ratio(m)*(1.2*m/(1+.2*m*m)-1/m);return dual(m,r[1]/der);}
function state(M,p0){const T=div(dual(1),add(dual(1),scale(pow(M,2),.2))),p=mul(p0,pow(T,3.5)),rho=div(p,T),u=mul(M,pow(scale(T,1.4),.5)),energy=add(scale(p,2.5),scale(mul(rho,pow(u,2)),.5));return {M,p,rho,u,U:[rho,mul(rho,u),energy],F:[mul(rho,u),add(mul(rho,pow(u,2)),p),mul(add(energy,p),u)]};}
function at(position,kind='pressure',panels=96){if(!(position>.5&&position<1))throw Error('Shock must lie inside divergent branch');if(!['pressure','thrust'].includes(kind)||panels<8||panels%2)throw Error('Invalid shock objective/quadrature');const xi=dual(position,1),As=area(xi),M1=mach(As,true),left=state(M1,dual(1)),M2=pow(div(add(dual(1),scale(pow(M1,2),.2)),sub(scale(pow(M1,2),1.4),dual(.2))),.5),p2=mul(left.p,add(dual(1),scale(sub(pow(M1,2),dual(1)),7/6))),p02=mul(p2,pow(add(dual(1),scale(pow(M2,2),.2)),3.5)),astar2=div(dual(1),p02),right=state(M2,p02),flow=(x,downstream)=>state(mach(div(area(x),downstream?astar2:dual(1)),!downstream&&x[0]>.5),downstream?p02:dual(1)),exit=flow(dual(1),true),entry=flow(dual(0),false),integrand=(x,downstream)=>{const p=flow(x,downstream).p;return kind==='pressure'?p:mul(p,areaD(x));};
function integrate(a,b,downstream){const h=scale(sub(b,a),1/panels);let total=dual(0);for(let i=0;i<=panels;i++){const x=add(a,scale(h,i)),w=i===0||i===panels?1:i%2?4:2;total=add(total,scale(integrand(x,downstream),w));}return scale(mul(total,h),1/3);}
const J=add(add(integrate(dual(0),dual(.5),false),integrate(dual(.5),xi,false)),integrate(xi,dual(1),true)),jump=kind==='pressure'?left.p[0]-right.p[0]:(left.p[0]-right.p[0])*areaD(xi)[0],points=[];
for(const [a,b,down]of [[0,.5,false],[.5,position,false],[position,1,true]])for(let i=0;i<=32;i++){const x=a+(b-a)*i/32,q=flow(dual(x),down);points.push({x,side:down?'downstream':'upstream',M:q.M[0],p:q.p[0],rho:q.rho[0],u:q.u[0]});}
const mismatch=left.F.map((f,k)=>f[0]-right.F[k][0]),momentum= sub(mul(area(dual(1)),exit.F[1]),mul(area(dual(0)),entry.F[1]));return {xi:position,pb:exit.p[0],Gxi:exit.p[1],J:J[0],Jxi:J[1],jump,field:J[1]-jump,p02:p02[0],Mminus:M1[0],Mplus:M2[0],RH:Math.hypot(...mismatch),massError:Math.abs(area(dual(1))[0]*exit.F[0][0]-area(dual(0))[0]*entry.F[0][0]),momentum:momentum[0],points};}
function bounds(){return [at(.98).pb,at(.52).pb].sort((a,b)=>a-b);}
function solve(pb,kind='pressure',panels=96){const a0=at(.52,kind,panels),b0=at(.98,kind,panels);if(!(pb>Math.min(a0.pb,b0.pb)&&pb<Math.max(a0.pb,b0.pb)))throw Error('Back pressure is outside the interior-shock bracket');let a=.52,b=.98,fa=a0.pb-pb;for(let i=0;i<52;i++){const x=(a+b)/2,fx=at(x,kind,8).pb-pb;if(fa*fx>0){a=x;fa=fx;}else b=x;}const r=at((a+b)/2,kind,panels);if(Math.abs(r.Gxi)<1e-9)throw Error('Shock-position sensitivity is singular');const tangent=1/r.Gxi,psi=-r.Jxi/r.Gxi;return {...r,residual:r.pb-pb,tangent,psi,gradient:-psi};}
const api={at,solve,bounds,area:x=>area(dual(x))[0]};if(typeof module!=='undefined')module.exports=api;else root.QShockFit=api;
})(typeof window!=='undefined'?window:globalThis);

B.6 完整激波捕捉离散 tangent 与 adjoint

B.6 Shock-capturing discrete tangent and adjoint

本节新增独立的格心有限体积流道;格点页面也展示同一参考实现。几何和边界与 B.5 相同,但未知流场由守恒残差求解,激波位置没有作为约束。拟合解只用于初值和独立验证。它完整求导所选离散残差;不宣称实现任意激波相互作用或完整连续 Euler 伴随场。

This independent cell-centred finite-volume duct is displayed on both page variants. It uses B.5 geometry and boundaries, but solves conservative residuals without constraining shock position. Shock fitting supplies initialization and independent validation only. It differentiates the selected discrete residual completely; arbitrary shock interactions and a full continuous Euler adjoint field are not claimed.

Ri=Ai+1/2F^i+1/2−Ai−1/2F^i−1/2−[0∫IipAx dx0],A(x)=1+2.2(x−0.5)2.R_i=A_{i+1/2}\widehat F_{i+1/2}-A_{i-1/2}\widehat F_{i-1/2}-\begin{bmatrix}0\\\int_{I_i}pA_x\,dx\\0\end{bmatrix},\quad A(x)=1+2.2(x-0.5)^2.

面积在网格顶点取值并分片线性插值。二阶在无量纲原始变量 (ρ,u,p) 上逐分量限幅,再转换回守恒变量;体积分使用相同重构。数值通量采用固定耗散速度 α=3,所有重构面状态必须满足 |u|+c<3、ρ>0.01、p>0.01、u>0;失败会拒绝更新,未使用改变残差的正性裁剪。表中出口与入口的比熵差是物理诊断,不构成对整个二阶格式离散熵不等式的证明。未满足波速上界时,本模型不适用。

Area is sampled at mesh vertices and interpolated piecewise linearly. Second order limits each nondimensional primitive component (ρ,u,p), converts to conservative states, and uses the same reconstruction for volume integration. The flux has fixed dissipation speed α=3. All reconstructed traces must satisfy |u|+c<3, ρ>0.01, p>0.01 and u>0; invalid updates are rejected without residual-altering positivity clipping. The displayed outlet-minus-inlet entropy is a physical diagnostic, not a proof of a discrete entropy inequality for the second-order scheme. Outside that wave-speed bound this model does not apply.

F^(UL,UR)=12(FL+FR)−α2(UR−UL),Jhv=−Rpb,JhTψ=−QUT.\widehat F(U_L,U_R)=\tfrac12(F_L+F_R)-\tfrac\alpha2(U_R-U_L),\qquad J_hv=-R_{p_b},\quad J_h^T\psi=-Q_U^T.

入口给总压与总温,保留来自内部的出射信息;出口给背压。这一闭合要求入口、出口均处于正向亚声速分支,内部允许超声速。Newton 使用一阶块 GS 预处理 GMRES,必要时增加伪时间对角项并回溯;最终以未加移位的真实守恒残差判断收敛。非线性迭代轨迹不是稳态伴随的求导对象。

The inlet prescribes total pressure and temperature while retaining outgoing interior information; the outlet prescribes back pressure. Both boundaries must remain on positive-flow subsonic branches, while interior states may be supersonic. Newton uses first-order block-GS-preconditioned GMRES, with pseudo-time diagonal shifts and backtracking when needed. Convergence uses the unshifted conservative residual. The nonlinear iteration trajectory is not the differentiated object of the steady adjoint.

reconstruct primitive variables, including limiter derivatives
convert reconstructed traces to conservative variables
scatter conservative face fluxes and integrate p*dA/dx
solve R(U,p_b)=0 using damped Newton / block-GS GMRES
record the exact residual graph at the converged U
apply J*U_d forward; apply J^T*R_bar by reverse accumulation
solve tangent and adjoint using the same exact linearization
assemble J only for the independent LU diagnostic
re-solve R(U,p_b±h)=0 for every finite-difference step

完整导数包括边界闭合、原始变量转换、limiter、数值通量、面积源项和目标积分。所有面积节点、入口总量和背压均进入计算图;下方逐步演示背压方向。有限差分使用完整原始模型,冻结 limiter 对照若产生梯度偏差应被保留。粗网格一阶耗散可能抹掉整个超声速区域;此时仅能称为已收敛的粗网格离散解。

The derivative includes boundary closure, variable conversion, limiting, fluxes, area sources and objective integration. Every area node, inlet total and back pressure enters the graph; the panel demonstrates the back-pressure direction. Finite differences re-solve the full primal model, retaining any bias from the frozen-limiter comparison. Coarse first-order dissipation may erase the supersonic region entirely; such a result is only a converged coarse discrete solution.

这里的压力积分与壁面推力分别为 ∫p dx 和 ∫pA′dx。离散梯度与全链路差分一致,只证明固定网格求导正确;与 B.5 梯度的误差需要另做加密研究。不要用最大梯度单元的索引作为可微激波位置目标。

The objectives are ∫p dx and wall thrust ∫pA′dx. Agreement with full-rerun differences verifies fixed-grid differentiation only; error against B.5 needs a separate refinement study. The index of the maximum-gradient cell is not a differentiable shock-location objective.

B.6(续)加密到 768 单元:与连续伴随比较

B.6 (continued) Refinement to 768 cells, against the continuous adjoint

上面的 24/48/96 网格不足以判断极限,下图把同一个捕捉格式在 pbp_b = 0.75 加密到 768 个单元,并与 B.3(续)由内部激波条件得到的连续伴随比较(该背压下 ξ = 0.8565,压力积分 dQ/dpb\mathrm dQ/\mathrm dp_b = 0.6585252175,推力 1.0809366136)。伴随与 tangent 在每个网格上都用精确的 matrix-free 算子和 GMRES 求解(页面的稠密 LU 只用于上面的小网格)。这是研究性探索:它说明这个格式在此算例上的梯度随网格加密接近连续值,但不证明一般收敛或收敛阶。

The 24/48/96 grids above are too coarse to judge a limit. The figures below refine the same capturing scheme at pbp_b = 0.75 to 768 cells and compare it with the continuous adjoint obtained from the internal shock conditions in B.3 (continued) (at this back pressure ξ = 0.8565, pressure integral dQ/dpb\mathrm dQ/\mathrm dp_b = 0.6585252175, thrust 1.0809366136). On every grid the adjoint and the tangent are solved with the exact matrix-free operators and GMRES (the page's dense LU is only for the small grids above). This is research exploration: it shows the scheme's gradient approaching the continuous value under refinement for this case, and proves neither general convergence nor a rate.

00.511.5M(a) 马赫数:黑线为拟合解(ξ = 0.856),蓝线为二阶捕捉 N = 48、96、192、384(a) Mach number: black, the fitted solution (ξ = 0.856); blue, second-order capturing, N = 48, 96, 192, 384-2-1.5-1-0.5000.250.50.751xλ₂, −ψ_h,₂(b) 动量分量:黑线为连续伴随 λ₂(仅喉部下游),红线为离散 −ψ_h,₂(b) momentum component: black, the continuous λ₂ (downstream of the throat only); red, the discrete −ψ_h,₂λ₂(ξ) = -0.638
图 B.5 · B.6 捕捉格式与连续伴随的对照(pbp_b = 0.75,研究性探索,离线数据)。(a) 马赫数:加密后捕捉到的超声速区与激波逐渐接近拟合解。(b) 压力积分的伴随动量分量:离散 −ψh\psi_h(积分残差与 Q + ψTR 约定,附录 A.6)与由内部激波条件得到的 λ₂。在 0.62 ≤ x ≤ 0.95、离激波 0.06 以外,两者的最大差在 N = 96、192、384 时为 0.085、0.044、0.020;激波附近的过渡带随网格变窄。出口最后 2 个单元里 −ψh\psi_h 明显偏离 λ:这是弱出口条件造成的离散伴随边界层,单元数不随加密增加,宽度与 h 成正比,不妨碍梯度收敛(图 B.6)。连续伴随只画到喉部下游,那里 λ 有对数奇点,离散值接近得较慢。数据由 shock_capture_study.js 用 B.6 的代码离线计算。Figure B.5 · The B.6 capturing scheme against the continuous adjoint (pbp_b = 0.75; research exploration, offline data). (a) Mach number: under refinement the captured supersonic region and shock approach the fitted solution. (b) Momentum component of the pressure-integral adjoint: the discrete −ψh\psi_h (integrated residuals and the Q + ψTR convention, Appendix A.6) against λ₂ from the internal shock conditions. For 0.62 ≤ x ≤ 0.95 and more than 0.06 from the shock, the largest difference is 0.085, 0.044 and 0.020 for N = 96, 192 and 384; the transition at the shock narrows with the mesh. In the last 2 cells before the outlet −ψh\psi_h departs clearly from λ: a discrete adjoint boundary layer caused by the weak outlet condition, with a fixed number of cells and so a width proportional to h, which does not stop the gradient converging (Figure B.6). The continuous adjoint is drawn downstream of the throat only; it is logarithmically singular there, and the discrete values approach it more slowly. Computed offline with the B.6 code by shock_capture_study.js.
10⁻³10⁻²10⁻¹N|dQ/dp_b − 连续值||dQ/dp_b − continuous|244896192384768∝ 1/N一阶,压力积分first order, pressure一阶,推力first order, thrust二阶,压力积分second order, pressure二阶,推力second order, thrust
图 B.6 · B.6 捕捉格式离散伴随梯度 dQ/dpb\mathrm dQ/\mathrm dp_b 相对连续值(内部激波条件,pbp_b = 0.75:压力积分 0.6585252175,推力 1.0809366136)的误差。一阶格式在 192 单元以后每加密一倍误差约减半:压力积分 2.6×10⁻²、1.1×10⁻²、5.1×10⁻³。二阶 minmod 在粗网格上不单调(N = 24 的小误差是偶然抵消),加密后继续减小,但远不到二阶:压力积分在 N = 384、768 为 1.6×10⁻³、1.3×10⁻³(实测阶 0.32),推力为 −2.1×10⁻³、−1.1×10⁻³(实测阶 1.01);激波处限幅退化为一阶。每个网格上 tangent 与 adjoint 的梯度一致到 10⁻¹⁰ 以内。Figure B.6 · Error of the B.6 capturing scheme's discrete-adjoint gradient dQ/dpb\mathrm dQ/\mathrm dp_b against the continuous value (internal shock conditions, pbp_b = 0.75: pressure integral 0.6585252175, thrust 1.0809366136). From 192 cells on, first order roughly halves the error with each refinement: pressure integral 2.6×10⁻², 1.1×10⁻², 5.1×10⁻³. Second-order minmod is non-monotone on coarse grids (the small error at N = 24 is a chance cancellation) and keeps decreasing under refinement, but far below second order: pressure integral 1.6×10⁻³ and 1.3×10⁻³ at N = 384 and 768 (measured rate 0.32), thrust −2.1×10⁻³ and −1.1×10⁻³ (measured rate 1.01); the limiter drops to first order at the shock. On every grid the tangent and adjoint gradients agree to within 10⁻¹⁰.
查看完整激波捕捉实现及验证运算Complete shock-capturing implementation and verification operations
/* Independent shock-capturing cell-volume Euler. Primitive reconstruction, fixed LLF speed 3, full residual AD. Newton uses block-GS preconditioned GMRES; optional LU is an independent small-system check. */
(function(root){'use strict';const E=typeof module!=='undefined'?require('./numerics'):root.EduNumerics,S=typeof module!=='undefined'?require('../q1d/reconstruction/shock_fitting'):root.QShockFit;const Q=typeof module!=='undefined'?require('../q1d/reconstruction/math'):root.QMath,gamma=1.4,zeros=n=>Array(n).fill(0),{real,complex,tape,gmres,dot,norm}=Q;
function solve3(A,b){const a=A.map((r,i)=>[...r,b[i]]);for(let k=0;k<3;k++){let p=k;for(let i=k+1;i<3;i++)if(Math.abs(a[i][k])>Math.abs(a[p][k]))p=i;if(Math.abs(a[p][k])<1e-15)throw Error('Singular 3x3 block');[a[p],a[k]]=[a[k],a[p]];for(let i=k+1;i<3;i++){const s=a[i][k]/a[k][k];for(let j=k;j<4;j++)a[i][j]-=s*a[k][j];}}const x=zeros(3);for(let i=2;i>=0;i--){let s=a[i][3];for(let j=i+1;j<3;j++)s-=a[i][j]*x[j];x[i]=s/a[i][i];}return x;}
function model({node=false,n=24,order=1,solver='gmres',objective='thrust',pb=.75,p0=1,T0=1,omega=.7,depth=.3,limiter='minmod',epsilon=.01,derivativeMode='full',sweeps=12}={}){
 if(!['pressure','thrust'].includes(objective))throw Error('Choose pressure or thrust');if(node)throw Error('Shock experiment uses cell-centred control volumes on both pages');const m=node?n+1:n,nd=3*m,h=1/n,x=Array.from({length:m},(_,i)=>(i+(node?0:.5))*h),ax=Array.from({length:n+1},(_,i)=>i*h),baseA=ax.map(x=>1+2.2*(x-.5)**2),params=[...baseA,pb,p0,T0],faceX=node?[0,...Array.from({length:n},(_,i)=>(i+.5)*h),1]:ax.slice(),faces=faceX.map((xf,i)=>({x:xf,L:i-1,R:i===m?-1:i})),constraints=node?[0,1,3*(m-1)]:[];
 function physics(o){const {c,add,sub,mul,div,pow}=o;
 const prim=U=>{const rho=U[0],u=div(U[1],rho),p=mul(c(.4),sub(U[2],mul(c(.5),mul(U[1],u)))),sound=pow(div(mul(c(gamma),p),rho),.5);return {rho,u,p,a:sound,T:div(p,rho)};};
 const cons=(r,u,p)=>[r,mul(r,u),add(div(p,c(.4)),mul(mul(c(.5),r),mul(u,u)))];
 const flux=U=>{const q=prim(U);return [U[1],add(mul(U[1],q.u),q.p),mul(add(U[2],q.p),q.u)];};
 const totals=U=>{const q=prim(U),factor=add(c(1),mul(c(.2),div(mul(q.u,q.u),mul(q.a,q.a))));return {p0:mul(q.p,pow(factor,3.5)),T0:mul(q.T,factor)};};
 function inlet(U,pt,tt){const q=prim(U),rm=sub(q.u,mul(c(5),q.a)),a1=mul(c(5),rm),a0=sub(mul(c(.5),mul(rm,rm)),mul(c(3.5),tt)),sound=div(add(mul(c(-1),a1),pow(sub(mul(a1,a1),mul(c(60),a0)),.5)),c(30)),u=add(rm,mul(c(5),sound)),M=div(u,sound),p=mul(pt,pow(add(c(1),mul(c(.2),mul(M,M))),-3.5)),rho=div(mul(c(gamma),p),mul(sound,sound));return cons(rho,u,p);}
 function outlet(U,back){const q=prim(U),entropy=div(q.p,pow(q.rho,gamma)),rho=pow(div(back,entropy),1/gamma),sound=pow(div(mul(c(gamma),back),rho),.5),u=sub(add(q.u,mul(c(5),q.a)),mul(c(5),sound));return cons(rho,u,back);}
 function numerical(L,R,A){const l=prim(L),r=prim(R),lam=c(3),fl=flux(L),fr=flux(R);return {H:fl.map((v,k)=>mul(A,sub(mul(c(.5),add(v,fr[k])),mul(mul(c(.5),lam),sub(R[k],L[k]))))),lam};}
 return {prim,cons,flux,totals,inlet,outlet,numerical};}
 function evaluate(o,U,b,q=order,withObjective=true){const {c,add,sub,mul}=o,ph=physics(o),A=b.slice(0,n+1),back=b[n+1],pt=b[n+2],tt=b[n+3],R=Array.from({length:nd},()=>c(0)),local=[],sources=[],quadrature=[];
 const val=o.value||(z=>z),ops={...o,value:val};
 const reconstruct=(V,i,xx)=>{if(q===1)return V.slice(3*i,3*i+3);const center=ph.prim(V.slice(3*i,3*i+3)),wi=[center.rho,center.u,center.p],d=(xx-x[i])/h;let slope=wi.map(()=>c(0));if(i>0&&i<m-1){const l=ph.prim(V.slice(3*(i-1),3*i)),r=ph.prim(V.slice(3*(i+1),3*(i+2))),wl=[l.rho,l.u,l.p],wr=[r.rho,r.u,r.p];slope=wi.map((v,k)=>E.slope(ops,sub(v,wl[k]),sub(wr[k],v),limiter,epsilon,derivativeMode));}const v=wi.map((v,k)=>add(v,mul(c(d),slope[k])));return ph.cons(...v); },area=xx=>{const k=Math.min(n-1,Math.floor(xx/h)),t=(xx-ax[k])/h;return add(mul(c(1-t),A[k]),mul(c(t),A[k+1]));};
 for(const f of faces){let L=f.L>=0?reconstruct(U,f.L,f.x):null,Rr=f.R>=0?reconstruct(U,f.R,f.x):null;const af=area(f.x);let z;if(node&&(f.L<0||f.R<0)){const state=f.L<0?Rr:L;z={H:ph.flux(state).map(v=>mul(af,v)),lam:c(0)};L=state;Rr=state;}else{if(f.L<0)L=ph.inlet(Rr,pt,tt);if(f.R<0)Rr=ph.outlet(L,back);z=ph.numerical(L,Rr,af);}z.H.forEach((v,k)=>{if(f.L>=0)R[3*f.L+k]=add(R[3*f.L+k],v);if(f.R>=0)R[3*f.R+k]=sub(R[3*f.R+k],v);});local.push({...z,L,R:Rr,A:af});}
 let J=c(0);
 for(let i=0;i<m;i++){let source=c(0),inv=c(0);const lo=faceX[i],hi=faceX[i+1],breaks=node&&i>0&&i<m-1?[lo,x[i],hi]:[lo,hi],qs=[];
 for(let k=1;k<breaks.length;k++){const left=breaks[k-1],right=breaks[k],mid=(left+right)/2,half=(right-left)/2,ai=Math.min(n-1,Math.floor(mid/h)),dA=mul(c(1/h),sub(A[ai+1],A[ai]));for(const z of [-1/Math.sqrt(3),1/Math.sqrt(3)]){const xx=mid+half*z,u=reconstruct(U,i,xx),p=ph.prim(u).p,af=area(xx),sc=mul(c(half),mul(p,dA));source=add(source,sc);let pref=null,objectiveContribution=objective==='thrust'?sc:mul(c(half),p);if(objective==='pressure')inv=add(inv,mul(c(half),p));qs.push({x:xx,w:half,p,A:af,dA,contribution:sc,pref,objectiveContribution});}}
 R[3*i+1]=sub(R[3*i+1],source);sources.push(source);quadrature.push(qs);if(withObjective)J=add(J,objective==='thrust'?source:inv);}
 const raw=R.slice();if(node){const q0=ph.totals(U.slice(0,3)),qe=ph.prim(U.slice(-3));R[0]=sub(q0.p0,pt);R[1]=sub(q0.T0,tt);R[3*(m-1)]=sub(qe.p,back);}return {R,J,raw,local,sources,quadrature};}
 function linearize(U,b=params,q=order,withObjective=true){const t=tape();t.op.value=i=>t.val[i];const u=U.map(t.op.input),p=b.map(t.op.input),out=evaluate(t.op,u,p,q,withObjective),rv=ids=>ids.map(i=>t.val[i]);return {R:rv(out.R),J:t.val[out.J],jvp:(v,dp=zeros(b.length))=>{const d=t.forward([...v,...dp]);return {R:out.R.map(i=>d[i]),raw:out.raw.map(i=>d[i]),J:d[out.J],faces:out.local.map(f=>f.H.map(i=>d[i])),local:out.local.map(f=>({L:f.L.map(i=>d[i]),R:f.R.map(i=>d[i]),A:d[f.A],lam:d[f.lam],H:f.H.map(i=>d[i])})),sources:out.sources.map(i=>d[i])};},vjp:(w,bJ=0)=>{const v=t.reverse([...out.R,out.J],[...w,bJ]);return {U:v.slice(0,nd),p:v.slice(nd)};},faceVjp:(i,w)=>{const z=t.reverse(out.local[i].H,w);return {U:z.slice(0,nd),p:z.slice(nd)};},sourceVjp:(i,w)=>{const z=t.reverse([out.sources[i]],[w]);return {U:z.slice(0,nd),p:z.slice(nd)};}};}
 function firstOrderMatrix(U,b=params){const L=linearize(U,b,1,false),rows=Array.from({length:nd},()=>zeros(nd));for(let j=0;j<nd;j++){const e=zeros(nd);e[j]=1;const v=L.jvp(e).R;v.forEach((x,i)=>rows[i][j]=x);}return rows;}
 function sweep(P,rhs,transpose=false){const out=zeros(nd),get=(i,j)=>transpose?P[j][i]:P[i][j];for(let it=0;it<m;it++){const i=transpose?m-1-it:it,b=rhs.slice(3*i,3*i+3),block=Array.from({length:3},(_,k)=>[0,1,2].map(l=>get(3*i+k,3*i+l)));for(let j=transpose?i+1:Math.max(0,i-1);transpose?j<Math.min(m,i+2):j<i;j++)for(let k=0;k<3;k++)for(let l=0;l<3;l++)b[k]-=get(3*i+k,3*j+l)*out[3*j+l];const v=solve3(block,b);v.forEach((x,k)=>out[3*i+k]=x);}return out;}
 function GS(P,rhs,transpose=false){let v=zeros(nd);for(let it=0;it<sweeps;it++){const r=rhs.map((x,i)=>x-(()=>{let s=0;const block=Math.floor(i/3);for(let j=3*Math.max(0,block-1);j<3*Math.min(m,block+2);j++)s+=(transpose?P[j][i]:P[i][j])*v[j];return s;})()),d=sweep(P,r,transpose);v=v.map((x,i)=>x+d[i]);}return v;}
 function valid(U,b=params){const out=evaluate(real,U,b,order,false),states=Array.from({length:m},(_,i)=>U.slice(3*i,3*i+3)).concat(out.local.flatMap(f=>[f.L,f.R]));const ends=[out.local[0].L,out.local[0].R,out.local.at(-1).L,out.local.at(-1).R];return ends.every(v=>{const z=physics(real).prim(v);return z.u/z.a<1;})&&b.slice(0,n+1).every(x=>x>.2)&&states.every(U=>{const q=physics(real).prim(U);return q.rho>.01&&q.p>.01&&q.u>0&&Math.abs(q.u)+q.a<3;});}
 function initial(b=params){const reference=S.solve(b[n+1]),pts=reference.points;return x.map(xx=>{let k=0;while(k<pts.length-2&&pts[k+1].x<xx)k++;const a=pts[k],z=pts[k+1],t=z.x===a.x?0:(xx-a.x)/(z.x-a.x);return physics(real).cons(a.rho+t*(z.rho-a.rho),a.u+t*(z.u-a.u),a.p+t*(z.p-a.p));}).flat();}
 function linearSolve(L,rhs,P,trans=false,method=solver,tol=2e-12){const op=v=>trans?L.vjp(v).U:L.jvp(v).R,pre=v=>GS(P,v,trans);if(method==='gmres')return gmres(op,rhs,pre,tol,nd);let v=zeros(nd),history=[],states=[];for(let it=0;it<12000;it++){const av=op(v),r=rhs.map((x,i)=>x-av[i]),rn=norm(r);history.push(rn);states.push(v.slice());if(rn<tol)return {x:v,residual:rn,history,states,converged:true};const d=pre(r);v=v.map((x,i)=>x+omega*d[i]);if(!Number.isFinite(norm(v))||norm(v)>1e10)break;}return {x:v,history,states,residual:history.at(-1),converged:false};}
 function solve(b=params,start,method='gmres',tol=2e-13){let U=start?start.slice():initial(b),history=[],states=[];for(let it=0;it<120;it++){const L=linearize(U,b,order,false),rn=norm(L.R);history.push(rn);states.push({it,U:U.slice(),r:rn});if(rn<tol){if(!valid(U,b))throw Error('Nonphysical state or fixed wave-speed bound exceeded');return {U,history,states,converged:true};}const A=method==='lu'?E.assemble(L,nd):null,P=method==='lu'?null:firstOrderMatrix(U,b);let accepted=false;for(const shift of [0,.1,1,10,100]){let d;try{if(method==='lu'){const M=A.map((r,i)=>r.map((v,j)=>v+(i===j?shift*h:0)));d=E.solve(E.factor(M),L.R.map(v=>-v));}else{const PS=P.map((r,i)=>r.map((v,j)=>v+(i===j?shift*h:0))),v=gmres(z=>L.jvp(z).R.map((v,i)=>v+shift*h*z[i]),L.R.map(v=>-v),z=>GS(PS,z),Math.min(1e-12,rn*.01),nd);if(!v.converged)continue;d=v.x;}}catch(e){continue;}for(let step=1;step>1e-7;step*=.5){const next=U.map((v,i)=>v+step*d[i]);if(valid(next,b)&&norm(evaluate(real,next,b,order,false).R)<(1-1e-4*step)*rn){U=next;accepted=true;break;}}if(accepted)break;}if(!accepted)return {U,history,states,converged:false,reason:'No admissible residual-decreasing update'};}return {U,history,states,converged:false,reason:'Nonlinear iteration limit'};}
 return {node,n,m,nd,h,depth,sweeps,x,ax,baseA,params,faceX,faces,order,solver,objective,constraints,physics:physics(real),evaluate:(U,b=params)=>evaluate(real,U,b),linearize,firstOrderMatrix,GS,linearSolve,valid,initial,solve};
}
const api={model,solve3,gamma,...Q};if(typeof module!=='undefined')module.exports=api;else root.QShockEuler=api;
})(typeof window!=='undefined'?window:globalThis);

(function(root){'use strict';const E=typeof module!=='undefined'?require('./euler'):root.QShockEuler,N=typeof module!=='undefined'?require('./numerics'):root.EduNumerics,S=typeof module!=='undefined'?require('../q1d/reconstruction/shock_fitting'):root.QShockFit;
function branches(f,U,kind,eps){const q=f.x.map((_,i)=>f.physics.prim(U.slice(3*i,3*i+3))),out=[];for(let i=1;i<f.m-1;i++)for(const key of ['rho','u','p'])out.push(N.limiter(q[i][key]-q[i-1][key],q[i+1][key]-q[i][key],kind,eps).branch);return out;}
function analyse(f,s,{mode='full',kind='minmod',eps=.01,parameter=f.n+1}={}){if(!s.converged)throw Error('Steady primal not converged');const g=E.model({n:f.n,order:f.order,limiter:kind,epsilon:eps,objective:f.objective,pb:f.params[f.n+1],derivativeMode:mode}),L=g.linearize(s.U),n=f.nd,A=N.assemble(L,n),AT=N.transpose(A),j=L.vjp(N.zeros(n),1),psi=N.solve(N.factor(AT),j.U.map(v=>-v)),dp=N.zeros(f.params.length);dp[parameter]=1;const tangent=N.solve(N.factor(A),L.jvp(N.zeros(n),dp).R.map(v=>-v)),gradient=L.vjp(psi,1).p[parameter],P=f.firstOrderMatrix(s.U),rhs=j.U.map(v=>-v),gm=N.gmres(v=>L.vjp(v).U,rhs,v=>f.GS(P,v,true),1e-10,n),tg=N.gmres(v=>L.jvp(v).R,L.jvp(N.zeros(n),dp).R.map(v=>-v),v=>f.GS(P,v),1e-10,n);let x=N.zeros(n),history=[];for(let k=0;k<180;k++){const ax=L.vjp(x).U,r=rhs.map((b,i)=>b-ax[i]),rn=N.norm(r);history.push(rn);if(rn<1e-10||!Number.isFinite(rn)||rn>1e8)break;const d=f.GS(P,r,true);x=x.map((v,i)=>v+.7*d[i]);}const w=Array.from({length:n},(_,i)=>Math.cos(i*.71)),v=w.map((_,i)=>Math.sin(i*.33));return {L,psi,tangent,gradient,tangentGradient:L.jvp(tangent,dp).J,gm,tg,history,luResidual:N.norm(L.vjp(psi,1).U),dotError:Math.abs(N.dot(w,L.jvp(v).R)-N.dot(v,L.vjp(w).U)),reference:S.solve(f.params[f.n+1],f.objective)};}
function fd(f,s,k=f.n+1,steps=[1e-3,1e-4,1e-5,1e-6],kind='minmod',eps=.01){kind=f.order===1?'first':kind;const base=branches(f,s.U,kind,eps),J=f.evaluate(s.U).J;return steps.map(h=>{const p=f.params.slice(),m=p.slice();p[k]+=h;m[k]-=h;const a=f.solve(p,s.U),b=f.solve(m,s.U);if(!a.converged||!b.converged)return {h,error:'Perturbed primal not converged'};const ja=f.evaluate(a.U,p).J,jb=f.evaluate(b.U,m).J;return {h,forward:(ja-J)/h,backward:(J-jb)/h,central:(ja-jb)/(2*h),residual:Math.max(a.history.at(-1),b.history.at(-1)),switches:[a,b].map(z=>branches(f,z.U,kind,eps).filter((v,i)=>v!==base[i]).length)};});}
const api={analyse,fd,branches};if(typeof module!=='undefined')module.exports=api;else root.ShockStudy=api;
})(typeof window!=='undefined'?window:globalThis);

B.7 Giles–Pierce 的四类解析伴随解

B.7 The four Giles–Pierce analytic adjoint families

这里按 Giles–Pierce(JFM 426, 2001,§§3–7)的 Green 函数构造完整呈现压力积分目标的四类解:全超声速、全亚声速、无激波跨声速、含激波跨声速。令 x∈[−1,1],面积 A(x)>0 可由调用者给出;图示使用 A=1+0.55x²。下列解析系数是逐段常数;代码只对一维已知积分做数值求积,未用伴随 ODE 求这四类曲线。

This follows the Green-function construction of Giles–Pierce (JFM 426, 2001, §§3–7) for the pressure-integral objective in all four regimes: wholly supersonic, wholly subsonic, isentropic transonic, and shocked transonic. The caller supplies a smooth positive area A(x) on [−1,1]; the figure uses A=1+0.55x². The analytic coefficients below are piecewise constant. Code quadrature evaluates only the resulting known one-dimensional integrals; it does not shoot the adjoint ODE to obtain these curves.

ℛ=(AF)′−A′p e2=0,Q=∫−11p dx,m=Aρu,H=cpT+12u2,p0=p(1+γ−12M2)γ/(γ−1).\mathcal R=(AF)' - A'p\,e_2=0,\quad Q=\int_{-1}^{1}p\,dx,\quad m=A\rho u,\quad H=c_pT+\tfrac12u^2,\quad p_0=p(1+\tfrac{\gamma-1}{2}M^2)^{\gamma/(\gamma-1)}.
δU=αAUm∣H,p0+βUH∣p0,M+χUp0∣H,M,pm=−u1−M2,pH=0,pp0=pp0.\delta U=\frac{\alpha}{A} U_m\big|_{H,p_0}+\beta U_H\big|_{p_0,M}+\chi U_{p_0}\big|_{H,M},\qquad p_m=-\frac{u}{1-M^2},\quad p_H=0,\quad p_{p_0}=\frac p{p_0}.

在 ξ 放置三个独立单位源 f₁=Fm、f₂=A Fh、f₃=A Fp0;每一源使相应的 α、β、χ 跨 ξ 跳 1。按入口、出口、声速喉部和激波条件求这些分段系数,再将压力响应与激波位移积分成 I₁、I₂、I₃。最后由 Green 对偶式逐点恢复三分量伴随。

Place three independent unit sources f₁=Fm, f₂=A Fh, f₃=A Fp0 at ξ; each raises its corresponding coefficient α, β or χ by one across ξ. Inlet, exit, sonic-throat and shock conditions determine the piecewise coefficients. Integrate the pressure response and shock displacement to obtain I₁, I₂ and I₃, then recover all three adjoint components pointwise by Green duality.

f1=(1,u,H)T,f2=A2H(−ρu,0,ρuH)T,f3=Ap0F,(f1 f2 f3)Tλ=(I1,I2,I3)T.f_1=(1,u,H)^T,\quad f_2=\frac{A}{2H}(-\rho u,0,\rho uH)^T,\quad f_3=\frac{A}{p_0}F,\qquad (f_1\ f_2\ f_3)^T\lambda=(I_1,I_2,I_3)^T.
分支 / regime质量源 α / mass source出口与内部条件 / conditions
全超声速 / supersonicξ 右侧 +1 / +1 right of ξ入口 M,H,p₀ 固定;出口自由 / fixed inlet M,H,p₀; free exit
全亚声速 / subsonicξ 左侧 −1 / −1 left of ξ入口 H,p₀;出口 p 固定 / inlet H,p₀; fixed exit p
无激波跨声速 / transonicξ<0:左侧 −1;ξ>0:右侧 +1 / left −1 or right +1入口 H,p₀;喉部 M=1 / inlet H,p₀; sonic throat
含激波跨声速 / shocked喉部与激波分段,再与出口耦合 / piecewise throat–shock–exit喉部 M=1、[F]=0、出口 p;δξ 未知 / sonic throat, RH, exit p and unknown shock shift
← 横向滑动查看全图 →← Scroll sideways to view the full figure →
M < 1M > 1shock / 激波supersonic / 全超声速xsubsonic / 全亚声速xtransonic / 无激波跨声速xshocked / 含激波跨声速x
四类稳态分支的流动示意:蓝色为局部亚声速,绿色为局部超声速,紫色虚线为激波;灰色虚线标记声速喉部。通道轮廓仅表示面积变化,不按比例画壁面。Flow topology of the four steady branches: blue is locally subsonic, green is locally supersonic, violet marks the shock, and the grey dashed line marks the sonic throat. The duct outline illustrates area variation without a wall scale.

含激波分支对 ξ 与激波的相对位置分别求解。激波下游的质量响应与出口压力响应给出 2×2 系统;上游总压变化与马赫变化再通过正激波关系确定位移 δξ。目标响应含界面项 −[p]δξ。恢复的 λ 在激波处连续,λ₂=−1/A′(ξs),两侧导数为零;默认二次非退化喉部附近按对数增长;这不适用于所有光滑面积。四次平坦喉部 A=1+(5/6)x⁴ 的压力伴随满足 |x|λ₂→−7/10。图中保留喉部奇性截断,不把有限图幅当作有界解。

The shocked branch solves separately for sources before and after the shock. Downstream mass balance and exit-pressure balance form a 2×2 system. The normal-shock relation then determines displacement δξ from upstream total-pressure and Mach variations. The objective response includes −[p]δξ. The reconstructed λ is continuous at the shock, has λ₂=−1/A′(ξs) and zero slope on both sides, and grows logarithmically near the default nondegenerate quadratic throat. This does not hold for every smooth area: at a quartic throat A=1+(5/6)x⁴, the pressure adjoint satisfies |x|λ₂→−7/10. The plot cuts off the sonic singularity.

χ+=χ−f(M−)+p0,−f′(M−)(δξ Mx+α−AMm),Ij=∫−11δpj dx−[p] δξj.\chi_+=\chi_-f(M_-)+p_{0,-}f'(M_-)\left(\delta\xi\,M_x+\frac{\alpha_-}{A}M_m\right),\qquad I_j=\int_{-1}^{1}\delta p_j\,dx-[p]\,\delta\xi_j.
f(M)=p2p1(1+γ−12M221+γ−12M2)γ/(γ−1),p2p1=1+2γγ+1(M2−1),M22=1+γ−12M2γM2−γ−12.f(M)=\frac{p_2}{p_1}\left(\frac{1+\frac{\gamma-1}{2}M_2^2}{1+\frac{\gamma-1}{2}M^2}\right)^{\gamma/(\gamma-1)},\quad \frac{p_2}{p_1}=1+\frac{2\gamma}{\gamma+1}(M^2-1),\quad M_2^2=\frac{1+\frac{\gamma-1}{2}M^2}{\gamma M^2-\frac{\gamma-1}{2}}.
超声速 / supersonicλ₂亚声速 / subsonicλ₂无激波跨声速 / transonicλ₂含激波跨声速 / shockedλ₂
图 B.7 · 四类解的 λ₂。虚线为声速喉部与激波;纵轴随每图数据自适应,不能跨图比较幅值。Figure B.7 · λ₂ for four regimes. Dashed lines mark the sonic throat and shock. Each panel has its own vertical scale.

可复核性:77 项检查覆盖四类 Green 对偶恒等式、独立伴随微分方程、非二次面积函数、与 B.3 激波射击解的一致性、激波连续性和喉部增长。这里的“任意面积”指满足相应稳态分支存在、正面积、光滑性及非退化激波位置条件的输入,不涵盖分支折叠或多激波。

Verification: 77 checks cover Green duality in all four regimes, the independent adjoint ODE, a nonquadratic area, agreement with the B.3 shock-shooting solution, shock continuity and sonic growth. Arbitrary area means a positive smooth input admitting the selected steady branch and a nondegenerate shock position; branch folds and multiple shocks are outside this family.

Green 函数实现 / Green-function implementation
/* Giles–Pierce (JFM 426, 2001), sections 3–7: pressure-integral Green functions.
   The equations below are analytic piecewise solutions in the invariant basis
   (mass flux m, stagnation enthalpy H, stagnation pressure p0). Only their
   one-dimensional integrals are evaluated numerically. The four branches share
   an arbitrary smooth converging–diverging area supplied by the caller.
   x in [-1,1], sonic throat at 0, ideal gas gamma=1.4, R=1. */
'use strict';
const gamma=1.4, cp=3.5, beta=.2;
const phi=M=>((5/6)*(1+beta*M*M))**3/M;
function mach(r,sup){
  if(!(r>=1-2e-13))throw Error('Area is smaller than the branch sonic area');
  if(Math.abs(r-1)<1e-13)return 1;
  let a=sup?1:1e-12,b=sup?30:1;
  for(let k=0;k<70;k++){const m=(a+b)/2;if((phi(m)<r)===sup)a=m;else b=m;}
  return (a+b)/2;
}
function solve3(A,b){
  const a=A.map((r,i)=>[...r,b[i]]);
  for(let k=0;k<3;k++){let p=k;for(let i=k+1;i<3;i++)if(Math.abs(a[i][k])>Math.abs(a[p][k]))p=i;
    if(Math.abs(a[p][k])<1e-14)throw Error('Dependent Green source vectors');
    [a[k],a[p]]=[a[p],a[k]];
    for(let i=k+1;i<3;i++){const c=a[i][k]/a[k][k];for(let j=k;j<4;j++)a[i][j]-=c*a[k][j];}}
  const x=[0,0,0];for(let i=2;i>=0;i--){let v=a[i][3];for(let j=i+1;j<3;j++)v-=a[i][j]*x[j];x[i]=v/a[i][i];}return x;
}
function create({regime='shocked',area=x=>1+.55*x*x,areaX=x=>1.1*x,p01=1,H=3.5,
                 inletMach=3,exitMach=.3,shockX=.5,panels=192}={}){
  if(!['supersonic','subsonic','transonic','shocked'].includes(regime))throw Error('Unknown Giles–Pierce branch');
  if(!(H>0&&p01>0&&area(0)>0&&area(-1)>0&&area(1)>0))throw Error('Invalid gas/area data');
  if(regime==='shocked'&&!(shockX>0&&shockX<1&&areaX(shockX)>0))throw Error('Shock must be inside the divergent branch');
  const T0=H/cp, Astar=regime==='supersonic'?area(-1)/phi(inletMach):regime==='subsonic'?area(1)/phi(exitMach):area(0);
  function state(x,side='up'){
    const down=regime==='shocked'&&(side==='down'||x>shockX), p0=down?shockData.p02:p01;
    const astar=down?Astar*p01/p0:Astar;
    const sup=regime==='supersonic'||(regime==='transonic'&&x>0)||(regime==='shocked'&&x>0&&!down);
    const M=mach(area(x)/astar,sup),T=T0/(1+beta*M*M),p=p0/(1+beta*M*M)**3.5;
    const rho=p/T,u=M*Math.sqrt(gamma*T),m=area(x)*rho*u;
    return {x,M,T,p,rho,u,m,p0,H,A:area(x),E:T/(gamma-1)+u*u/2};
  }
  let shockData=null;
  if(regime==='shocked'){
    const q=state(shockX,'up'),M1=q.M,pr=1+2*gamma/(gamma+1)*(M1*M1-1);
    const M2=Math.sqrt((1+beta*M1*M1)/(gamma*M1*M1-beta));
    const p02=p01*pr*((1+beta*M2*M2)/(1+beta*M1*M1))**3.5;
    shockData={M1,M2,p1:q.p,p2:q.p*pr,p02,pr,m:q.m};
  }
  const inlet=state(-1),outlet=state(1),K=regime==='shocked'?shockData.m/shockData.p02:0;
  const D=-outlet.u/(1-outlet.M*outlet.M)/area(1),E=outlet.p/outlet.p0;
  function coupled(target,extra){const c=(-extra-target*D)/(E-K*D);return {a:target-K*c,b:0,c};}
  function pressurePerMass(q){return -q.u/(1-q.M*q.M)/q.A;}
  function basis(q){
    const f1=[1,q.u,H],f2=[-q.A*q.rho*q.u/(2*H),0,q.A*q.rho*q.u/2],
      f3=[q.A*q.rho*q.u/q.p0,q.A*(q.rho*q.u*q.u+q.p)/q.p0,q.A*q.rho*q.u*H/q.p0];
    return [f1,f2,f3];
  }
  function shockShift(a1,c1,c2){
    const s=shockData,M=s.M1,den=gamma*M*M-beta,M2=s.M2;
    const dM2sq=-2*M*(gamma+beta*beta)/(den*den),dM2=dM2sq/(2*M2);
    const dpr=4*gamma*M/(gamma+1),
      dlog=dpr/s.pr+3.5*(2*beta*M2*dM2/(1+beta*M2*M2)-2*beta*M/(1+beta*M*M));
    const fp=(s.p02/p01)*dlog;
    const Mx=areaX(shockX)/area(shockX)*M*(1+beta*M*M)/(M*M-1);
    const Mm=M/s.m*(1+beta*M*M)/(1-M*M);
    return (c2-c1*s.p02/p01-p01*fp*a1*Mm)/(p01*fp*Mx);
  }
  function green(source,xi){
    if(!(source>=1&&source<=3&&Number.isInteger(source)&&xi>-1&&xi<1))throw Error('Invalid Green source/position');
    if((regime==='transonic'||regime==='shocked')&&Math.abs(xi)<1e-12)throw Error('Source at sonic throat');
    if(regime==='shocked'&&Math.abs(xi-shockX)<1e-12)throw Error('Source at shock');
    let delta=0,down={a:0,b:0,c:0};
    if(regime==='shocked'){
      if(source===1&&xi>0&&xi<shockX){down=coupled(1,0);delta=shockShift(1,0,down.c);}
      if(source===1&&xi>shockX){down=coupled(0,D);delta=shockShift(0,0,down.c);}
      if(source===3&&xi<shockX){down=coupled(shockData.m/p01,0);delta=shockShift(0,1,down.c);}
      if(source===3&&xi>shockX){down=coupled(0,E);delta=shockShift(0,0,down.c);}
    }
    function coeff(x){
      if(source===2)return {a:0,b:x>xi?1:0,c:0};
      if(regime==='supersonic')return {a:source===1&&x>xi?1:0,b:0,c:source===3&&x>xi?1:0};
      if(regime==='subsonic')return {a:source===1&&x<xi?-1:source===3?-E/D:0,b:0,c:source===3&&x>xi?1:0};
      if(regime==='transonic')return {a:source===1&&((xi<0&&x<xi)||(xi>0&&x>xi))?(xi<0?-1:1):0,b:0,c:source===3&&x>xi?1:0};
      if(source===1&&xi<0)return {a:x<xi?-1:0,b:0,c:0};
      if(x<shockX){
        if(source===1)return {a:x>xi?1:0,b:0,c:0};
        return {a:0,b:0,c:x>xi?1:0};
      }
      if(source===1)return {a:down.a+(xi>shockX&&x>xi?1:0),b:0,c:down.c};
      return {a:down.a,b:0,c:down.c+(xi>shockX&&x>xi?1:0)};
    }
    function density(x){const t=coeff(x);if(!t.a&&!t.c)return 0;
      const q=state(x,regime==='shocked'&&x>shockX?'down':'up');
      return (t.a? t.a*pressurePerMass(q):0)+t.c*q.p/q.p0;}
    const cuts=[-1,0,1,xi];if(regime==='shocked')cuts.push(shockX);
    cuts.sort((a,b)=>a-b);let integral=0;
    for(let j=1;j<cuts.length;j++){
      const a=cuts[j-1],b=cuts[j];if(b-a<1e-13)continue;
      const n=panels+panels%2,h=(b-a)/n;
      // Offset interval endpoints to avoid selecting an adjacent branch or M=1.
      let z=density(a+Math.min(1e-10,h*1e-5))+density(b-Math.min(1e-10,h*1e-5));
      for(let k=1;k<n;k++)z+=(k%2?4:2)*density(a+k*h);
      integral+=z*h/3;
    }
    const I=integral-(regime==='shocked'?(shockData.p2-shockData.p1)*delta:0);
    return {source,xi,I,delta,coeff};
  }
  function adjoint(xi){const G=[1,2,3].map(j=>green(j,xi)),q=state(xi,regime==='shocked'&&xi>shockX?'down':'up');
    const F=basis(q),v=solve3(F,G.map(g=>g.I));
    return {xi,v,I:G.map(g=>g.I),delta:G.map(g=>g.delta),sourceVectors:F};
  }
  return {regime,area,areaX,p01,H,Astar,shockX,shockData,inlet,outlet,state,green,adjoint,basis};
}
module.exports={create,phi,mach};

B.8 Roe 激波捕捉与离散伴随的相位实验

B.8 Roe shock capturing and discrete-adjoint phase study

Giles、Duta、Müller 与 Pierce(AIAA J. 41, 2003,§5)指出:原始压力积分即使随加密趋近正确值,尖锐 Roe 激波随背压跨单元时产生的目标误差仍可能有 O(h) 的齿纹,其背压导数保持 O(1) 相位误差。本实验用同一物理机制重新构造一个可重复的拟一维 Euler 算例,数据不是论文图的数字化副本。

Giles, Duta, Müller and Pierce (AIAA J. 41, 2003, §5) show that a convergent pressure integral can still have O(h) scallops as a sharp Roe shock moves cell by cell with exit pressure, leaving an O(1) phase error in its derivative. This is a new reproducible quasi-1-D Euler experiment driven by the same mechanism, not digitized data from their figures.

Fx+A′A(F−P)=0,A(x)=1+0.5x+0.3x2,Min=1.5,Q=∫01p dx,pb=Pfit(ξ),ξ=0.5+θN.F_x+\frac{A'}{A}(F-P)=0,\quad A(x)=1+0.5x+0.3x^2,\quad M_{\rm in}=1.5,\quad Q=\int_0^1p\,dx,\quad p_b=P_{\rm fit}(\xi),\quad \xi=0.5+\frac{\theta}{N}.

一阶格心有限体积使用 Roe 通量及完整计算图 JVP/VJP。零声学波速且波强非零时,未光滑 Roe 通量不可微;此处计算图给出所选分支的线性映射,不能称为经典 Jacobian。原 B.8 模型的 N=24 有向证书已定位背压约 0.6888970486283346 的精确稳态声学切换根,压力目标左右导数约为 2.6453294685 和 1.3921358415,差的绝对值严格大于 1.25;此有限非默认背压反例不替代下面固定相位序列的网格极限。另一组用固定 LF 波速 3,第三组取 LF 波速 3N^{1/4},对应数值黏性物理宽度 ε≈3h^{3/4}:激波跨越的单元数增加而物理宽度减小。每个背压都重求原始解,并独立求 tangent 与离散伴随;参考值从激波拟合流动分段积分后作收敛的四点差分。

The first-order cell-volume scheme uses Roe flux and full computational-graph JVP/VJP. With a zero acoustic speed and nonzero wave strength, the unsmoothed Roe flux is not differentiable; its graph products describe the selected branch, not a classical Jacobian. A directed N=24 certificate for the original B.8 model locates an exact steady acoustic switch root at back pressure approximately 0.6888970486283346. The pressure objective has left/right derivatives about 2.6453294685 and 1.3921358415, with a gap strictly exceeding 1.25; this finite nondefault-pressure counterexample does not settle the fixed phase sequences below. Comparators use fixed LF speed 3 and LF speed 3N^{1/4}, corresponding to physical viscosity width ε≈3h^{3/4}: more cells span the shock while its physical width shrinks. Every back pressure gets a fresh primal solve and independent tangent and discrete-adjoint solves. The reference differentiates a separately shock-fitted, piecewise integrated flow with a stabilized four-point formula.

1.41.722.3N=24N=48N=96N=19201/61/3激波单元相位 / shock cell phase
图 B.8 · Roe 离散伴随梯度随激波在单元内的位置改变;曲线按同一相位在四个网格上比较。Figure B.8 · Roe discrete-adjoint gradient against shock position within a cell, compared at the same phase on four grids.
θ=0 · 1/3
← 横向滑动查看全图 →← Scroll sideways to view the full figure →
N=192 · one cell enlarged / 单元放大Roefixed LFbroad LF1.21.41.61.82.02.2dQ/dp_bθ=0fit 1.69921.39191.69461.6792θ=1/6fit 1.69802.14291.69341.6780θ=1/3fit 1.69691.80581.69221.6768
逐帧演示 N=192 的三个实际计算相位:点“播放”则每帧停留 3 秒,也可用上一帧、下一帧逐帧查看(开启“减少动态效果”时播放键停用,只能逐帧查看)。上方放大一个单元内的激波位置,下方圆点为三种离散伴随梯度,紫色虚线为激波拟合参考值。帧间不插值,也不表示额外计算结果。Three measured phases at N=192. Press Play for three seconds per frame, or step through them with the previous and next buttons (with reduced motion on, Play is disabled and only stepping is available). The upper strip magnifies the shock within one cell; lower dots are the three discrete-adjoint gradients and the violet dashed line is the shock-fitted reference. Intermediate phases are not interpolated or claimed as computed results.
NRoe 最大 |误差| / max errorRoe 相位跨度 / phase spread固定 LF 最大 |误差|增宽 LF 最大 |误差|增宽 LF 过渡单元
244.36e-10.7211.18e-15.58e-16
484.41e-10.7381.77e-22.62e-18
964.43e-10.7478.77e-36.53e-210
1924.45e-10.7514.66e-32.01e-213

表中 Roe 梯度的相位跨度在四次加密中仍保持明显量级,而两类 LF 的梯度相位差收缩。增宽 LF 在粗网格有较大的耗散偏差,因此不能仅看一个网格断言它更准确;单元数增加与物理宽度缩小需同时检查。独立解析分析已构造本 Roe 格式的完整精确局部单激波根:对本节数学序列 θ=0、1/6、1/3,压力梯度极限分别约为 1.390108、2.145405、1.809748,连续值约为 1.699205,误差绝对下极限严格大于 0.3、0.4、0.1,原解和目标值仍收敛。保存的默认背压另保持为其实际 dyadic 有理值;区间认证和整数网格相位子列给同一固定背压的无限加密反例,子列梯度误差下极限大于 0.3。这些是理想精确十进制 Euler 的局部精确根结论;默认求解器全部细网格根身份、binary64 全程序、数值网格阈值和完整 Euler Lean 尚未证明。Giles(2003)及 Giles–Ulbrich(2010)的严格收敛分析是标量守恒律/Burgers,不能直接移植为 Euler 定理。

Roe gradient spread remains substantial over four refinements, whereas both LF phase spreads shrink. Broadening LF has a large coarse-grid bias, so one grid cannot establish better accuracy; its cell count and physical shock width must be checked together. A separate analytic construction gives exact local single-shock roots of this complete Roe residual. For the mathematical sequences theta=0, 1/6 and 1/3, pressure-gradient limits are approximately 1.390108, 2.145405 and 1.809748, versus the continuous 1.699205. Absolute limiting errors exceed 0.3, 0.4 and 0.1 even though states and objective values converge. The saved default pressure is also held fixed as its actual dyadic rational: interval certification and an integer-grid phase subsequence give a refinement counterexample at this same fixed pressure, with subsequence gradient error exceeding 0.3. These are local exact-root results for ideal exact-decimal Euler. All-fine-grid default solver root identity, binary64 execution, a numeric mesh threshold and full Euler Lean remain unproved. The rigorous analyses of Giles (2003) and Giles–Ulbrich (2010) concern scalar conservation laws/Burgers and do not transfer directly to Euler.

Roe 固定输入的严格反例与相位动画 / Fixed-input Roe counterexample and phase animation

下图菱形是解析局部精确根的梯度极限(系数有向认证);圆点是原页面有限浮点观察。动画只显示冻结一格层及极限公式,不把该层称为完整喷管稳态根。默认全部细网格根身份仍未证明。

Diamonds display analytic local exact-root gradient limits with directed-certified coefficients; circles are finite original-page floating observations. The animation displays frozen one-cell profiles and the limiting formula. Those profiles are not full nozzle steady roots. Default all-grid root identity remains open.

Roe pressure-gradient limits and exact frozen profiles
原页面相位与独立解析极限 / Original page phases and independently derived limits

已开启“减少动态效果”:此处的冻结层动画已隐藏,静态图见上。

Reduce-motion is on: the frozen-layer animation is hidden here; see the static figure above.

Frozen Roe core pressure changes the gradient limit
冻结层核心相位变化 / Frozen-layer core phase changes

R1–R8 完整证明与边界见仓库中的证明文档(尚未随本站发布)/ The complete proof and its scope are in the proof document in the repository (not published with this site)

Giles–Pierce 2001 原文 · Giles 等 2003 原文 · Giles 标量激波论文

Giles–Pierce 2001 · Giles et al. 2003 · Giles scalar-shock paper

离散相位实验实现 / Discrete phase-study implementation
/* Euler counterpart of the sharp-shock sensitivity experiment in Giles et al.
   AIAA J. 41 (2003), §5: Roe FDS versus fixed/broadening LF diffusion.
   The residual is their dF/dx + (A'/A)(F-P)=0 form in a divergent duct;
   inlet is fully supersonic, exit pressure fixes a normal-shock position.
   This is a fresh controlled experiment, not a digitization of their figures.
   Run from this directory: node giles_discrete_study.js
   The JSON includes source provenance and nonlinear/tangent/adjoint diagnostics. */
'use strict';
const fs=require('fs'),path=require('path'),crypto=require('crypto');
const E=require('../../experiments/giles_euler');
const SOURCES=[__filename,path.join(__dirname,'../../experiments/giles_euler.js'),path.join(__dirname,'../../experiments/numerics.js'),path.join(__dirname,'math.js')];
const provenance=()=>{const h=crypto.createHash('sha256');SOURCES.forEach(p=>h.update(fs.readFileSync(p)));return h.digest('hex');};
const GRIDS=[24,48,96,192],PHASES=[0,1/6,1/3],SCHEMES=['roe','fixed-lf','broad-lf'];
const root=E.model({geometry:'diverging'});
function position(pb){let a=.001,b=.999,fa=root.fittedBackPressure(a)-pb,fb=root.fittedBackPressure(b)-pb;
  if(fa*fb>=0)throw Error('Back pressure outside shock branch');
  for(let k=0;k<58;k++){const x=(a+b)/2,f=root.fittedBackPressure(x)-pb;if(f*fa>0){a=x;fa=f;}else b=x;}return (a+b)/2;}
function objective(pb){const xi=position(pb),fit=root.fittedProfile(xi);
  const integral=(a,b,down)=>{const n=384,h=(b-a)/n;let s=0;
    for(let k=0;k<=n;k++)s+=(k===0||k===n?1:k%2?4:2)*fit.state(a+k*h,down).p;
    return h*s/3;};
  return integral(0,xi,false)+integral(xi,1,true);}
function reference(pb){const h=2e-5;
  const four=(objective(pb-2*h)-8*objective(pb-h)+8*objective(pb+h)-objective(pb+2*h))/(12*h);
  const half=(objective(pb-h)-8*objective(pb-h/2)+8*objective(pb+h/2)-objective(pb+h))/(6*h);
  if(Math.abs(four-half)>2e-7)throw Error('Fitted-reference differentiation did not stabilize');
  return four;}
function run(){const rows=[];
  for(const n of GRIDS)for(const phase of PHASES){
    const xi=.5+phase/n,pb=root.fittedBackPressure(xi),ref=reference(pb);
    for(const scheme of SCHEMES){
      const options={n,geometry:'diverging',sourceForm:'giles',pb,order:1,objective:'pressure',
        fluxScheme:scheme==='roe'?'roe':'rusanov',llfSpeed:scheme==='broad-lf'?3*n**.25:3};
      const m=E.model(options),t0=Date.now(),s=m.solve();
      if(!s.converged)throw Error(`Primal ${scheme} n=${n} phase=${phase}: ${s.reason} ${s.history.at(-1)}`);
      const L=m.linearize(s.U),P=m.firstOrderMatrix(s.U),zero=Array(m.nd).fill(0),dp=Array(m.params.length).fill(0);
      dp[n+1]=1;
      const jU=L.vjp(zero,1).U;
      const adj=m.linearSolve(L,jU.map(v=>-v),P,true,'gmres',2e-11);
      const tan=m.linearSolve(L,L.jvp(zero,dp).R.map(v=>-v),P,false,'gmres',2e-11);
      if(!adj.converged||!tan.converged)throw Error(`Linear solve ${scheme} n=${n} phase=${phase}`);
      const gradient=L.vjp(adj.x,1).p[n+1],tangentGradient=L.jvp(tan.x,dp).J;
      if(Math.abs(gradient-tangentGradient)>2e-8)throw Error(`Tangent/adjoint mismatch ${scheme} n=${n} phase=${phase}`);
      const mach=m.x.map((_,i)=>{const q=m.physics.prim(s.U.slice(3*i,3*i+3));return q.u/q.a;});
      rows.push({n,phase,xi,pb,scheme,gradient,tangentGradient,reference:ref,error:gradient-ref,
        objective:m.evaluate(s.U).J,fittedObjective:objective(pb),transitionCells:mach.filter(v=>v>.9&&v<1.3).length,
        primalResidual:s.history.at(-1),seconds:(Date.now()-t0)/1000});
      console.log(`n=${n} phase=${phase.toFixed(3)} ${scheme.padEnd(8)} gradient=${gradient.toFixed(8)} error=${(gradient-ref).toExponential(2)} cells=${rows.at(-1).transitionCells}`);
    }
  }
  return {provenance:provenance(),metadata:{area:'1 + 0.5 x + 0.3 x^2',inletMach:1.5,p01:1,T0:1,sourceForm:'dF/dx + (A\'/A)(F-P) = 0',grids:GRIDS,phases:PHASES,schemes:SCHEMES},rows};
}
if(require.main===module){const out=run(),file=path.join(__dirname,'giles_discrete_study.json');fs.writeFileSync(file,JSON.stringify(out));console.log('wrote '+file);}
module.exports={run,provenance,reference,objective,position};

附录 C:小规模伴随方程的直接求解

Appendix C: direct adjoint solves for small systems

本附录的一阶、二阶均不采用 matrix-free 求解:先显式组装并存储完整精确 Jacobian,再对其转置作带主元 LU 分解和三角求解。用精确导数取得矩阵列属于组装步骤;matrix-free 反向算子只在直接解完成后用于独立残差验证。

For both first and second order, this appendix does not use a matrix-free solve: explicitly assemble and store the complete exact Jacobian, then factor its transpose with pivoted LU and solve triangular systems. Derivative products used to obtain columns belong to assembly; the matrix-free reverse operator is used only afterwards for independent residual verification.

C.1 小系统为什么值得直接求解

C.1 Why use a direct method for a small system

默认 12 区间时,格心有 36 个状态未知量,格点有 39 个。这个规模下,可以把精确 Jacobian 全部写出来,采用带部分主元选取的 LU 分解直接解伴随方程。它避免许多次 GS 或 Krylov 算子调用,通常是这种教学小系统中很快的选择;是否最快由下方同条件实测决定,不能只比较迭代次数。

With the default 12 intervals there are 36 cell-centred or 39 node-centred state unknowns. At this size we can assemble the exact Jacobian and solve the adjoint with partial-pivot LU. This avoids many GS or Krylov operator applications and is often a fast choice for a small teaching system. The benchmark below determines which is fastest under the same conditions; iteration counts alone cannot establish that.

J=RU(U∗,D),JTψ=−QUT≡r,g=QDT+RDTψ.J=R_U(U^*,D),\qquad J^T\psi=-Q_U^T\equiv r,\qquad g=Q_D^T+R_D^T\psi.

正文仍保留 matrix-free 精确算子。本附录额外提供显式矩阵路径:以单位种子 U˙=ej\dot U=e_j 做一次精确前向传播,得到第 j 列 R˙=Jej\dot R=Je_j,然后转置得到 JT。这里的 J 必须与顶端所选的一阶或二阶残差一致,包含重构、波速、源项与边界行。不能把一阶预处理矩阵 P 的转置直接解当成二阶伴随解。解出 ψ 后,以 Rˉ=ψ\bar R=\psi、Qˉ=1\bar Q=1 为种子做一次反向传播,得到梯度 Dˉ=g\bar D=g。

The main text retains exact matrix-free operators. This appendix adds an explicit-matrix path: one exact tangent sweep with the unit seed U˙=ej\dot U=e_j gives column R˙=Jej\dot R=Je_j, then transpose J. The matrix must match the selected first- or second-order residual, including reconstruction, wave speed, source and boundary rows. Solving with the first-order preconditioner PT alone is not a second-order adjoint solution. After ψ is solved, one reverse sweep with the seeds Rˉ=ψ\bar R=\psi and Qˉ=1\bar Q=1 gives the gradient Dˉ=g\bar D=g.

# column j of J: the tangent R_d of the seed U_d = e_j
for j = 0,...,n-1:
    J[:,j] = residual_d(U, U_d=unit_vector(j))
M = transpose(J)
rhs = -objective_state_gradient
LU, permutation = factor_with_partial_pivoting(M)
psi = triangular_solve(LU, permutation, rhs)
# one reverse sweep with the seeds R_bar = psi, Q_bar = 1
D_bar = residual_bar(U, R_bar=psi, Q_bar=1)   # = g

C.2 主元、消元、前代与回代

C.2 Pivoting, elimination and triangular solves

在第 k 列,从尚未消元的行中选绝对值最大的主元,把整行交换到第 k 行;右端项也必须按照相同置换重排。用主元消去下面的元素,把乘数保存在严格下三角,更新后的上三角保存 U。L 的对角线默认为 1。完整行交换也交换已经保存的 L 乘数,否则分解关系就会被破坏。

In column k, select the largest-magnitude remaining entry and swap its entire row into position k. Apply the same permutation to the right-hand side. Eliminate entries below the pivot, storing multipliers in the strict lower triangle and the updated U in the upper triangle. L has unit diagonal. Full row swaps also swap previously stored L multipliers, preserving the factorisation.

ΠJT=LU,Ly=Πr,Uψ=y,ℓik=mik/mkk,mij←mij−ℓikmkj.\Pi J^T=L U,\quad Ly=\Pi r,\quad U\psi=y,\quad \ell_{ik}=m_{ik}/m_{kk},\quad m_{ij}\leftarrow m_{ij}-\ell_{ik}m_{kj}.
yi=(Πr)i−∑j<iℓijyj,ψi=yi−∑j>iuijψjuii.y_i=(\Pi r)_i-\sum_{j<i}\ell_{ij}y_j,\qquad\psi_i=\frac{y_i-\sum_{j>i}u_{ij}\psi_j}{u_{ii}}.

不显式计算逆矩阵。LU 分解一次之后,不同目标只需重新计算右端项并做两次三角求解;只有流场、网格、边界或空间阶数改变导致 J 改变时才必须重新分解。主元接近机器精度尺度时停止并报告数值奇异,不能用人为小数替换零主元。

Do not form a matrix inverse. Once LU is factored, another objective needs a new right-hand side and two triangular solves. Refactor when changes in state, geometry, boundary data or spatial order change J. A pivot near machine precision triggers a numerical-singularity error rather than substitution of an arbitrary small pivot.

逐步推导:Euler 块矩阵为什么要换行

Step by step: why the Euler block matrix needs row swaps

JTJ^T 由 3×33\times3 块组成(本页 N = 12 时 39 维)。一阶时每个块只与相邻块耦合,按标量计下、上带宽都是 5;二阶重构把耦合延伸到隔一个节点,带宽变为 8。与 Burgers 不同,对角元并不总是本列剩余元素中绝对值最大者,所以部分主元真的会交换行:一阶 3 次、二阶 16 次。一阶第一次换行发生在 k=0k=0:当前对角元的绝对值是 0.0379,第 2 行同列元素的绝对值是 0.399,于是交换这两行。

JTJ^T consists of 3×33\times3 blocks (dimension 39 here for N = 12). At first order each block couples only to its neighbours, giving lower and upper scalar bandwidths 5; second-order reconstruction reaches one node further and the bandwidths become 8. Unlike Burgers, the diagonal is not always the largest remaining entry of its column, so partial pivoting really swaps rows: 3 times at first order and 16 at second. The first first-order swap happens at k=0k=0: the current diagonal entry has magnitude 0.0379 and row 2 has 0.399 in the same column, so the two rows are exchanged.

换行最多让 UU 变宽多少:带宽为 pp、qq 时,第 kk 步的主元只能来自第 kk 到 k+pk+p 行(更下面的行在第 kk 列为零)。被换到第 kk 行的行,非零元至多到第 k+p+qk+p+q 列,所以 UU 的上带宽不超过 p+qp+q。

How much wider can swaps make UU? With bandwidths pp and qq, the pivot of step kk can only come from rows kk to k+pk+p (rows further down are zero in column kk). A row moved to position kk has nonzeros at most up to column k+p+qk+p+q, so the upper bandwidth of UU never exceeds p+qp+q.

一阶的 3 次换行都发生在两端节点的同一块行内,被交换的两行非零列完全相同,换行不改变非零结构,所以 UU 的上带宽仍是 5,这次分解也没有产生任何填充。二阶有行从下面的块行换上来,带着更靠右的非零元:UU 的上带宽从 8 增加到 9(不超过 p+q=16p+q=16),消元还在原来为零的位置产生 21 个新的非零元(填充)。图 C.1 是两种阶数的 JTJ^T,图 C.2 是二阶分解后的因子。

At first order all 3 swaps stay inside one block row of the two end nodes; the two rows exchanged have the same nonzero columns, so the swaps leave the pattern unchanged, the upper bandwidth of UU stays 5, and this factorisation creates no fill at all. At second order rows move up from block rows below, bringing entries further right: the upper bandwidth of UU grows from 8 to 9 (never beyond p+q=16p+q=16), and elimination creates 21 new nonzeros where the matrix had zeros (fill). Figure C.1 shows JTJ^T for both orders and Figure C.2 the factors of the second-order one.

图 C.1 · 本页格点格式的精确 Jᵀ 的非零结构(N = 12,收敛解处),细线划分 3×3 块。(a) 一阶 JT​(b) 二阶 JT​
图 C.1 · 本页格点格式的精确 JT 的非零结构(N = 12,收敛解处),细线划分 3×3 块。
Figure C.1 · The nonzero pattern of the exact Jᵀ of this page's node scheme (N = 12, at the converged state); thin lines mark the 3×3 blocks.(a) first-order JT​(b) second-order JT​
Figure C.1 · The nonzero pattern of the exact JT of this page's node scheme (N = 12, at the converged state); thin lines mark the 3×3 blocks.
图 C.2 · 二阶 Jᵀ 的带主元 LU 因子(按置换后的行顺序)。绿色为 L 的乘数,蓝色为 U;红框是填充,左侧琥珀色圆点标出被交换过的行。按行置换后的 L(对角线下)与 U(对角线及以上)L 的乘数U 的元素填充:原矩阵为零(21 个)被交换过的行(24 行)带宽:下 8/上 8U 的上带宽 9 ≤ p + q = 16
图 C.2 · 二阶 JT 的带主元 LU 因子(按置换后的行顺序)。绿色为 L 的乘数,蓝色为 U;红框是填充,左侧琥珀色圆点标出被交换过的行。
Figure C.2 · The partial-pivot LU factors of the second-order Jᵀ (rows in pivoted order). Green are the multipliers of L, blue the entries of U; red outlines mark fill, and amber dots in the margin mark rows moved by swaps.L (below the diagonal) and U (on and above it), rows permutedmultipliers in Lentries of Ufill: zero in the matrix (21)rows moved by swaps (24)bandwidths: 8 below / 8 aboveupper bandwidth of U 9 ≤ p + q = 16
Figure C.2 · The partial-pivot LU factors of the second-order JT (rows in pivoted order). Green are the multipliers of L, blue the entries of U; red outlines mark fill, and amber dots in the margin mark rows moved by swaps.

C.3 看清矩阵和直接求解过程

C.3 Inspect the matrix and direct solution

下方随页面阶数和目标更新。矩阵图显示 JT 的非零耦合;可选行查看数值,拖动消元步查看主元交换及存储在同一数组中的 L/U。右端、前代结果 y、伴随 ψ 和网格图均来自实际计算。直接法没有外层迭代收敛曲线:消元步不是 GS 迭代。

The panel follows the selected order and objective. Its matrix map shows nonzero couplings in JT. Select a row to inspect values and an elimination step to inspect pivot swaps and packed L/U. The right-hand side, forward-substitution y, adjoint ψ and mesh plot are computed live. Elimination steps are not GS iterations; the direct method has no outer convergence history.

C.4 验证与公平计时

C.4 Verification and fair timing

ra=JTψ+QUT,η=∥ra∥∞∥JT∥∞∥ψ∥∞+∥QUT∥∞.r_a=J^T\psi+Q_U^T,\qquad\eta=\frac{\|r_a\|_\infty}{\|J^T\|_\infty\|\psi\|_\infty+\|Q_U^T\|_\infty}.

真残差用正文的 matrix-free 反向算子重新计算,另比较 GS、GMRES 的伴随解与全部设计梯度。小后向误差表示解满足附近的线性系统,不保证病态系统中解的前向误差也小。

Recompute the true residual with the main text’s matrix-free reverse operator and compare adjoint solutions and every design gradient against GS and GMRES. A small backward error means the computed result solves a nearby system; it does not ensure a small forward error for an ill-conditioned system.

计时共享同一个已收敛流场、精确线性化和右端项,不包含原始流场求解、绘图或源码展示。直接法计入精确矩阵组装、转置、LU 及三角求解;迭代法计入一阶 P 的组装和线性求解。另列已分解 LU 的重复右端求解时间。每种方法预热后重复 7 次并取中位数;浏览器、硬件及计时分辨率会影响结果。比较的是本页教学实现,不代表优化数值库的速度排名。

Timing shares the converged flow, exact linearisation and right-hand side, excluding primal solves, rendering and source display. Direct timing includes exact assembly, transpose, LU and triangular solves; iterative timing includes first-order P assembly and the linear solve. Reused-LU right-hand-side time is reported separately. Each method is warmed up, repeated seven times and reported by its median. Browser, hardware and timer resolution affect results. This compares these teaching implementations, not optimised numerical libraries.

逐步推导:一次分解同时服务 tangent 与 adjoint

Step by step: one factorisation serves both the tangent and the adjoint

本附录分解的是 JTJ^T。若改为分解 JJ,同一组因子也能解伴随:由 ΠJ=LU\Pi J=LU 得 JT=UTLTΠJ^T=U^TL^T\Pi,伴随只是把两次三角求解的顺序和方向换过来:

This appendix factors JTJ^T. Had it factored JJ, the same factors would also solve the adjoint: ΠJ=LU\Pi J=LU gives JT=UTLTΠJ^T=U^TL^T\Pi, and the adjoint merely swaps the order and direction of the two triangular solves:

Ly=Πt,Uv=y⏟J v=t=−RDD˙UTz=r,LTy=z,ψ=ΠTy⏟JTψ=r=−QUT.\underbrace{Ly=\Pi t,\quad Uv=y}_{J\,v=t=-R_D\dot D}\qquad\qquad \underbrace{U^Tz=r,\quad L^Ty=z,\quad \psi=\Pi^Ty}_{J^T\psi=r=-Q_U^T}.

逐步推导:后向误差换算成前向误差

Step by step: turning the backward error into a forward error

C.4 定义的 η\eta 恰好是使 ψ\psi 成为某个扰动系统精确解的最小相对扰动(Rigal–Gaches):存在 EE、ff 使 (JT+E)ψ=r+f(J^T+E)\psi=r+f,∥E∥∞≤η∥JT∥∞\|E\|_\infty\le\eta\|J^T\|_\infty,∥f∥∞≤η∥r∥∞\|f\|_\infty\le\eta\|r\|_\infty,其中 r=−QUTr=-Q_U^T。精确解满足 JTψ∗=rJ^T\psi^*=r,两式相减得 JT(ψ−ψ∗)=f−EψJ^T(\psi-\psi^*)=f-E\psi,取范数:

The η\eta defined in C.4 is exactly the smallest relative perturbation for which ψ\psi solves a perturbed system (Rigal–Gaches): there are EE and ff with (JT+E)ψ=r+f(J^T+E)\psi=r+f, ∥E∥∞≤η∥JT∥∞\|E\|_\infty\le\eta\|J^T\|_\infty and ∥f∥∞≤η∥r∥∞\|f\|_\infty\le\eta\|r\|_\infty, where r=−QUTr=-Q_U^T. The exact solution satisfies JTψ∗=rJ^T\psi^*=r; subtracting gives JT(ψ−ψ∗)=f−EψJ^T(\psi-\psi^*)=f-E\psi, and taking norms:

∥ψ−ψ∗∥∞∥ψ∥∞≤κ∞(JT) η(1+∥r∥∞∥JT∥∞∥ψ∥∞)≲2 κ∞(JT) η.\frac{\|\psi-\psi^*\|_\infty}{\|\psi\|_\infty}\le\kappa_\infty(J^T)\,\eta\Bigl(1+\frac{\|r\|_\infty}{\|J^T\|_\infty\|\psi\|_\infty}\Bigr)\lesssim2\,\kappa_\infty(J^T)\,\eta.

本页 N = 12(格点,推力目标):一阶 κ∞(JT)=449\kappa_\infty(J^T)=449、η=4.8×10−17\eta=4.8\times10^{-17};二阶 κ∞(JT)=1503\kappa_\infty(J^T)=1503、η=4.6×10−17\eta=4.6\times10^{-17}。伴随的相对前向误差不超过约 1.4×10−131.4\times10^{-13}。作为检查,用补偿点积算残差、迭代细化三次得到 ψ∗\psi^*,LU 解的实际相对误差一阶是 3.1×10−163.1\times10^{-16}、二阶是 5.5×10−155.5\times10^{-15},都在界内。

Here N = 12 (node, thrust objective): first order κ∞(JT)=449\kappa_\infty(J^T)=449 and η=4.8×10−17\eta=4.8\times10^{-17}; second order κ∞(JT)=1503\kappa_\infty(J^T)=1503 and η=4.6×10−17\eta=4.6\times10^{-17}. The relative forward error of the adjoint is therefore at most about 1.4×10−131.4\times10^{-13}. As a check, three steps of iterative refinement with residuals from a compensated dot product give ψ∗\psi^*; the actual relative error of the LU solution is 3.1×10−163.1\times10^{-16} at first order and 5.5×10−155.5\times10^{-15} at second, both within the bound.

稠密矩阵存储需要 O(n²),LU 分解需要 O(n³),重复右端求解需要 O(n²)。在默认 36/39 维系统中代价很小;工业非结构网格 RANS 的规模则完全不同,需要考虑稀疏分解的填充、内存与并行扩展。因此附录 C 是小规模问题的直接解基准,正文的 matrix-free/预处理方法仍是向大型问题发展的重要路线。

Dense storage costs O(n²), LU factorisation O(n³), and subsequent right-hand-side solves O(n²). These costs are small for 36/39 unknowns. Industrial unstructured RANS is a different scale, where sparse-factorisation fill, memory and parallel scaling matter. Appendix C supplies a small-system direct reference; matrix-free methods and preconditioning remain important for larger problems.

展开完整直接法实现Complete direct-solver implementation
/* Small-system exact adjoint: explicit assembly and partial-pivot LU. */
(function(root){'use strict';
const Q=typeof module!=='undefined'?require('./core'):root.QEuler,{norm}=Q;
function assemble(L,n){const A=Array.from({length:n},()=>Array(n).fill(0));for(let j=0;j<n;j++){const e=Array(n).fill(0);e[j]=1;L.jvp(e).R.forEach((v,i)=>A[i][j]=v);}return A;}
function factor(M,trace=false){const n=M.length,lu=M.map(r=>r.slice()),permutation=Array.from({length:n},(_,i)=>i),steps=[],scale=Math.max(...M.flat().map(Math.abs));if(!scale||!Number.isFinite(scale))throw Error('Invalid or zero matrix');for(let k=0;k<n;k++){let pivot=k;for(let i=k+1;i<n;i++)if(Math.abs(lu[i][k])>Math.abs(lu[pivot][k]))pivot=i;if(Math.abs(lu[pivot][k])<=Number.EPSILON*scale*n)throw Error('Numerically singular LU pivot at '+k);[lu[k],lu[pivot]]=[lu[pivot],lu[k]];[permutation[k],permutation[pivot]]=[permutation[pivot],permutation[k]];for(let i=k+1;i<n;i++){lu[i][k]/=lu[k][k];for(let j=k+1;j<n;j++)lu[i][j]-=lu[i][k]*lu[k][j];}if(trace)steps.push({k,pivot,lu:lu.map(r=>r.slice()),permutation:permutation.slice()});}return {lu,permutation,steps};}
function solve(F,b){const {lu,permutation}=F,n=b.length,y=permutation.map(i=>b[i]),x=Array(n).fill(0);for(let i=0;i<n;i++)for(let j=0;j<i;j++)y[i]-=lu[i][j]*y[j];for(let i=n-1;i>=0;i--){let r=y[i];for(let j=i+1;j<n;j++)r-=lu[i][j]*x[j];x[i]=r/lu[i][i];}return {x,y,permutedRhs:permutation.map(i=>b[i])};}
const transpose=A=>A[0].map((_,j)=>A.map(r=>r[j]));
function adjoint(f,solution,trace=false){if(f.objective==='inverse')f.target();const L=f.linearize(solution.U),A=assemble(L,f.nd),AT=transpose(A),rhs=L.vjp(Array(f.nd).fill(0),1).U.map(v=>-v),F=factor(AT,trace),s=solve(F,rhs),r=L.vjp(s.x).U.map((v,i)=>v-rhs[i]),matrixNorm=Math.max(...AT.map(row=>row.reduce((s,v)=>s+Math.abs(v),0))),max=a=>Math.max(...a.map(Math.abs)),den=matrixNorm*max(s.x)+max(rhs);return {...s,A,AT,rhs,F,residual:norm(r),backwardError:den?max(r)/den:0,gradient:L.vjp(s.x,1).p};}
const api={assemble,transpose,factor,solve,adjoint};if(typeof module!=='undefined')module.exports=api;else root.QDirect=api;
})(typeof window!=='undefined'?window:globalThis);

附录 D:光滑性、limiter 与求导收敛

Appendix D: smoothness, limiters and derivative convergence

先分辨四种不同问题:原始残差是否收敛、经典导数是否存在、固定线性系统是否解收敛、网格加密后是否逼近物理敏感度。实验不预设硬 limiter 必然发散,也不把光滑 limiter 当作收敛保证。

Separate four questions: convergence of the primal residual, existence of a classical derivative, convergence of a fixed linear solve, and approximation of physical sensitivities under refinement. A hard limiter need not diverge, and a smooth limiter does not guarantee convergence.

本附录用独立的标量 Burgers 实验隔离机制;它不使用顶部 Euler 的网格或边界参数。Euler 的完整激波捕捉与 limiter 导数对照见本页 B.6。

This appendix isolates mechanisms with independent scalar Burgers experiments, separate from the Euler controls above. Euler shock capturing and limiter differentiation are in B.6.

D.1 光滑的线性分布,也会触及 minmod 切换

D.1 Even a smooth linear profile can reach a minmod switch

ui−1=uc−a,ui=uc+μ,ui+1=uc+a,si=minmod⁡(a+μ,a−μ)=a−∣μ∣(∣μ∣<a).u_{i-1}=u_c-a,\quad u_i=u_c+\mu,\quad u_{i+1}=u_c+a,\qquad s_i=\operatorname{minmod}(a+\mu,a-\mu)=a-|\mu|\quad(|\mu|<a).

μ=0 的左右导数分别为 +1 与 −1。程序相等时选择左分支,只给出该分支的线性化。中心差分得到零不代表导数存在。以下交互同时列出切换条件、局部 tangent 和反向累加权重。

At μ=0 the left and right derivatives are +1 and −1. The code selects the left branch at a tie, defining that branch’s linearization only. A zero central difference does not establish a derivative. Inspect the switch, local tangent and reverse accumulation weights below.

D.2 完整导数与冻结系数究竟差在哪里

D.2 Full derivatives versus frozen coefficients

uf=ui+ϕiΔi,u˙f=u˙i+ϕiΔ˙i+Δiϕ˙i.u_f=u_i+\phi_i\Delta_i,\qquad \dot u_f=\dot u_i+\phi_i\dot\Delta_i+\Delta_i\dot\phi_i.

冻结 limiter 系数删除最后一项;保存 minmod 的分支仍应对分支内部运算完整求导。两者都可能通过各自的转置点积测试,但冻结系数一般不是原始残差的完整导数。为了明确对比,冻结模式保持基态重构值相同,只替换线性化;它没有重新定义一个全局相同的可微残差。

Freezing the coefficient drops the final term. Saving a minmod branch still differentiates all operations within that branch. Both maps may pass their own transpose test, while coefficient freezing generally is not the full residual derivative. The frozen mode preserves the base reconstruction value and replaces only its linearization; it does not define the same globally differentiable residual.

sε(a,b)=ab(a+b)a2+b2+ε2,ε>0.s_\varepsilon(a,b)=\frac{ab(a+b)}{a^2+b^2+\varepsilon^2},\qquad \varepsilon>0.

光滑对照采用上述有理函数,对全部实数输入光滑;不再附加硬 min/max 或符号截断。它保持常值状态,但不是本页证明过的 TVD 或正性保持格式。ε 使用无量纲变量差分的尺度,改变 ε 就改变离散模型。Euler 若产生非正密度或压力、或超过固定耗散波速上界,将拒绝该状态而不静默裁剪。

The rational comparison is smooth for all real inputs, with no added hard min/max or sign clipping. It preserves constant states, but is not claimed to be TVD or positivity preserving. Epsilon uses the scale of nondimensional variable differences; changing it changes the discretization. Euler rejects nonpositive states or states beyond its fixed dissipation-speed bound rather than silently clipping them.

图 D.1 · 线性分布 uᵢ₋₁ = uc − a、uᵢ = uc + μ、uᵢ₊₁ = uc + a(a = 1)上的限幅斜率随 μ 的变化。minmod(a+μ, a−μ) 是帐篷形:μ = 0 处有尖点(左右导数 +1、−1),|μ| ≥ a 时两差异号,斜率为 0。有理对照 sε 处处光滑,μ = 0 处导数为 0;|μ| > a 时它变为负值,所以它不是 minmod 的光滑近似,而是另一个离散模型。−1.0−0.50.00.51.0−3−2−10123μminmod(a+μ, a−μ)sε, ε = 0.01sε, ε = 0.5斜率 +1斜率 −1两差异号:sε < 0
图 D.1 · 线性分布 ui−1 = uc − a、ui = uc + μ、ui+1 = uc + a(a = 1)上的限幅斜率随 μ 的变化。minmod(a+μ, a−μ) 是帐篷形:μ = 0 处有尖点(左右导数 +1、−1),|μ| ≥ a 时两差异号,斜率为 0。有理对照 sε 处处光滑,μ = 0 处导数为 0;|μ| > a 时它变为负值,所以它不是 minmod 的光滑近似,而是另一个离散模型。
Figure D.1 · Limited slope on the linear profile uᵢ₋₁ = uc − a, uᵢ = uc + μ, uᵢ₊₁ = uc + a (a = 1) as μ varies. minmod(a+μ, a−μ) is a tent: a kink at μ = 0 (one-sided derivatives +1 and −1), and slope 0 once |μ| ≥ a, where the two differences change sign. The rational comparison sε is smooth everywhere, with zero derivative at μ = 0; for |μ| > a it turns negative, so it is not a smooth approximation of minmod but a different discrete model.−1.0−0.50.00.51.0−3−2−10123μminmod(a+μ, a−μ)sε, ε = 0.01sε, ε = 0.5slope +1slope −1opposite signs: sε < 0
Figure D.1 · Limited slope on the linear profile ui−1 = uc − a, ui = uc + μ, ui+1 = uc + a (a = 1) as μ varies. minmod(a+μ, a−μ) is a tent: a kink at μ = 0 (one-sided derivatives +1 and −1), and slope 0 once |μ| ≥ a, where the two differences change sign. The rational comparison sε is smooth everywhere, with zero derivative at μ = 0; for |μ| > a it turns negative, so it is not a smooth approximation of minmod but a different discrete model.

逐步推导:minmod 的单侧导数与有理限幅的导数

Step by step: the one-sided derivatives of minmod and the derivatives of the rational limiter

在 D.1 的线性分布上,两侧差为 a+μa+\mu 与 a−μa-\mu。∣μ∣<a|\mu|<a 时二者同号,minmod 取绝对值较小者:

On the linear profile of D.1 the two differences are a+μa+\mu and a−μa-\mu. For ∣μ∣<a|\mu|<a they have the same sign and minmod takes the smaller magnitude:

s(μ)=a−∣μ∣,s(τ)−s(−τ)2τ=0,lim⁡τ→0+s(τ)−s(0)τ=−1,lim⁡τ→0+s(0)−s(−τ)τ=+1.\begin{gathered}s(\mu)=a-|\mu|,\qquad \frac{s(\tau)-s(-\tau)}{2\tau}=0,\\\lim_{\tau\to0^+}\frac{s(\tau)-s(0)}{\tau}=-1,\qquad \lim_{\tau\to0^+}\frac{s(0)-s(-\tau)}{\tau}=+1.\end{gathered}

中心差分为零,却既不是左导数也不是右导数;程序在相等时选左分支 d=a+μd=a+\mu,返回的线性化是 +1+1。有理对照把两个差 aa、bb 放进一个商,求导用商法则:

The central difference is zero, yet it is neither one-sided derivative; the program picks the left branch d=a+μd=a+\mu at the tie and returns the linearisation +1+1. The rational comparison puts the two differences aa, bb into one quotient, differentiated by the quotient rule:

sε=Na2+b2+ε2,N=ab(a+b),∂sε∂a=b(2a+b)(a2+b2+ε2)−2a⋅ab(a+b)(a2+b2+ε2)2,∂sε∂b=a(a+2b)(a2+b2+ε2)−2b⋅ab(a+b)(a2+b2+ε2)2.\begin{gathered}s_\varepsilon=\frac{N}{a^2+b^2+\varepsilon^2},\qquad N=ab(a+b),\\\frac{\partial s_\varepsilon}{\partial a}=\frac{b(2a+b)(a^2+b^2+\varepsilon^2)-2a\cdot ab(a+b)}{(a^2+b^2+\varepsilon^2)^2},\\\frac{\partial s_\varepsilon}{\partial b}=\frac{a(a+2b)(a^2+b^2+\varepsilon^2)-2b\cdot ab(a+b)}{(a^2+b^2+\varepsilon^2)^2}.\end{gathered}

在相等点 a=b=ma=b=m,两个偏导相同:(2m4+3m2ε2)/(2m2+ε2)2(2m^4+3m^2\varepsilon^2)/(2m^2+\varepsilon^2)^2,ε→0\varepsilon\to0 时趋于 12\tfrac12,也就是中心平均的导数。本页默认 ε=0.01\varepsilon=0.01、m=1m=1 时为 0.500020.50002。沿 D.1 的切片(两差为 a0+μa_0+\mu、a0−μa_0-\mu),dsε/dμ=∂asε−∂bsεds_\varepsilon/d\mu=\partial_as_\varepsilon-\partial_bs_\varepsilon 在 μ=0\mu=0 按对称性为零:有理函数没有尖点。minmod 在同一点的所选分支导数是 (∂a,∂b)=(1,0)(\partial_a,\partial_b)=(1,0),与 (12,12)(\tfrac12,\tfrac12) 不同:两个模型在光滑单调区域也给出不同的线性化。

At the tie a=b=ma=b=m the two partials coincide: (2m4+3m2ε2)/(2m2+ε2)2(2m^4+3m^2\varepsilon^2)/(2m^2+\varepsilon^2)^2, which tends to 12\tfrac12, the derivative of the central average, as ε→0\varepsilon\to0. With this page's default ε=0.01\varepsilon=0.01 and m=1m=1 it equals 0.500020.50002. Along the slice of D.1 (differences a0+μa_0+\mu and a0−μa_0-\mu), dsε/dμ=∂asε−∂bsεds_\varepsilon/d\mu=\partial_as_\varepsilon-\partial_bs_\varepsilon vanishes at μ=0\mu=0 by symmetry: the rational function has no kink. The selected-branch derivative of minmod at the same point is (∂a,∂b)=(1,0)(\partial_a,\partial_b)=(1,0), not (12,12)(\tfrac12,\tfrac12): the two models linearise differently even in a smooth monotone region.

D.3 无激波稳态 Burgers:分支、矩阵和误差

D.3 Smooth steady Burgers: branches, matrices and errors

(12u2−νux)x=s(x,a),u∗(x)=1+asin⁡(2π(x−0.07)),a=0.2.\left(\tfrac12u^2-\nu u_x\right)_x=s(x,a),\quad u_*(x)=1+a\sin(2\pi(x-0.07)),\quad a=0.2.

两端给定解析 Dirichlet 值,源项积分由解析通量端点差计算。每次参数扰动都同时改变源项和边界,但数值未知量仍由残差求解,未填入解析解。设计变量为振幅 a,目标 Q=hΣ(1+xi)ui。一阶与二阶使用相同 Rusanov 波速 2 和中心黏性通量;边界扩散距离为 h/2,首末单元不重构。

Both ends use analytic Dirichlet data; integrated sources are analytic physical-flux differences. Perturbations change sources and boundaries together, but unknowns are solved from residuals, never filled with the analytic profile. The design variable is amplitude a and Q=hΣ(1+xi)ui. Both orders use Rusanov speed 2 and centred viscous fluxes; boundary diffusion distances are h/2, with no endpoint-cell reconstruction.

U = sampled reference as an initial guess only
repeat until ||R(U,a)|| is small:
    J = exact selected-branch Jacobian
    d = LU(J, -R)                 # small scalar experiment
    line-search U + step*d        # reject failed convergence
J v = -R_a;  J^T psi = -Q_U^T
g = Q_a + psi^T R_a
compare full-rerun FD, branch changes and LU/GS/GMRES

本标量实验用小矩阵 LU 及回溯得到原始状态,以隔离导数线性系统的求解困难。随后在同一个固定状态上比较 GS 缺陷校正、GS 预处理 GMRES 与 LU。精确算子通过计算图 JVP/VJP;一阶矩阵仅构造预处理,伴随使用整个预处理映射的转置。

This scalar experiment uses small-matrix LU and backtracking for the primal, isolating difficulty in derivative linear solves. At the same fixed state, compare GS defect correction, GS-preconditioned GMRES and LU. Exact actions use graph JVP/VJP; a first-order matrix builds the preconditioner, whose entire map is transposed for the adjoint.

Jv=−Ra,JTψ=−QUT,ek+1=(I−ωBJ)ek.J v=-R_a,\quad J^T\psi=-Q_U^T,\quad e^{k+1}=(I-\omega BJ)e^k.

GS 发散而 LU 后向残差很小,不能解释成“导数不存在”。反过来,求解器收敛也不能证明 minmod 切换点存在唯一导数。下方显示线性真残差、无穷范数条件数、迭代矩阵范数、原始分支变化和完整重算差分。没有计算精确谱半径:范数大于 1 不是发散证明。

GS divergence with a small LU backward residual does not establish nonexistence of a derivative. Conversely, linear convergence does not establish a unique derivative at a minmod switch. Inspect true residuals, infinity-norm conditioning, iteration-matrix norm, primal branch changes and full-rerun differences. Exact spectral radii are not computed: a norm above one is not proof of divergence.

D.4 固定物理时间:峰值增长还是求导失稳

D.4 Fixed physical time: concentration or derivative instability

ut+(u2/2)x=νuxx,u(x,0)=12[1−tanh⁡(x−aδ)].u_t+(u^2/2)_x=\nu u_{xx},\qquad u(x,0)=\tfrac12\left[1-\tanh\left(\frac{x-a}{\delta}\right)\right].

独立非定常实验使用初值的精确单元平均,左/右外侧状态为 1/0,T=0.12。选择 ν、初始过渡层宽度 δ、网格及 limiter;只对位置 a=0.4 求导。SSP-RK2 的时间步数由网格、固定波速 1.5 和黏性稳定性上界确定,在位置差分中保持不变。所有 RK 子阶段的完整导数都进入 tangent 与反向时间 adjoint。

The independent transient experiment uses exact initial cell averages, exterior states 1/0 and T=0.12. Choose viscosity, initial width, grid and limiter; differentiate only position a=0.4. SSP-RK2 step count uses the mesh, fixed speed 1.5 and a viscous stability bound, and remains unchanged in position perturbations. Both RK stages contribute fully to the tangent and reverse-time adjoint.

U˙n+1=Φn,UU˙n,Uˉn=Φn,UTUˉn+1,UˉN=QUNT(vn≡U˙n).\dot U^{n+1}=\Phi_{n,U}\dot U^n,\qquad \bar U^n=\Phi_{n,U}^T\bar U^{n+1},\qquad \bar U^N=Q_{U^N}^T\qquad(v^n\equiv\dot U^n).

黏性 ν 控制 PDE,ε 控制重构映射,δ 控制初始层宽度,三者不能混为一谈。先固定 ν、δ 加密,再改变 ν 或 δ。图同时显示 max|v|、hΣ|v| 和积分梯度;对于移动间断的无黏极限,tangent 可以集中为分布,峰值增大而积分敏感度仍有限。这里不给一般激波的连续伴随收敛保证。

Viscosity changes the PDE, epsilon changes reconstruction, and delta changes initial thickness. First refine at fixed viscosity and width, then vary one parameter. Compare max|v|, hΣ|v| and integral gradients. A moving discontinuity’s inviscid-limit tangent can concentrate as a distribution while integrated sensitivity stays finite. No general continuous-adjoint convergence guarantee is asserted.

逐步推导:初值导数为什么越来越尖

Step by step: why the initial derivative sharpens

对初值直接求导,tanh⁡\tanh 的导数是 sech⁡2\operatorname{sech}^2:

Differentiate the initial data directly; the derivative of tanh⁡\tanh is sech⁡2\operatorname{sech}^2:

∂u(x,0)∂a=12δsech⁡2(x−aδ),max⁡x∂u∂a=12δ,∫−∞∞∂u∂a dx=12[tanh⁡x−aδ]−∞∞=1.\begin{gathered}\frac{\partial u(x,0)}{\partial a}=\frac1{2\delta}\operatorname{sech}^2\Bigl(\frac{x-a}\delta\Bigr),\qquad \max_x\frac{\partial u}{\partial a}=\frac1{2\delta},\\\int_{-\infty}^{\infty}\frac{\partial u}{\partial a}\,dx=\frac12\Bigl[\tanh\frac{x-a}\delta\Bigr]_{-\infty}^{\infty}=1.\end{gathered}

峰值与 1/δ1/\delta 成正比,面积却恒为 uL−uR=1u_L-u_R=1:δ→0\delta\to0 时导数趋于 [u]δ(x−a)[u]\delta(x-a)。离散初值用精确单元平均,单元 ii 的导数是 [tanh⁡((x−a)/δ)]xi−1/2xi+1/2/(2h)\bigl[\tanh((x-a)/\delta)\bigr]_{x_{i-1/2}}^{x_{i+1/2}}/(2h),不超过 1/h1/h:峰值由 min⁡(1/(2δ),1/h)\min(1/(2\delta),1/h) 决定。所以“最大值随加密增大”是分布型导数的正常表现;应当检查的是积分量(例如 dQ/dadQ/da)是否收敛。

The peak is proportional to 1/δ1/\delta while the area stays uL−uR=1u_L-u_R=1: as δ→0\delta\to0 the derivative tends to [u]δ(x−a)[u]\delta(x-a). The discrete initial data use exact cell averages, whose derivative in cell ii is [tanh⁡((x−a)/δ)]xi−1/2xi+1/2/(2h)\bigl[\tanh((x-a)/\delta)\bigr]_{x_{i-1/2}}^{x_{i+1/2}}/(2h), at most 1/h1/h: the peak is set by min⁡(1/(2δ),1/h)\min(1/(2\delta),1/h). A maximum that grows under refinement is therefore normal for a distributional derivative; what must converge are integrated quantities such as dQ/dadQ/da.

图 D.2 · 初值 u(x,0) = ½[1 − tanh((x − a)/δ)] 对位置 a 的导数,δ = 0.05、0.025、0.0125。峰值 1/(2δ) 随 δ 减小而增大,曲线下面积始终是 uL − uR = 1:极限是分布 [u]δ(x − a),与附录 B 的激波位移导数相同。0102030400.20.30.40.50.6x∂u(x,0)/∂aδ = 0.05(峰值 10)δ = 0.025(峰值 20)δ = 0.0125(峰值 40)
图 D.2 · 初值 u(x,0) = ½[1 − tanh((x − a)/δ)] 对位置 a 的导数,δ = 0.05、0.025、0.0125。峰值 1/(2δ) 随 δ 减小而增大,曲线下面积始终是 uL − uR = 1:极限是分布 [u]δ(x − a),与附录 B 的激波位移导数相同。
Figure D.2 · Derivative of the initial data u(x,0) = ½[1 − tanh((x − a)/δ)] with respect to the position a, for δ = 0.05, 0.025, 0.0125. The peak 1/(2δ) grows as δ shrinks while the area under each curve stays uL − uR = 1: the limit is the distribution [u]δ(x − a), the same as the shock-displacement derivative of Appendix B.0102030400.20.30.40.50.6x∂u(x,0)/∂aδ = 0.05 (peak 10)δ = 0.025 (peak 20)δ = 0.0125 (peak 40)
Figure D.2 · Derivative of the initial data u(x,0) = ½[1 − tanh((x − a)/δ)] with respect to the position a, for δ = 0.05, 0.025, 0.0125. The peak 1/(2δ) grows as δ shrinks while the area under each curve stays uL − uR = 1: the limit is the distribution [u]δ(x − a), the same as the shock-displacement derivative of Appendix B.

D.5 阅读结果的顺序

D.5 How to interpret results

先检查原始真残差与状态有效性,再判断 limiter 分支和单侧导数,再比较同一个线性系统的 GS/GMRES/LU,最后进行网格加密及连续参考比较。两种转置矩阵有相同的二范数条件数;迭代差异要从预处理、右端项、非正规增长及停止标准解释,不能笼统归因于“伴随更不稳定”。

Check primal residual and admissibility first, then limiter branches and one-sided derivatives, then GS/GMRES/LU on the same system, and finally refinement against continuous references. A matrix and its transpose have the same 2-norm condition number; iteration differences require examining preconditioners, right-hand sides, nonnormal growth and stopping criteria, not a blanket claim that adjoints are less stable.

查看完整独立标量实验与线性诊断实现Complete scalar experiment and linear diagnostics
/* Small educational systems: matrix-free exact products; explicit matrices only for LU/conditioning diagnostics. */
(function(root){'use strict';
const Q=typeof module!=='undefined'?require('../q1d/reconstruction/math'):root.QMath;
const {norm,dot}=Q,zeros=n=>Array(n).fill(0),transpose=A=>A[0].map((_,j)=>A.map(r=>r[j])),mv=(A,x)=>A.map(r=>dot(r,x));
function factor(A){const n=A.length,lu=A.map(r=>r.slice()),p=Array.from({length:n},(_,i)=>i),scale=Math.max(...A.flat().map(Math.abs));for(let k=0;k<n;k++){let j=k;for(let i=k+1;i<n;i++)if(Math.abs(lu[i][k])>Math.abs(lu[j][k]))j=i;if(!Number.isFinite(lu[j][k])||Math.abs(lu[j][k])<1e-14*scale)throw Error('Singular LU pivot '+k);[lu[j],lu[k]]=[lu[k],lu[j]];[p[j],p[k]]=[p[k],p[j]];for(let i=k+1;i<n;i++){lu[i][k]/=lu[k][k];for(let j=k+1;j<n;j++)lu[i][j]-=lu[i][k]*lu[k][j];}}return {lu,p};}
function solve(F,b){const {lu,p}=F,n=b.length,x=p.map(i=>b[i]);for(let i=0;i<n;i++)for(let j=0;j<i;j++)x[i]-=lu[i][j]*x[j];for(let i=n-1;i>=0;i--){for(let j=i+1;j<n;j++)x[i]-=lu[i][j]*x[j];x[i]/=lu[i][i];}return x;}
function assemble(L,n){const A=Array.from({length:n},()=>zeros(n));for(let j=0;j<n;j++){const e=zeros(n);e[j]=1;L.jvp(e).R.forEach((v,i)=>A[i][j]=v);}return A;}
function limiter(a,b,kind='minmod',eps=.01){if(kind==='first')return {s:0,da:0,db:0,branch:'first',margin:1};if(kind==='none')return {s:(a+b)/2,da:.5,db:.5,branch:'unlimited',margin:1};if(kind==='smooth'){const den=a*a+b*b+eps*eps,num=a*b*(a+b);return {s:num/den,da:((2*a*b+b*b)*den-2*a*num)/(den*den),db:((a*a+2*a*b)*den-2*b*num)/(den*den),branch:'smooth',margin:1};}const margin=Math.min(Math.abs(a),Math.abs(b),Math.abs(Math.abs(a)-Math.abs(b)));if(a*b<=0)return {s:0,da:0,db:0,branch:'zero',margin};return Math.abs(a)<=Math.abs(b)?{s:a,da:1,db:0,branch:'left',margin}:{s:b,da:0,db:1,branch:'right',margin};}
function slope(o,a,b,kind,eps,mode='full'){const r=limiter(o.value(a),o.value(b),kind,eps),{c,add,mul,sub,div}=o;if(mode==='frozen'){const d=(o.value(a)+o.value(b))/2,phi=Math.abs(d)>1e-15?r.s/d:0;return add(c(r.s),mul(c(phi/2),add(sub(a,c(o.value(a))),sub(b,c(o.value(b))))));}if(kind==='smooth'){const den=add(add(mul(a,a),mul(b,b)),c(eps*eps));return div(mul(mul(a,b),add(a,b)),den);}return add(c(r.s),add(mul(c(r.da),sub(a,c(o.value(a)))),mul(c(r.db),sub(b,c(o.value(b))))));}
function operators(U,p,evalFn,mode='full'){const t=Q.tape();t.op.value=i=>t.val[i];const ui=U.map(t.op.input),pi=p.map(t.op.input),r=evalFn(t.op,ui,pi,mode);return {R:r.R.map(i=>t.val[i]),J:t.val[r.J],jvp:(v,dp=zeros(p.length))=>{const z=t.forward([...v,...dp]);return {R:r.R.map(i=>z[i]),J:z[r.J]};},vjp:(w,j=0)=>{const z=t.reverse([...r.R,r.J],[...w,j]);return {U:z.slice(0,U.length),p:z.slice(U.length)};}};}
const real={...Q.real,value:x=>x};
function gsMatrix(P,sweeps=2){const n=P.length,B=Array.from({length:n},()=>zeros(n));for(let k=0;k<n;k++){const b=zeros(n),x=zeros(n);b[k]=1;for(let s=0;s<sweeps;s++)for(let i=0;i<n;i++){let v=b[i];for(let j=0;j<n;j++)if(i!==j)v-=P[i][j]*x[j];if(Math.abs(P[i][i])<1e-14)throw Error('Zero GS diagonal');x[i]=v/P[i][i];}x.forEach((v,i)=>B[i][k]=v);}return B;}
function compare(L,P,rhs,adj=false,omega=.7,preAction=null){const n=rhs.length,A=assemble(L,n),AA=adj?transpose(A):A,F=factor(AA),exact=solve(F,rhs),B0=preAction?transpose(Array.from({length:n},(_,j)=>{const e=zeros(n);e[j]=1;return preAction(e);})):gsMatrix(P),B=adj?transpose(B0):B0,op=v=>adj?L.vjp(v).U:L.jvp(v).R,pre=v=>mv(B,v),gm=Q.gmres(op,rhs,pre,1e-11,n);let x=zeros(n),history=[],reason='limit';for(let k=0;k<250;k++){const ax=op(x),r=rhs.map((b,i)=>b-ax[i]),rn=norm(r);history.push(rn);if(rn<1e-10){reason='converged';break;}if(!Number.isFinite(rn)||rn>Math.max(1,norm(rhs))*1e8){reason='growth';break;}const d=pre(r);x=x.map((v,i)=>v+omega*d[i]);}
const inv=transpose(Array.from({length:n},(_,j)=>{const e=zeros(n);e[j]=1;return solve(F,e);})),ni=M=>Math.max(...M.map(r=>r.reduce((s,v)=>s+Math.abs(v),0))),cond=ni(AA)*ni(inv),M=AA.map((_,i)=>AA[0].map((_,j)=>(i===j?1:0)-omega*B[i].reduce((s,v,k)=>s+v*AA[k][j],0)));return {exact,luResidual:norm(op(exact).map((v,i)=>v-rhs[i])),gm,gs:{x,history,reason},conditionInfinity:cond,iterationNormInfinity:ni(M),A};}
const api={...Q,zeros,transpose,mv,factor,solve,assemble,limiter,slope,operators,real,gsMatrix,compare};if(typeof module!=='undefined')module.exports=api;else root.EduNumerics=api;
})(typeof window!=='undefined'?window:globalThis);

/* Independent scalar smoothness experiment. Cell averages, fixed mesh and fixed physical time grid. */
(function(root){'use strict';const N=typeof module!=='undefined'?require('./numerics'):root.EduNumerics,{real,zeros,norm}=N;
function model({n=16,kind='minmod',eps=.01,nu=.01,amplitude=.2,phase=.07,omega=.7}={}){
if(!Number.isInteger(n)||n<8||n>128||nu<0||nu>.1||eps<=0)throw Error('Invalid scalar experiment');const h=1/n,x=Array.from({length:n},(_,i)=>(i+.5)*h),params=[amplitude,phase],alpha=2;
function exact(o,x,p){return o.add(o.c(1),o.mul(p[0],o.sin(o.mul(o.c(2*Math.PI),o.sub(o.c(x),p[1])))));}
function derivative(o,x,p){return o.mul(o.mul(o.c(2*Math.PI),p[0]),o.cos(o.mul(o.c(2*Math.PI),o.sub(o.c(x),p[1]))));}
function evaluate(o,U,p,mode='full',lim=kind){const {c,add,sub,mul}=o,slopes=U.map((u,i)=>i===0||i===n-1?c(0):N.slope(o,sub(u,U[i-1]),sub(U[i+1],u),lim,eps,mode)),F=[];
for(let f=0;f<=n;f++){const L=f?add(U[f-1],mul(c(.5),slopes[f-1])):exact(o,0,p),R=f<n?sub(U[f],mul(c(.5),slopes[f])):exact(o,1,p);const left=f?U[f-1]:L,right=f<n?U[f]:R,distance=f===0||f===n?h/2:h;F[f]=sub(sub(mul(c(.25),add(mul(L,L),mul(R,R))),mul(c(alpha/2),sub(R,L))),mul(c(nu/distance),sub(right,left)));}
const physicalFlux=z=>{const u=exact(o,z,p);return sub(mul(c(.5),mul(u,u)),mul(c(nu),derivative(o,z,p)));};const R=U.map((u,i)=>sub(sub(F[i+1],F[i]),sub(physicalFlux((i+1)*h),physicalFlux(i*h)))),J=U.reduce((s,u,i)=>add(s,mul(c(h*(1+x[i])),u)),c(0));return {R,J};}
function linearize(U,p=params,mode='full',lim=kind){return N.operators(U,p,(o,u,p,m)=>evaluate(o,u,p,m,lim),mode);}
function branches(U){return U.map((u,i)=>i===0||i===n-1?'end':N.limiter(u-U[i-1],U[i+1]-u,kind,eps).branch);}
function solve(p=params,start){let U=start?start.slice():x.map(xx=>exact(real,xx,p)),history=[],switches=[],prev=branches(U),states=[];for(let k=0;k<70;k++){const L=linearize(U,p),r=norm(L.R);history.push(r);states.push(U.slice());const branch=branches(U);switches.push(branch.filter((v,i)=>v!==prev[i]).length);prev=branch;if(r<1e-11)return {U,history,switches,states,converged:true};const d=N.solve(N.factor(N.assemble(L,n)),L.R.map(v=>-v));let t=1,accepted=false;for(let j=0;j<25;j++){const v=U.map((u,i)=>u+t*d[i]);if(Math.min(...v)>.1&&Math.max(...v)<1.9&&norm(evaluate(real,v,p).R)<(1-1e-4*t)*r){U=v;accepted=true;break;}t*=.5;}if(!accepted)return {U,history,switches,states,converged:false,reason:'No residual-decreasing branch correction'};}return {U,history,switches,states,converged:false,reason:'Iteration limit'};}
function analyse(s,mode='full',parameter=0){if(!s.converged)throw Error('Steady primal has not converged');const L=linearize(s.U,params,mode),P=N.assemble(linearize(s.U,params,'full','first'),n),dp=[0,0];dp[parameter]=1;const d=L.jvp(zeros(n),dp),j=L.vjp(zeros(n),1),t=N.compare(L,P,d.R.map(v=>-v),false,omega),a=N.compare(L,P,j.U.map(v=>-v),true,omega),gradient=L.vjp(a.exact,1).p[parameter],tg=L.jvp(t.exact,dp).J,w=x.map(xx=>Math.cos(3*xx)),v=x.map(xx=>Math.sin(7*xx));return {L,t,a,gradient,tangentGradient:tg,dotError:Math.abs(N.dot(w,L.jvp(v).R)-N.dot(v,L.vjp(w).U))};}
return {n,h,x,params,kind,eps,nu,solve,analyse,linearize,branches,evaluate:(U,p=params)=>evaluate(real,U,p),exact:xx=>exact(real,xx,params)};
}
function transient({n=40,nu=.005,eps=.01,kind='minmod',width=.05,T=.12,position=.4}={}){
 if(n<8||n>160||nu<0||width<=0||eps<=0||T<0||T>.2)throw Error('Invalid transient experiment');const h=1/n,x=Array.from({length:n},(_,i)=>(i+.5)*h),alpha=1.5,steps=Math.max(1,Math.ceil(T/(.2/(alpha/h+2*nu/(h*h))))),dt=T/steps;
function spatial(o,U,p,mode){const {c,add,sub,mul}=o,sl=U.map((u,i)=>i===0||i===n-1?c(0):N.slope(o,sub(u,U[i-1]),sub(U[i+1],u),kind,eps,mode)),F=[];for(let f=0;f<=n;f++){const L=f?add(U[f-1],mul(c(.5),sl[f-1])):c(1),R=f<n?sub(U[f],mul(c(.5),sl[f])):c(0),ul=f?U[f-1]:c(1),ur=f<n?U[f]:c(0);F[f]=sub(sub(mul(c(.25),add(mul(L,L),mul(R,R))),mul(c(alpha/2),sub(R,L))),mul(c(nu/(f===0||f===n?h/2:h)),sub(ur,ul)));}return {R:U.map((_,i)=>mul(c(1/h),sub(F[i],F[i+1]))),J:c(0),boundaryFlux:sub(F[0],F[n])};}
// Exact cell averages of a tanh transition; initial derivative follows its moving centre.
const logcosh=z=>Math.abs(z)+Math.log1p(Math.exp(-2*Math.abs(z)))-Math.log(2),initial=a=>x.map((_,i)=>.5-.5*width/h*(logcosh(((i+1)*h-a)/width)-logcosh((i*h-a)/width))),initialDerivative=a=>x.map((_,i)=>.5/h*(Math.tanh(((i+1)*h-a)/width)-Math.tanh((i*h-a)/width)));
function run(a=position,keep=true,mode='full'){let U=initial(a),v=initialDerivative(a),history=[U.slice()],vs=[v.slice()],tape=[],massError=0;
for(let k=0;k<steps;k++){const A=N.operators(U,[],spatial,mode),y=U.map((u,i)=>u+dt*A.R[i]),B=N.operators(y,[],spatial,mode),next=U.map((u,i)=>.5*(u+y[i]+dt*B.R[i]));const va=A.jvp(v).R,vy=v.map((z,i)=>z+dt*va[i]),vb=B.jvp(vy).R;v=v.map((z,i)=>.5*(z+vy[i]+dt*vb[i]));massError=Math.max(massError,Math.abs(h*next.reduce((s,z,i)=>s+z-U[i],0)-.5*dt*(spatial(real,U,[],mode).boundaryFlux+spatial(real,y,[],mode).boundaryFlux)));U=next;if(keep){tape.push({A,B});history.push(U.slice());vs.push(v.slice());}}
const weights=x.map(xx=>h*(1+xx)),J=N.dot(weights,U);let w=weights.slice(),dual=Array(steps+1);dual[steps]=w.slice();if(keep)for(let k=steps-1;k>=0;k--){const {A,B}=tape[k],bw=B.vjp(w).U,wy=w.map((z,i)=>.5*(z+dt*bw[i])),aw=A.vjp(wy).U;w=w.map((z,i)=>.5*z+wy[i]+dt*aw[i]);dual[k]=w.slice();}return {U,J,history,vs,dual,forward:N.dot(weights,v),adjoint:keep?N.dot(w,initialDerivative(a)):null,maxTangent:Math.max(...v.map(Math.abs)),l1Tangent:h*v.reduce((s,z)=>s+Math.abs(z),0),massError};}
return {n,h,x,steps,dt,run,position,nu,width};}
const api={model,transient};if(typeof module!=='undefined')module.exports=api;else root.SmoothBurgers=api;
})(typeof window!=='undefined'?window:globalThis);