方程:一维定常层流 Navier–Stokes 方程组
The equations: steady one-dimensional laminar Navier–Stokes
先明确这个算例求什么
What this model actually solves
本页求解等截面、平面一维、可压缩层流的稳态问题。未知量只随轴向位置变化,横向速度为零;黏性来自分子应力,传热来自 Fourier 热传导。它不解析管壁边界层,不含湍流模型,也不是变截面喷管的黏性修正。教学重点是:同一段残差程序如何产生原始解、精确 tangent 乘积和精确转置乘积。
This page solves steady, compressible, plane one-dimensional laminar flow at constant area. Unknowns vary only axially and transverse velocity is zero. Molecular stress and Fourier conduction supply viscous transport. It resolves neither duct-wall boundary layers nor turbulence and is not a viscous correction to a variable-area nozzle. The teaching focus is how one residual program supplies the primal solution, exact tangent products and exact transpose products.
从守恒律到定常方程组
From conservation laws to the steady equations
一维可压缩流有三条守恒律:质量、动量和总能量。用守恒变量 表示时,任一区间内这三个量的变化只来自区间两端的通量和区间内的源项。写成微分形式,就是一维 Navier–Stokes 方程组:
One-dimensional compressible flow has three conservation laws: mass, momentum and total energy. In the conservative variables , the amount of each in an interval changes only through the fluxes at its two ends and the source inside it. In differential form these are the one-dimensional Navier–Stokes equations:
这里 是连续的守恒场, 是单位质量的总能量;第 2 节起的 、 是它的离散版本,附录 A 需要区分时把连续场写作 。对流通量 与 Euler 方程相同:流体携带质量、动量和能量,压力做功。新增的是黏性通量 :黏性正应力 传递动量,黏性功 与热流 传递能量;总通量是对流减黏性。状态和物性由下面的关系闭合:
Here is the continuous conservative field and the total energy per unit mass; the and of Section 2 onwards are its discrete versions, and Appendix A writes the continuous field where the two must be told apart. The convective flux is that of the Euler equations: the fluid carries mass, momentum and energy, and the pressure does work. New is the viscous flux : the viscous normal stress transfers momentum, and the viscous work and the heat flux transfer energy; the total flux is convection minus viscosity. The state and the transport properties are closed by
采用无量纲气体常数 1,、、。应力系数 来自三维 Newton 应力在平面一维运动下的 Stokes 假设。质量分量没有分子扩散。能量黏性通量同时包含黏性功和导热,漏掉任意一项都会改变原始问题和它的导数。Sutherland 式中的 , 是无量纲 Sutherland 常数,使 ;它不是比热比 。输入的 已按参考密度、长度和速度无量纲化,不再额外乘 。
The nondimensional gas constant is one, with , , . The factor follows from three-dimensional Newtonian stress under Stokes' hypothesis restricted to plane motion. There is no molecular mass diffusion. The viscous energy flux contains both viscous work and conduction; omitting either changes the primal problem and its derivative. In the Sutherland law, with the nondimensional Sutherland constant , so that ; it is not the ratio of specific heats . The input is already scaled by reference density, length and velocity; no additional is applied.
本页只研究稳态解。令 ,就得到要离散的方程组和它的边界条件(页顶默认的加热算例):
This page studies steady solutions only. Setting gives the equations to be discretised, with their boundary conditions (the heated case, the default at the top of the page):
解是光滑的,处处 且保持亚声速,没有激波,所以第 3 节的二阶格式使用不加 limiter 的线性重构。
The solution is smooth, with everywhere, and stays subsonic, without shocks, so the second-order scheme of Section 3 uses linear reconstruction without a limiter.
图 1把这个问题画在一张图上。上面是流道:热源 集中在区间中部,入口给定 3 个量,出口给定 2 个量。下面四幅小图是默认参数下的解:曲线是附录 A 的独立连续参考解(256 步 RK4 的射击法),圆点是本页算出的离散解(、二阶,节点值;第 2 节起讲怎样算)。四个量相对入口的变化都不到 1%,所以每幅图的纵轴只画很窄的一段。圆点与曲线之差是 时的离散误差,四个量中最大约 ,网格加密一倍后降到约 。最下面一行是六个设计分量的当前值。
Figure 1 draws the whole problem. On top is the duct: the heat source is concentrated in the middle of the interval, 3 quantities are prescribed at the inlet and 2 at the outlet. The four small panels below show the solution at the default parameters: the curves are the independent continuous reference of Appendix A (shooting with 256 RK4 steps), the dots the discrete solution this page computes (, second order, nodal values; Section 2 onwards shows how). Each quantity changes by less than 1% from its inlet value, so each vertical axis spans a narrow range. The dots differ from the curves by the discretisation error at , at most about over the four quantities, and about on a mesh twice as fine. The bottom line gives the current values of the six design components.
波速、特征与两端的条件个数
Wave speeds, characteristics and the conditions at each end
先看没有黏性和导热的部分,即 Euler 方程。对流通量的 Jacobian 有三个特征值 、、,其中 是声速:小扰动沿这三个速度传播,它们叫作特征速度。本页的流动处处满足 (入口 ),所以 :入口处三者依次为 、、。在入口, 与 两族特征进入区间, 一族离开;在出口只有 一族进入(图 2(a))。每一族进入的特征需要一个边界条件,所以 Euler 方程在入口给 2 个条件、在出口给 1 个,拟一维 Euler 页正是这样(入口给总压和总温,出口给静压)。
Start with the part without viscosity and conduction, the Euler equations. The Jacobian of the convective flux has three eigenvalues, , and , where is the speed of sound: small disturbances travel with these three speeds, the characteristic speeds. The flow on this page has everywhere (at the inlet ), so : at the inlet they are , and . At the inlet two families, and , enter the interval and leaves; at the outlet only enters (Figure 2(a)). Each entering family needs one boundary condition, so the Euler equations take 2 conditions at the inlet and 1 at the outlet, as on the quasi-1-D Euler page (total pressure and total temperature at the inlet, static pressure at the outlet).
黏性和导热改变了这个计数。 与 把 和 的二阶导数带进动量方程和能量方程,这两个方程于是有了抛物型(扩散)的部分:扰动不只沿特征传播,还同时向上游和下游扩散。定常时,动量方程对 、能量方程对 都是二阶的,各在两端要一个条件;质量方程没有扩散项,仍是一阶,由 从入口输运,只在入口要一个条件。合起来,定常方程组的阶数是 :入口 3 个条件,出口 2 个(图 2(b))。本页入口给定 、、,出口给定法向总应力 与热流 :动量方程的两个条件是入口的 与出口的 ,能量方程的是入口的 与出口的 ,质量方程的是入口的 。
Viscosity and conduction change this count. and put second derivatives of and into the momentum and energy equations, which therefore gain a parabolic (diffusive) part: disturbances no longer travel only along characteristics but also diffuse upstream and downstream at the same time. In the steady equations the momentum equation is second order in and the energy equation second order in , so each takes one condition at each end; the mass equation has no diffusion, stays first order and is carried from the inlet by , so it takes one condition, at the inlet. In total the steady system has order : 3 conditions at the inlet and 2 at the outlet (Figure 2(b)). This page prescribes , and at the inlet and the total normal stress and the heat flux at the outlet: the momentum equation's two conditions are at the inlet and at the outlet, the energy equation's are at the inlet and at the outlet, and the mass equation's is at the inlet.
特征速度 并没有失去作用:它们仍然描述对流部分,第 3 节数值耗散的速度 就是面两侧 的平均。默认参数下 、,都是 1 的量级:黏性和导热在整个区间都起作用,而不只在两端的薄层里。数出条件的个数只说明每端要给几个量,它本身不证明问题适定;第 4 节把这些条件写成实际的边界映射。
The characteristic speeds keep their role: they still describe the convective part, and the dissipation speed of Section 3 is the average of on the two sides of a face. With the default parameters and are of order one: viscosity and conduction act over the whole interval, not only in thin layers at the ends. Counting conditions only says how many quantities each end needs; it does not by itself prove the problem well-posed, and Section 4 turns these conditions into the implemented boundary maps.
本页的问题:数据、设计变量与基准解
The problem on this page: data, design variables and the baseline solution
页顶的默认算例是一段受热的等截面流道,区间为 :入口 给定 、、,出口 给定 与 。热源只进入能量方程:
The default case at the top of the page is a heated duct of constant area on : the inlet prescribes , and , the outlet prescribes and . The heat source enters the energy equation only:
设计变量是六维向量 :两个热源幅度,两个物性参数(参考黏度与 Prandtl 数),两个边界数据——入口温度 与出口数据 (本算例是 )。按伴随优化文献的通常写法,设计变量记作 。第 5 节把 写成离散残差的第二个自变量。默认值是 (图 1底部),加入的总热量 。与 Burgers 页的设计(源项的值与入口值)不同, 与 出现在每个面的黏性通量里,所以对它们求导要经过所有的面(第 12 节)。页顶还可以改选“制造解”算例:入口相同,出口改为给定 、(此时 ),并加上一个由解析解构造的固定源项;第 15 节用它测精度阶。两种算例是不同的边值问题(第 4 节)。
The design is the six-component vector : two heating amplitudes, two transport parameters (the reference viscosity and the Prandtl number) and two boundary data, the inlet temperature and the outlet datum (here ). As is usual in the adjoint-optimization literature, the design variables are written . Section 5 makes the second argument of the discrete residual. The defaults are (bottom of Figure 1), so the heat added is . Unlike the design of the Burgers page (its source values and inlet value), and appear in the viscous flux of every face, so their derivatives run through all faces (Section 12). The top of the page also offers a manufactured case: the same inlet, an outlet that prescribes and (then ), and a fixed source built from an analytic solution; Section 15 uses it to measure the order of accuracy. The two cases are different boundary-value problems (Section 4).
基准解是均匀状态附近的一个光滑小扰动(图 1)。没有热源时,均匀状态 、、、 满足方程和全部 5 个边界条件()。加上热源后,温度从入口的 升到出口的 ,约升高 。压力几乎不变(与 相差不超过 );在近似等压下受热,气体膨胀,密度下降约 ,而质量通量 不变,速度相应上升约 (以上数值取自连续参考解)。
The baseline solution is a smooth, small departure from a uniform state (Figure 1). Without heating, the uniform state , , , satisfies the equations and all 5 boundary conditions (). With the heating, the temperature rises from at the inlet to at the outlet, by about . The pressure hardly changes (it stays within of ); heated at nearly constant pressure, the gas expands: its density falls by about and, since the mass flux is fixed, the velocity rises by about (values of the continuous reference).
对 积分一次,三个分量各给出一个守恒关系(本算例动量方程没有源项):
Integrating once in gives one conservation statement per component (the momentum equation has no source in this case):
质量通量处处相同,总动量通量 处处相同,总能量通量 从入口起每经过一段就增加这一段加入的热量;附录 A 正是用 和 把方程组化成常微分方程。图 3把能量通量拆成三部分:对流 、黏性功 和导热 。加入的热量 中,约 (55%)使出口处的对流通量高于入口,由流体从出口带走;约 (47%)由热传导逆着流动方向从入口离开区间:入口温度固定为 ,下游被加热,所以入口处 、;黏性功一项只变化 (−2%)。没有导热的 Euler 方程没有 :加入的热量只能全部由流体从出口带走。格点页的圆点略偏离曲线:入口节点的三行被约束替换,节点 0 的半个控制体不再满足平衡,内部面上的质量通量是 而不是 。这是 时的离散误差,网格加密一倍后质量通量为 (被替换的行见第 4 节)。
The mass flux is the same everywhere, so is the total momentum flux , and the total energy flux grows along the duct by the heat added on the way; Appendix A uses exactly and to turn the equations into ordinary differential equations. Figure 3 splits the energy flux into three parts: convection , viscous work and conduction . Of the heat added, , about (55%) raises the convective flux at the outlet above that at the inlet and is carried out of the outlet by the flow; about (47%) leaves the interval through the inlet by conduction, against the flow: the inlet temperature is held at and the gas downstream is heated, so at the inlet and . The viscous-work term changes by only (−2%). The Euler equations have no : there, all of the added heat would leave through the outlet with the flow. On this node page the dots sit slightly off the curves: the three rows of the inlet node are replaced by constraints, so the half volume of node 0 no longer balances, and the interior faces carry the mass flux instead of . This is the discretisation error at ; on a mesh twice as fine the mass flux is (Section 4 describes the replaced rows).
第 7 节给出设计的目的。默认目标是温度跟踪:让温度 接近目标温度 ,这是一个反设计问题。另两个可选目标是出口温度和入口热流 ;后者正是上面由导热从入口送出的那部分热流(为负值)。本页计算所选目标对全部 6 个设计分量的导数。
Section 7 gives the purpose of the design. The default objective is temperature tracking: bring the temperature close to the target , an inverse-design problem. The two other objectives are the outlet temperature and the inlet heat flux , which is exactly the conducted heat flux leaving through the inlet above (a negative number). The page computes the derivatives of the chosen objective with respect to all 6 design components.
这个方程组的作用
What these equations are for
在流体力学中,Navier–Stokes 方程组在 Euler 方程上加入分子黏性和热传导,是空气这类牛顿流体中黏性、导热流动的基本模型。黏性让相邻的流体层交换动量,导热让热量沿温度降低的方向扩散。雷诺数 比较对流与黏性扩散,Péclet 数 比较对流与导热;二者都很大时,黏性和导热只在薄层里起主要作用(边界层、激波内部),一维方程组的一个经典应用就是计算正激波的内部结构。本页的 、 都是 1 的量级,黏性和导热遍及整个区间,流动保持光滑、亚声速。
In fluid dynamics the Navier–Stokes equations add molecular viscosity and heat conduction to the Euler equations; they are the basic model of viscous, heat-conducting flow of a Newtonian fluid such as air. Viscosity exchanges momentum between neighbouring layers of fluid, and conduction carries heat towards lower temperature. The Reynolds number compares convection with viscous diffusion and the Péclet number compares it with conduction; when both are large, viscosity and conduction matter mainly in thin layers (boundary layers, the inside of a shock), and a classical use of the one-dimensional equations is the internal structure of a normal shock. On this page and are of order one, so viscosity and conduction act over the whole interval and the flow stays smooth and subsonic.
在本课程里,本页排在拟一维 Euler 之后、二维层流 N–S 之前(图 4)。与前两页相比,它新增了下面几件事:
In this course the page comes after quasi-1-D Euler and before 2-D laminar N–S (Figure 4). Compared with the pages before it, it adds:
- 同一组未知量,多一个通量。每点仍是 、、 三个守恒量,与拟一维 Euler 相同;面上的通量从 变成 。
- The same unknowns, one more flux. Each point still carries the three conservative quantities , , , as on the quasi-1-D Euler page; the face flux becomes instead of .
- 面上要用梯度。黏性通量需要 、。离散时它们来自面两侧状态之差,而面物性来自两侧的平均状态:一个面有两条依赖路径(第 3 节),前向与伴随都要沿两条路径求导。
- Gradients at faces. The viscous flux needs and . Discretely they come from differences of the states on the two sides of a face, while the face properties come from the averaged state: a face has two dependency paths (Section 3), and the tangent and the adjoint must follow both.
- 随温度变化的物性。选 Sutherland 物性时, 与 把对温度的导数带进每个黏性通量。
- Temperature-dependent properties. With Sutherland transport, and bring derivatives with respect to temperature into every viscous flux.
- 设计变量进入物理模型。 与 出现在每个面的黏性通量里,而不只在源项或边界上(第 12 节)。
- Design variables inside the physics. and appear in the viscous flux of every face, not only in a source or at a boundary (Section 12).
- 更多的边界条件。入口 3 个、出口 2 个,出口条件本身含应力与热流;格点页用约束替换 5 行残差(第 4 节)。
- More boundary conditions. 3 at the inlet and 2 at the outlet, where the conditions involve the stress and the heat flux; the node page replaces 5 residual rows by constraints (Section 4).
- 独立的参考解。这里没有 Burgers 页那样的闭式解;附录 A 用射击法解一个独立的连续边值问题,给出参考解和连续伴随,第 15 节用制造解测精度阶。
- An independent reference. There is no closed form as on the Burgers page; Appendix A solves an independent continuous boundary-value problem by shooting for a reference solution and the continuous adjoint, and Section 15 measures the order of accuracy with a manufactured solution.
图 4把本页放在整个网站的路线上。上排是一维 Burgers 页画出的主线,每往右一步主要增加一类新内容,每点的未知量依次为 1、3、4、4、5 个:一维 Burgers、拟一维 Euler、二维 Euler、二维层流 N–S、二维 RANS–SA。本页在主线下方:从拟一维 Euler 出发,先在一维里加入黏性和导热,再到二维层流 N–S 加上第二个空间方向;主线上的二维 Euler 则先加方向、后加黏性。二维层流 N–S 页使用同样的物性模型(常物性或 Sutherland 黏度,),但应力是 张量,面上的梯度要由最小二乘重构得到。图中每个方框都链接到该页的第 1 节(同一种格式)。
Figure 4 places this page on the route through the site. The top row is the main line drawn on the 1-D Burgers page; each step to the right mainly adds one kind of ingredient, and the unknowns per point are 1, 3, 4, 4, 5: 1-D Burgers, quasi-1-D Euler, 2-D Euler, 2-D laminar N–S, 2-D RANS–SA. This page sits below the main line: starting from quasi-1-D Euler it adds viscosity and conduction while staying in one dimension, and 2-D laminar N–S then adds the second direction; along the main line, 2-D Euler adds the direction first and viscosity afterwards. The 2-D laminar N–S page uses the same transport model (constant or Sutherland viscosity, ), but its stress is a tensor and its face gradients come from least-squares reconstructions. Every box of the figure links to Section 1 of its page (same scheme).
本页要回答的问题
The question this page answers
本页只回答一个问题:设计 的 6 个分量各改变一点,目标 改变多少?页首的全页路线图把后面各节串成一条线。按页首默认设置(二阶、、块 GS 预处理的 GMRES)实际计数,6 次前向求解共用 206 次 GMRES 迭代,得到 6 个方向导数;1 次伴随求解只用 35 次,就给出全部 6 个分量。
This page answers one question: how much does the objective change when each of the 6 components of the design 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, , block-GS-preconditioned GMRES), the 6 forward solves take 206 GMRES iterations in total for 6 directional derivatives, while the single adjoint solve takes 35 and gives all 6 components.
要在计算机上回答这个问题,先要把连续方程组变成有限个未知量的代数方程。第 2 节选定网格,决定三个守恒量存放在哪里;第 3 节再把第 1 节的通量写到每个面上。
To answer it on a computer, the continuous equations must first become algebraic equations in finitely many unknowns. Section 2 chooses the mesh and decides where the three conservative quantities are stored; Section 3 writes the fluxes of Section 1 on every face.
网格:未知量与数组编号
The mesh: unknowns and array indexing
第 1 节的方程组在区间上的每一个 都成立,计算机却只能存有限个数。本节选定网格,决定三个守恒量存放在哪里、数组怎样编号;第 3 节再把第 1 节的通量写到每个面上。
The equations of Section 1 hold at every of the interval, but a computer stores finitely many numbers. This section chooses the mesh, where the three conservative quantities are stored and how the arrays are indexed; Section 3 then writes the fluxes of Section 1 on every face.
网格、数组和索引
Mesh, arrays and indices
把 分为 个等长区间,。存储点为 ,,共有 个状态块。内部对偶面是相邻节点中点,两端面为 0、1;端点控制体宽度为 ,内部为 。端点状态也是未知量,边界条件通过残差行替换施加。
Split into equal intervals with . Store state blocks at , . Interior dual faces lie halfway between nodes and endpoint faces are 0 and 1. Endpoint volumes have width , interior volumes . Endpoint states remain unknowns; boundary conditions replace residual rows.
代码从 0 开始编号, 的三个分量在全局数组中分别占 ;公式里的分量下标写作 1、2、3(质量、动量、能量),对应槽位 0、1、2。这里 是动量密度,不是网格点数; 是总能量密度,不是比内能。内部面 的左、右状态编号是 、。下图横线上的点是状态存储位置,竖线是控制体面;数值来自当前求解。
Code indices start at zero: the components of occupy global slots ; in formulas the component subscripts 1, 2, 3 (mass, momentum, energy) correspond to slots 0, 1, 2. Here is momentum density, not a point count, and is total energy density, not specific internal energy. Interior face has left and right indices , . Dots below locate stored states and vertical lines mark control-volume faces; values come from the current solve.
格点页的存储值是节点守恒量,由它转换的温度是节点处的点值;精度检验与解析节点值比较(第 15 节)。下标 标记整个离散问题,不表示连续场(例如连续伴随)的采样,也不是差分验证的参数步长。
On the node page the stored values are nodal conservative states, and the temperature converted from them is a point value at the node; accuracy checks compare with exact nodal values (Section 15). The subscript labels the discrete problem, not samples of a continuous field (such as a sampled continuous adjoint) or a parameter-difference step.
网格和数组定下之后,下一步是面上的通量:第 3 节由两侧存储的状态算出每个内部面上的数值通量,对流部分来自重构的左右状态和数值耗散,黏性部分来自相邻状态之差。图 5 预览了整条计算链:第 3 节起逐步搭建,第 8、9 节对它求导。
With the mesh and the arrays fixed, the next step is the flux on a face: Section 3 computes the numerical flux on every interior face from the stored states on its two sides, the convective part from reconstructed left and right states with numerical dissipation, the viscous part from differences of neighbouring states. Figure 5 previews the whole chain: Section 3 onwards builds it, and Sections 8 and 9 differentiate it.
面上的通量:重构与黏性梯度
The flux on a face: reconstruction and viscous gradients
本节实验:先读下表每个面的离散梯度、面物性、应力与热流,再对照图 6 的两条路径:哪几列来自面平均状态,哪几列来自左右原始量之差?
Experiment: Read the face gradients, face transport, stress and heat flux in the table below, then match them with the two paths of Figure 6: which columns come from the averaged face state and which from differences of left and right primitive values?
第 2 节决定了状态存放在哪里、数组怎样编号;本节从这些存储的状态出发,算出内部面上的通量:先分清物理模型与离散运算,再写对流面状态的重构和黏性面的两条插值路径。
Section 2 decided where the states are stored and how the arrays are indexed; this section starts from those stored states and computes the flux on an interior face: first the line between the physical model and the discrete operations, then the reconstruction of the convective face states and the two interpolation paths of a viscous face.
物理模型与离散运算的分界
Separate the physical model from discrete operations
面上用的物理通量就是第 1 节的对流通量与黏性通量。下面先给出状态函数;它们接收一个三分量状态,而不是整个网格。第 1 节本构关系中的速度和温度空间导数,在程序里必须由明确的差分模板替代;从本节起,黏性面梯度统一记作 。
The physical flux on a face is the convective and viscous flux of Section 1. The state functions below accept one three-component state, not the whole mesh. Spatial derivatives in the continuous constitutive law of Section 1 must be replaced by specified difference stencils in the program. Discrete viscous face gradients are denoted throughout the main text.
这就是第 1 节的 与 ,只是面上的 、 换成了差分 、。总通量为对流减黏性:黏性能量通量是 ,减去它后热流以 进入净通量;而 本身带负号,所以净通量中的导热项是 。
These are the and of Section 1 with and replaced on the face by the differences and . Net flux is convection minus viscosity: the viscous energy flux is , so subtracting it puts into the net flux, and since itself carries a minus sign, the conduction term in the net flux is .
面物性按第 1 节的物性模型,在面温度 (下文)上求值。一阶/二阶开关只改变对流和目标求积状态的重构;两种模式都保留中心黏性差分、同一边界闭合和物性模型。格点端点的三点单侧梯度对光滑点值为二阶;格心页的边界模板作用于单元平均,不能据此宣称也为二阶(见格心页第 4 节)。Rusanov 形式的数值耗散不等于分子黏性。
The face transport follows the transport law of Section 1, evaluated at the face temperature (below). The order switch changes reconstruction for convection and objective quadrature states. Both modes retain centred viscous differences, the same boundary closures and the same transport law. Three-point node endpoint gradients are second order for smooth point values; the boundary stencils of the cell page act on cell averages and are not second order for that reason (Section 4 of the cell page). Rusanov-form numerical dissipation is not molecular viscosity.
把二阶重构写成有限个权重
Write second-order reconstruction as finite weights
重构直接作用于守恒量,没有 limiter。 是当前固定网格上的已知数,不是待求的连续微分算子。在内部面取 、。边界面另按第 4 节处理,不能把内部双侧公式套到不存在的外侧单元。
Reconstruction acts on conservative variables without a limiter. Each is a known number on the fixed mesh, not an unknown continuous differential operator. At an interior face use and . Section 4 supplies boundary closures; do not apply an interior two-sided formula to a nonexistent exterior cell.
当前开放参数限定在正向、光滑、亚声速分支。平均波速用于该分支的耗散,不宣称是任意间断下的最大特征速度界。即使一阶重构, 仍依赖状态,精确求导必须保留其变化。
The exposed parameter range targets a smooth positive-velocity subsonic branch. This averaged speed supplies dissipation on that branch; it is not claimed to bound all characteristic speeds across arbitrary jumps. Even with first-order reconstruction, remains state dependent and must be differentiated.
黏性面有两条不同的插值路径
Two distinct paths to a viscous face
面物性使用平均守恒状态转换出的温度;梯度使用左右原始变量之差。一般有 。这是两个不同的计算节点,tangent 与反向传播都必须保留两条路径。代码变量 ux、tx 对应这里的离散梯度,不代表解析求导。
Face transport uses temperature converted from the averaged conservative state, whereas gradients difference the separately converted primitive states. In general . These are distinct computational nodes; both tangent and reverse propagation retain both paths. Code variables ux and tx store these discrete gradients, not analytic derivatives.
以上公式处理的是内部对偶面:两侧各有一个节点。端点节点本身也是未知量;两个端面用端点状态的物理通量,黏性梯度改用单侧差分;面循环结束后,入口 3 行和出口 2 行残差被边界约束替换。第 4 节给出这些单侧梯度和约束。
These formulas treat interior dual faces, with a node on each side. The endpoint nodes are unknowns too: the two end faces use the physical flux of the endpoint state and one-sided differences for the viscous gradients, and after the face loop 3 inlet rows and 2 outlet rows of the residual are replaced by boundary constraints. Section 4 gives these one-sided gradients and constraints.
边界条件:规定哪些量,怎样进入通量
Boundary conditions: what is prescribed and how it enters the fluxes
先列出物理约束,再写成实际边界映射
From physical constraints to implemented boundary maps
第 1 节从方程的阶与特征的方向数出:入口要 3 个条件,出口要 2 个。本节把这些条件写成实际的边界映射。
Section 1 counted, from the order of the equations and the direction of the characteristics, 3 conditions at the inlet and 2 at the outlet. This section writes them as the implemented boundary maps.
加热问题入口规定 ,出口规定法向总应力 与 。它没有同时指定出口压力和零应力。制造解问题仍使用同一入口,出口改为规定 。两种闭合是不同的边值问题,不能把它们的伴随边界项混用。
The heated case prescribes at the inlet, and total normal stress with at the outlet. It does not simultaneously prescribe pressure and zero stress. The manufactured case uses the same inlet but prescribes outlet . These are different boundary-value problems and their adjoint boundary contributions cannot be interchanged.
格点:先计算通量,最后替换五行
Nodes: evaluate fluxes, then replace five rows
边界对流通量取端点存储状态的物理通量,黏性通量使用上面的单侧差分。随后,入口三个分量替换为以下约束(约束向量写作 ;字母 在本页只表示声速)。注意:存储的是守恒量,而约束包含速度、温度,因此这些约束的 Jacobian 不是三阶单位阵。
Boundary convection uses the physical flux of the endpoint stored state; viscosity uses the one-sided differences above. The inlet's three rows are then replaced by the following constraints (the constraint vector is written ; on this page the letter means only the sound speed). Stored variables are conservative, while constraints include velocity and temperature, so their Jacobian is not a three-by-three identity.
出口只替换动量行和能量行,质量平衡行保留。压力/应力约束依赖端点及单侧梯度涉及的两个上游点,热流约束同样具有邻点耦合。反向传播先提取这五行的种子,再把旧守恒行种子清零;约束反传可继续写入邻点,不能把所有边界状态导数清零。
Only outlet momentum and energy rows are replaced; the mass balance remains. The pressure/stress constraint couples the endpoint to the two upstream points in the one-sided gradient, as does the heat-flux constraint. Reverse propagation first extracts the five constraint seeds, then zeros the overwritten balance seeds. Constraint derivatives can still accumulate into neighbours; do not zero all boundary state derivatives.
下面先比较端点状态及应力/热流,再核对净边界通量减体源项与原始平衡行之和。格点版的最终约束行之和不是守恒收支。条件数量本身不能证明连续问题适定或离散矩阵非奇异;这些属于不同层次,需结合当前分支与验证结果判断。
The panel compares endpoint states, stress and heat flux, then checks boundary net flux minus volume sources against the sum of raw balance rows. For nodes, summing final constraint rows is not a conservation budget. Counting conditions alone proves neither continuous well-posedness nor discrete nonsingularity; these are separate questions about the current branch and its verification.
残差:面循环、源项求积与边界覆盖
Residual: face loop, source quadrature and boundary rows
实际求导对象是一个数组到数组的函数
The derivative acts on an array-to-array function
其中 是两个热源幅度, 是入口温度, 是出口边界数据:加热算例是出口法向总应力的给定值 (),制造解算例是出口温度 (第 4 节)。页顶参数选单在两种算例中都称它为“出口参数”;第 12–14 节与附录 A 的实验表格按算例标为 或 ,附录的公式写 ,它们是同一个分量。
Here are the two heat-source amplitudes, is the inlet temperature and is the outlet boundary datum: the prescribed normal total stress in the heated case (), and the outlet temperature in the manufactured case (Section 4). The parameter menu at the top calls it "Outlet parameter" in both cases, the lab tables of Sections 12–14 and Appendix A label it or according to the case, and the appendix formulas write : all name the same component.
固定网格、阶数、物性模式和边界类型后,程序接收 ,输出 。本文的离散伴随针对这个函数求导,不把连续偏微分算子及其边界条件直接搬来当作矩阵转置。制造源项在建立当前模型时固定;改变参数作局部求导时不重新制造。
With mesh, order, transport mode and boundary type fixed, the program maps to . The discrete adjoint differentiates this function; it does not substitute a continuous differential operator and its boundary conditions for a matrix transpose. Manufactured forcing is frozen when the current model is created, not regenerated during local parameter differentiation.
是控制体左右面,。源项使用两点 Gauss 求积;这里写的是程序实际求和,不是未经离散的积分。残差没有除以体积。一个内部面只求值一次,向左加、向右减;这样成对消去来自装配本身,而不是两个独立通量近似相等。
The control-volume faces are and . Sources use two-point Gauss quadrature: this is the actual program sum, not an undiscretized integral. Residuals are not divided by volume. Compute each interior face once, adding to the left and subtracting from the right. Pairwise cancellation follows from assembly itself, not approximate equality of independently evaluated fluxes.
加热模式的固定源项为零;制造解模式的固定源项由独立解析函数计算。它不依赖当前离散状态,所以本模型 ;对热源参数的偏导则非零。将来若引入状态相关源项,这个结论不再成立。
The fixed source is zero in heating mode and comes from independent analytic functions in manufactured mode. It is independent of the current discrete state, so in this model, while heat-parameter derivatives remain nonzero. This conclusion would change for a state-dependent source.
在五条被替换行取 0,其余对角元取 1; 把第 4 节的五个约束(向量 )插入相应行。这个写法把“覆盖数组元素”也纳入数学函数定义。
The diagonal mask is zero on the five replaced rows and one elsewhere; inserts Section 4's five constraints (the vector ). Thus overwriting array entries is part of the mathematical function definition.
Rraw[:] = 0
for f = 0..M:
traces = reconstruct_or_close_boundary(U, D, f)
face_state, Gu, GT = viscous_face_data(U, D, f)
H[f] = convective_flux(traces) - viscous_flux(face_state, Gu, GT, D)
if f > 0: Rraw[f-1] += H[f]
if f < M: Rraw[f] -= H[f]
for i = 0..M-1: Rraw[i] -= two_point_source_quadrature(i, D)
R = copy(Rraw)
if node_centred: overwrite_five_boundary_rows(R, U, D)播放器按这一装配顺序更新数组。先观察一个面的左右等量反号贡献,再跳到末尾检查源项及边界覆盖。原始量表、局部通量表和最终残差数组分别是不同层次,不能把局部通量的导数直接当作全局残差导数。
The player updates arrays in this assembly order. Inspect the equal and opposite contribution of one face, then advance to sources and boundary overwrites. Primitive states, local face fluxes and the final residual array are different levels; a local flux derivative alone is not the global residual derivative.
Jacobian 与块 GS:精确导数与近似求解矩阵
Jacobian and block GS: the exact derivative and the approximate solver matrix
精确 Jacobian 与近似求解矩阵
Exact Jacobian and approximate solve matrix
包含重构、原始变量转换、数值耗散、黏性差分、温度物性和边界闭合的全部状态依赖。 则固定状态数组,只改变参数。目标函数不属于残差 Jacobian;它另行给出第 7 节的右端向量。二阶重构使单个面的导数访问更多邻点,因此即使一维网格也不能想当然地只保留最近邻块。
The exact includes every state dependency through reconstruction, primitive conversion, numerical dissipation, viscous differences, temperature-dependent transport and boundary closure. The parameter Jacobian changes parameters while holding the state array fixed. The objective is not part of the residual Jacobian; it supplies the separate right-hand side in Section 7. Second-order reconstruction extends face dependencies to more neighbours, so even in one dimension nearest-neighbour blocks alone are not generally exact.
代码把同一当前状态送入一阶对流版本,构造 ;黏性、物性和边界仍完整保留。 分别是块对角、严格块下三角和严格块上三角,每个块为 。 表示从零初值出发的一次前向块 GS 扫描,不是 。即使一阶模式 ,一次扫描通常也不是线性方程的精确解。
The code constructs from first-order convection at the same current state, retaining full viscosity, transport and boundaries. Its block diagonal, strictly lower and strictly upper parts are , with blocks. The map is one forward block GS sweep starting from zero, not . Even when first-order mode gives , one sweep is generally not the exact linear solution.
先解非线性原始问题
Solve the nonlinear primal problem first
这是 Newton 修正;每一步重新线性化,用右预处理 GMRES 解线性系统,然后回溯选择步长。实际代码要求重构面状态保持正密度、正温度及指定正向亚声速范围,同时残差满足充分下降。改变步长是找解的策略,不是额外物理项。稳态伴随在最终收敛状态重新建立精确 Jacobian,不对回溯次数求导。
This is a Newton correction. Each iteration relinearizes, solves with right-preconditioned GMRES, and backtracks the step. The implementation checks reconstructed face states for positive density and temperature and the prescribed positive subsonic range, together with sufficient residual decrease. Step control is a solution strategy, not an extra physical term. The steady adjoint rebuilds the exact Jacobian at the converged state; it does not differentiate the number of backtracking steps.
下面第一张图是 Newton 非线性残差;选择某次迭代可查看对应状态。矩阵图为教学检查额外显式组装,不能据此把主导数接口称为非 matrix-free:主接口仍通过计算图计算乘积,显式矩阵用于结构观察、预处理及附录 C 的直接法。
The first plot shows the nonlinear Newton residual; selecting an iteration displays its state. The matrix plot is an extra explicit assembly for inspection. Main derivative interfaces still evaluate graph products without storing the exact matrix; explicit matrices serve visualization, preconditioning and Appendix C's direct method.
目标函数:温度跟踪、出口温度与入口热流
Objectives: temperature tracking, outlet temperature and inlet heat flux
先离散目标,再形成偏导向量
Discretize the objective before forming derivative vectors
本页统一把 定义为列向量,避免把偏导数行与梯度列直接相加。求 固定参数;求 固定所有状态槽位。伴随右端是 ,而最终六维设计梯度还包含 。它们的维数及物理含义不同。
This page consistently defines as columns, avoiding addition of derivative rows to gradient columns. Hold parameters fixed for , and every state slot fixed for . The adjoint right-hand side is , whereas the final six-dimensional design gradient also includes . Their dimensions and meanings are different.
温度跟踪:从重构到求积
Temperature tracking: reconstruction through quadrature
求积点与权重和源项一致,但求积状态需要重构,随后才转换成温度。固定网格和固定解析目标意味着点、权重与目标值对设计的 tangent 均为零;本页不做几何设计。用 表示存储状态的 tangent(前向模式的方向导数,点号约定见第 8 节),则逐项链式法则给出:
Quadrature points and weights match the source quadrature, but objective states are reconstructed before conversion to temperature. With this fixed mesh and fixed analytic target, points, weights and target samples have zero tangents with respect to the design; this page does not optimize geometry. With the tangent of a stored state (a forward-mode directional derivative; Section 8 sets out the dot convention), the chain rule gives:
这里 表示三个分量组成的块。求积权重已经进入右端,伴随方程中不能再乘一次体积。二阶目标会把一个求积点的权重分发给多个存储状态;只在最近状态上写目标梯度会漏掉重构路径。
Here is a three-component block. Quadrature weights already enter the right-hand side and must not be multiplied in again. A second-order objective scatters one quadrature point's contribution to several stored states; writing it only to the nearest state omits the reconstruction path.
端点目标:显式参数项不能省
Endpoint objectives retain explicit parameter terms
热流目标即使固定状态,也会随 改变;格心入口还显式依赖规定温度。制造解模式的出口温度在最终收敛后由边界给定,对热源幅度的总导数应为零。格心版可以直接通过目标的参数项得到,格点版则经强边界约束得到同一结论;不能把两版的中间偏导项强行要求相等。
Even with fixed state, heat flux changes with ; the cell inlet also explicitly depends on prescribed temperature. In manufactured mode the converged outlet temperature is prescribed, so its total heat-source derivative should vanish. Cells can express this directly through the objective's parameter term; nodes reach the same conclusion through their strong boundary constraint. Intermediate partial derivatives need not match between the two schemes.
前向:沿残差依赖链传播方向导数
Tangent: propagate directional derivatives along the residual chain
本节实验:一次 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.
点号表示有限维方向导数
A dot denotes a finite-dimensional directional derivative
状态种子 是存储状态的 tangent, 是六维参数种子(设计方向)。本页按自动微分的记法书写离散程序的导数:点号是前向模式(tangent)导数,第 9 节的上横线是反向模式(adjoint)导数;变分符号 只用于附录 A 中连续函数的变分。这里没有时间积分,点号也不是时间导数。计算 时令 ;计算 时令 。下面沿实际残差依赖链逐步传播,而不是近似差分一个 Jacobian。
The state seed is the tangent of the stored states and is the six-dimensional parameter seed (the design direction). This page writes derivatives of the discrete program in the notation of algorithmic differentiation: a dot is a forward-mode (tangent) derivative and the overbar of Section 9 a reverse-mode (adjoint) derivative, while the variation symbol is used only for variations of continuous functions in Appendix A. There is no time integration, so a dot is not a time derivative either. Set for and for . Follow the actual residual dependency chain below rather than approximating a Jacobian by finite differences.
第一步:守恒量转换的局部 Jacobian
Step 1: the local conservative-to-primitive Jacobian
上式 是一个局部三分量状态的 tangent,顺序为密度、动量密度、能量密度;对应代码分量槽位 0、1、2。网格位置另用 的下标 表示。 是 行,分别与三分量 tangent 相乘。所有系数在当前基态求值。
Here is the tangent of one local three-component state, ordered as density, momentum density and energy density, matching code component slots 0, 1 and 2. The subscript in separately identifies a mesh location. The rows have size and multiply a three-component tangent. Evaluate every coefficient at the current base state.
气体常数为 1,,所以压力可以直接写成守恒量的函数。它的行导数 与上式的 相同,后面的通量 Jacobian 与格点出口约束都用它:
With the gas constant 1 and , the pressure is a function of the conservative variables alone. Its row is the same as above, and the flux Jacobian and the node outlet constraint below use it:
第二步:对流面重构与耗散
Step 2: convective traces and dissipation
冻结耗散速度会删去倒数第二项 ,只保留耗散部分的最后一项,得到的是另一套线性算子。它可能通过自己的转置点积测试,但并不是当前残差的精确导数。边界面使用物理通量,没有内部 Rusanov 耗散;必须先求导边界状态映射。
Freezing dissipation speed drops the penultimate term and produces a different linear operator. That operator may pass its own transpose test while failing to differentiate the actual residual. Boundary faces use physical fluxes without interior Rusanov dissipation; differentiate their state closure first.
对流通量 Jacobian
The convective flux Jacobian
把 的三行按 合并,就是 Euler 通量的 Jacobian 矩阵( 是比总焓,气体常数为 1 时 ;本页的 是面上的净通量,与它无关):
Collecting the three rows of by gives the Jacobian of the Euler flux ( is the specific total enthalpy; with the gas constant 1, ; the of this page is the net flux through a face and has nothing to do with it):
第二行来自 ,第三行来自 ,逐项对 求偏导即得。例如在入口规定状态 、、:
The second row comes from and the third from , differentiated term by term with respect to . At the prescribed inlet state , , , for example:
这三个特征值就是第 1 节图 2 的入口特征速度。第 9 节的对流转置直接用 ;Rusanov 耗散还要 与 ,它们都在两侧重构状态上求值。
These eigenvalues are the inlet characteristic speeds of Figure 2 in Section 1. The convective transpose of Section 9 uses directly; the Rusanov dissipation also needs and , both evaluated on the two reconstructed traces.
第三步:两条黏性路径和物性
Step 3: both viscous paths and transport
第四步:黏性通量是面状态与面梯度的函数
Step 4: the viscous flux as a function of the face state and the face gradient
第三步逐项写出了各个 tangent。按输入分组,本构黏性通量只依赖三类量:面平均状态 (经过 、 以及物性 、)、两个面梯度 ,以及设计分量 。所以它的 tangent 是四个分块 Jacobian 之和( 在 上求值):
Step 3 wrote every tangent term by term. Grouped by input, the constitutive viscous flux depends on three kinds of quantity only: the averaged face state (through , and the transport , ), the two face gradients , and the design components . Its tangent is therefore a sum of four block Jacobians ( evaluated at ):
与 都和 成正比,所以整个本构黏性通量与 成正比,对它的导数就是通量本身除以 ; 只出现在 里,只改变能量分量中的导热。第 12 节用这两个恒等式把设计导数写成逐面求和。
Both and are proportional to , so the whole constitutive viscous flux is proportional to and its derivative is the flux itself divided by ; appears only in and changes only the conduction of the energy component. Section 12 uses these two identities to write the design derivatives as sums over faces.
物性对温度的导数与热流
The temperature derivative of the transport, and the heat flux
Sutherland 律取对数后逐项求导:
Take the logarithm of the Sutherland law and differentiate term by term:
所以在 附近温度升高 1% 时黏度约升高 0.79%;常物性时 。热流 同时经过梯度与导热系数,导热系数又经过温度和两个设计分量:
Near a 1% rise in temperature thus raises the viscosity by about 0.79%; with constant transport . The heat flux runs through both the gradient and the conductivity, and the conductivity through the temperature and two design components:
面梯度是一个有限模板
A face gradient is a finite stencil
内部面与两个端面的梯度都是存储状态的线性组合:
The gradients of interior faces and of the two end faces are linear combinations of stored states:
一阶与二阶的黏性模板相同。模板的 tangent 是“收集”:每个模板点贡献 ;第 9 节会看到它的伴随是用同一组权重“分发”(图 9)。
The viscous stencils are the same at first and second order. The tangent of a stencil gathers: every stencil point contributes ; Section 9 shows that its adjoint scatters with the same weights (Figure 9).
第五步:边界面、边界行与源项
Step 5: boundary faces, boundary rows and sources
以上内部面梯度在边界换成第 4 节的单侧线性组合,最后再用五条约束的 tangent 覆盖相应残差行。例如入口温度行的 tangent 是 ,不是能量槽位的 tangent 减 。
At boundaries replace the interior gradient formulas by Section 4's one-sided combinations, then overwrite the five rows with the tangents of the constraints. For example the tangent of the inlet temperature row is , not the energy-slot tangent minus .
格点页的两个端面在节点上:对流通量取端点状态的物理通量(没有 Rusanov 耗散),梯度用第四步的三点单侧模板,tangent 照第一到四步写出。随后五行被约束覆盖,它们的 tangent 也是覆盖而不是累加:
The two end faces of the node page sit on nodes: the convective flux is the physical flux of the end state (no Rusanov dissipation), the gradients use the three-point one-sided stencils of Step 4, and the tangents follow Steps 1–4. The five rows are then overwritten by the constraints, and so are their tangents, which replace rather than accumulate:
这里 是 的密度分量, 在节点 的温度上求值, 用出口模板。出口只覆盖动量行与能量行,质量行仍是平衡行,仍收到 的质量分量。
Here is the density component of , are evaluated at the temperature of node , and uses the outlet stencil. The outlet overwrites only the momentum and energy rows; its mass row stays a balance row and still receives the mass component of .
源项不依赖状态,它的 tangent 只来自两个热源幅度( 是第 12 节的求积权重):
The source does not depend on the state; its tangent comes from the two heat amplitudes only ( are the quadrature weights of Section 12):
U_d = v; D_d = design direction # tangent seeds
for scalar operation z = f(x1, ..., xn) in forward order:
z_d = sum(partial(f, xk) * xk_d)
# z = x*y: z_d = y*x_d + x*y_d
# evaluate the same reconstruction, face, source and boundary operations
return R_d, J_d # residual and objective tangents代码清单和播放器沿用 Tapenade 的命名:x_d 即 ,x_b 即 (生成的子程序称为 residual_d、residual_b)。播放器以固定的非零状态种子 和参数种子 逐步展示 (表中的 R_d)的装配。第 10 节才求解设计引起的状态响应;一个算子调用与一次线性求解不是同一件事。
Code listings and players follow Tapenade's naming: x_d is and x_b is (the generated routines are residual_d and residual_b). The player uses a fixed nonzero state seed and the parameter seed to display, step by step, the assembly of (R_d in the table). Section 10 solves the state response to a design direction; an operator application is not a linear solve.
算例:一个内部面的前向传播
Worked example: one interior face, forward
取页顶默认参数(N = 12,二阶,加热,常物性,温度跟踪)的收敛解,看第 个面(,左右是节点 5、6),设计方向 (只改变 ),状态种子 取第 10 节对这个方向解出的状态 tangent。下表每个量给出基态值,以及 tangent 的两部分:固定状态只改设计(,属于 ),与固定设计只改状态(,属于 )。
Take the converged solution at the page defaults (N = 12, second order, heated, constant transport, tracking) and face (, between nodes 5 and 6), the design direction (only changes), and as the state seed the state tangent that Section 10 solves for this direction. For each quantity the table gives the base value and the two parts of its tangent: the design varied with the state fixed (, part of ) and the state varied with the design fixed (, part of ).
| 量quantity | 基态值base value | 来自 ()from () | 来自 ()from () | tangent 合计tangent, total |
|---|---|---|---|---|
| 4.042×10⁻³ | 0 | −0.01832 | −0.01832 | |
| 8.837×10⁻³ | 0 | −0.04383 | −0.04383 | |
| 0.3512 | 0 | −5.108×10⁻³ | −5.108×10⁻³ | |
| 1.005 | 0 | −0.01791 | −0.01791 | |
| 0.1500 | 1.000 | 0 | 1.000 | |
| 0.7292 | 4.861 | 0 | 4.861 | |
| 8.084×10⁻⁴ | 5.389×10⁻³ | −3.664×10⁻³ | 1.725×10⁻³ | |
| −6.443×10⁻³ | −0.04296 | 0.03196 | −0.01099 | |
| 6.727×10⁻³ | 0.04485 | −0.03325 | 0.01159 | |
| 1.252 | 0 | −0.01795 | −0.01795 | |
| 1.123 | −5.389×10⁻³ | 2.925×10⁻³ | −2.464×10⁻³ | |
| 1.245 | −0.04485 | 0.01531 | −0.02954 |
读法:常物性时 ,所以 ,;固定状态时梯度不变,,。状态的响应使速度与温度梯度都变小(黏度与导热增大,剖面变平),抵消了一部分:合计 、。对流通量不含设计,它的 tangent 全部来自 。
Reading it: with constant transport , so and ; with the state fixed the gradients do not move, and . The response of the state makes both the velocity and the temperature gradient smaller (more viscosity and conduction flatten the profiles) and cancels part of this: in total and . The convective flux contains no design; its tangent comes entirely from .
把所有面的 分发到两侧、再减去源项的 tangent,就得到 ;因为 是解出的 tangent,结果在线性求解的精度内为零。
Scattering the of every face to its two sides and subtracting the source tangents gives ; since is the solved tangent, the result is zero to the accuracy of the linear solve.
图 7 用解出的 tangent 逐面播放前向扫描:设计方向 取页顶的“前向/连续对照方向”, 由第 10 节的求解器解出,按钮与本页其它播放器相同。每一步高亮当前面读取的存储状态:二阶对流重构读 到 ,黏性部分只读 、(端面读三个节点);然后把 加到左边控制体、从右边减去。条形是累加中的 :一个控制体收到两侧面的贡献之后就回到零附近,这正是 tangent 方程 逐行成立;热源方向要等最后减去源项的 tangent 才归零,格点页的五行最后被约束的 tangent 覆盖。表中给出当前面的 及其来自 与来自 的两部分,最后一步给出 。
Figure 7 plays the forward sweep face by face with the solved tangent: the design direction is the "tangent / reference direction" chosen at the top of the page, comes from the solver of Section 10, and the buttons are those of the page's other players. Each step highlights the stored states the current face reads: the second-order convective reconstruction reads to , the viscous part only and (an end face reads three nodes); then is added to the left control volume and subtracted from the right one. The bars are as it accumulates: once a control volume has received both of its faces it returns to near zero, which is the tangent equation holding row by row; a heat-source direction returns to zero only after the source tangents are subtracted, and on the node page the five rows are finally overwritten by the constraint tangents. The table gives the current face's and its parts from and from ; the last step gives .
伴随:先反转边界覆盖,再反转装配
Adjoint: undo the boundary overwrite, then reverse the assembly
本节实验:逐步播放反向累加,数一数同一存储状态从几个面、几条路径收到贡献;共享输入必须累加,不能覆盖。
Experiment: Step through the reverse accumulation and count how many faces and paths contribute to one stored state. Shared inputs must accumulate, not overwrite.
反向传播计算什么
What reverse propagation computes
残差种子 是任意的 维向量, 是目标种子;上横线表示标量 对某个计算节点的偏导,即反向模式导数,不是连续伴随场。取 得到矩阵转置乘积 ,取 得到目标梯度 。第 11 节的伴随解 就是一个特定的残差种子:取 、,反向扫描给出 ;取 ,则 ,设计伴随 就是总设计梯度。
The residual seed is an arbitrary vector of length and seeds the objective. An overbar is a partial derivative of the scalar with respect to a computational node, a reverse-mode derivative, not a continuous adjoint field. Seeds give transpose products ; gives the objective gradients . The adjoint solution of Section 11 is one particular residual seed: with and the reverse sweep gives ; with it gives , and the design adjoint is the total design gradient.
转置来自有限求和换序,没有分部积分。参数方向同样换序,就得到前向与反向模式之间的对偶(点积)恒等式:
The transpose follows from exchanging finite sums, without integration by parts. Exchanging the sums for the parameter direction as well gives the duality (dot-product) identity between forward and reverse mode:
它对任意种子 都成立,是检查转置实现的点积测试。反向时对每个标量操作倒序应用链式法则:先把全部累加器清零(),再逐条执行 ;共享输入必须累加。前向中一项被多次读取,反向就收到多条贡献。
It holds for any seeds and is the dot-product test of the transpose implementation. In reverse mode apply the chain rule in reverse operation order: clear every accumulator (), then execute statement by statement; shared inputs accumulate. Multiple forward reads become multiple reverse contributions.
点积测试能发现什么、不能发现什么,可以直接做实验(图 8)。第 8 节说过,冻结耗散速度会得到另一套线性算子。只在 tangent 里冻结,adjoint 仍对精确残差反向,两侧立刻不再相等;把 tangent 与 adjoint 一起冻结,两侧又相等到舍入误差,测试通过,可这对算子求的是另一个函数的导数,梯度与复数步长相差 到 (相对)。点积测试检查的是“互为转置”;导数是否完整,要靠第 13、14 节重新求解的差分与复数步长。
What the dot-product test can and cannot find is a direct experiment (Figure 8). Section 8 noted that freezing the dissipation speed gives another linear operator. Freeze it in the tangent only, keep the adjoint exact, and the two sides disagree at once; freeze it in both, and the two sides agree to round-off again, so the test passes, yet this pair differentiates another function and its gradient differs from the complex step by to (relative). The dot-product test checks transposition; whether the derivative is complete takes the re-solved differences and the complex step of Sections 13 and 14.
先反转边界覆盖,再反转装配
Reverse boundary overwrites before assembly
是原始平衡行的伴随:掩码已把被覆盖行的种子清零。边界面只有一侧接控制体,所以种子只有一项。强边界约束的反传单独累加到状态与参数;不能把已经覆盖掉的守恒行又反传一次。格心版掩码为单位阵且没有约束插入,但边界通量本身仍需反传。
is the adjoint of the raw balance rows; the mask has already zeroed the seeds of overwritten rows. A boundary face touches only one control volume, so its seed has a single contribution. Reverse strong constraints separately into states and parameters; do not differentiate overwritten balances again. Cells use an identity mask and no constraint insertion, but still reverse their boundary fluxes.
内部黏性面的逐项反传
Reverse an interior viscous face term by term
令 ,其三个分量按质量、动量、能量排列。因为净通量减去黏性通量,黏性部分先收到 。下面各累加器先清零(),再按显示顺序执行,即前向运算的倒序:
Let , ordered as mass, momentum and energy. Net flux subtracts viscosity, so the viscous path starts with . Clear these accumulators () and execute them in the displayed order, the reverse of the forward operations:
这几行展示了面平均和原始量差分的两条反向路径。常物性模式只令 ,不能删除对 的设计导数。格点页的加热出口是约束行 与 :它们的种子先从被替换的守恒行取出(第 4 节),再按约束的导数反传。约束里的 、 在格点页仍是本构量(出口的三点梯度,物性取节点 的温度),所以 、 接着沿上面的黏性链反传(见下面的边界段),只是没有内部面的面平均与 Rusanov 耗散。
These equations expose both reverse paths: face averaging and primitive differences. Constant transport sets only , retaining the design derivative with respect to . On the node page the heated outlet is the constraint rows and : their seeds are first taken from the replaced balance rows (Section 4) and then reversed through the constraint derivatives. The and inside the constraints are still constitutive on the node page (the three-point outlet gradient, the transport at the temperature of node ), so and then continue along the viscous chain above (see the boundary paragraph below), only without the face average and the Rusanov dissipation of an interior face.
对流与二阶重构的转置
Transpose convection and second-order reconstruction
黏性通量的转置(分块形式)
The viscous flux transposed (block form)
把第 8 节第四步的分块 Jacobian 转置,就是上面逐项反传的紧凑写法(;常物性时第一块只剩 ):
Transposing the block Jacobians of Step 4 in Section 8 gives the term-by-term reversal above in compact form (; with constant transport the first block reduces to ):
梯度模板的转置
The transpose of the gradient stencil
第四步的模板 反过来就是分发:每个模板点按同一个权重收到一份(图 9):
Reversed, the stencil of Step 4 scatters: every stencil point receives a share with the same weight (Figure 9):
内部面 ,所以 减、 加,正是上面最后一行的两个 项。一个存储状态出现在几个面的模板里,就从几个面收到贡献;这就是共享输入必须累加的原因。
At an interior face , so decreases and increases: these are the two terms of the last line above. A stored state that appears in the stencils of several faces receives a contribution from each; this is why shared inputs must accumulate.
边界面、边界行、源项与目标的反传
Boundary faces, boundary rows, sources and the objective in reverse
格点页先处理五个约束行:它们的种子就是 在这五行的分量,随后这五行的原始平衡被掩码清零(本节开头)。按第 8 节第五步倒序:
The node page first handles the five constraint rows: their seeds are the components of on these rows, and the raw balances of the rows are then masked (the start of this section). In the reverse order of Step 5 in Section 8:
、 再按上面的黏性链反传(物性在节点 的温度上求值),梯度沿三点单侧模板分发到 。温度跟踪目标 ,所以 、:入口温度约束行与出口应力约束行的乘子取负号,就是这两个边界数据的梯度。
and then reverse through the viscous chain above (with the transport evaluated at the temperature of node ), and the gradients scatter along the three-point one-sided stencil to . The tracking objective has , so and : the multipliers of the inlet-temperature and outlet-stress constraint rows, negated, are the gradients with respect to these two boundary data.
源项与目标在前向程序里排在面循环之后,所以反向时先于各个面反传(仅在五个约束行之后):
Sources and the objective come after the face loop in the forward program, so in reverse they are reversed before the faces (after the five constraint rows only):
、 是目标自己的反向扫描,第 11 节列出三种目标的公式。
and are the objective's own reverse sweep; Section 11 lists them for the three objectives.
整个面循环:语句、tangent 与 adjoint
The whole face loop: statement, tangent and adjoint
下表把一个内部面按代码的执行顺序逐条列出(core.js 的 evaluate)。tangent 一列从上往下执行;adjoint 一列从下往上执行,每条都是累加,所有系数都在基态求值,每行的 在该行写明的状态上求值, 取 与 。每个 adjoint 行都是同一行 tangent 的转置:对任意 、,。按顺序执行完,得到的就是这个面对 与 的贡献。边界面用第 8 节第五步与上面的边界反传代替第 1–4 行,并且没有 Rusanov 耗散(第 9 行不存在,第 10 行只剩边界状态的物理通量 )。格点页的加热出口照常用出口处的三点梯度执行第 5–8 行;出口节点的动量行与能量行随后被约束 与 替换,所以 、 只通过这两条约束进入梯度。
The table lists one interior face statement by statement in the order the code executes it (evaluate() in core.js). Execute the tangent column top to bottom and the adjoint column bottom to top; every adjoint statement accumulates, every coefficient is evaluated at the base state, the of a row at the state named in that row, and stands for and . Each adjoint row is the transpose of the tangent in the same row: for any , . Executed in order, they give this face's contribution to and . Boundary faces replace rows 1–4 by Step 5 of Section 8 and the boundary reversal above, and have no Rusanov dissipation (row 9 is absent and row 10 reduces to the physical flux of the boundary state). On the node page the heated outlet runs rows 5–8 as usual with the three-point outlet gradient; the outlet node's momentum and energy rows are then replaced by the constraints and , so and enter the gradient only through those two constraints.
| # | 语句(前向执行顺序)statement (forward order) | tangent:自上而下tangent: top to bottom | adjoint:自下而上adjoint: bottom to top |
|---|---|---|---|
| 1 | 语句statement | tangenttangent | adjointadjoint |
| 2 | 语句statement | tangenttangent | adjointadjoint |
| 3 | 语句statement | tangenttangent | adjointadjoint |
| 4 | 语句statement | tangenttangent | adjointadjoint |
| 5 | 语句statement | tangenttangent | adjointadjoint |
| 6 | 语句statement | tangenttangent | adjointadjoint |
| 7 | 语句statement | tangenttangent | adjointadjoint |
| 8 | 语句statement | tangenttangent | adjointadjoint |
| 9 | 语句statement | tangenttangent | adjointadjoint |
| 10 | 语句statement | tangenttangent | adjointadjoint |
| 11 | 语句statement | tangenttangent | adjointadjoint |
| 12 | 语句statement | tangenttangent | adjointadjoint |
| 13 | 语句statement | tangenttangent | adjointadjoint |
x_b = 0 for every reverse accumulator
R_b = residual seed; J_b = objective seed
for operation z = f(inputs) in reverse order:
for input x in inputs:
x_b += partial(f, x) * z_b
# final boundary replacements are encountered first
# each shared input receives all contributions
return U_b, D_b播放器以固定种子 、 逐步累加 (表中的 U_b);它是任意种子的转置乘积,不是已经求出的伴随解。内部面的局部点积应与全局点积分别检查:局部正确不能排除全局分发或边界掩码错误。附录 B 可以选择面,独立查看实际局部 JVP/VJP 数值。
The player accumulates (U_b in the table) step by step for a fixed seed with : a transpose product for an arbitrary seed, not a solved adjoint. Check local and global dot products separately: local correctness does not exclude assembly or boundary-mask errors. Appendix B selects a face and displays actual local JVP/VJP values.
算例:同一个面的反向传播
Worked example: the same face, backward
仍取第 8 节算例的第 个面,种子取第 11 节解出的伴随:、。两侧节点 5、6 的行都是内部行,没有被掩码,所以 就是这两个节点的乘子块之差。按上表从下往上执行(常物性,):
Keep face of the example in Section 8 and seed with the adjoint solved in Section 11: , . The rows of nodes 5 and 6 on either side are interior and unmasked, so is the difference of their multiplier blocks. Execute the table above from the bottom up (constant transport, ):
| 量quantity | 值value |
|---|---|
| 5.206×10⁻⁴ | |
| 1.003×10⁻⁴ | |
| −2.325×10⁻⁴ | |
| −1.863×10⁻⁵ | |
| 1.880×10⁻⁷ | |
| −2.325×10⁻⁴ | |
| −3.727×10⁻⁶ | |
| 1.695×10⁻⁴ | |
| 2.055×10⁻⁶ | |
| 9.888×10⁻⁶ | |
| 9.888×10⁻⁶ | |
| −2.081×10⁻⁶ |
这个面对总梯度的贡献是 与 ,正好等于第 12 节的逐面公式 与 。局部对偶恒等式把两次传播连起来:用第 8 节算例的 (两部分之和),
This face contributes and to the total gradient, exactly the face formulas and of Section 12. The local duality identity ties the two propagations together: with the of the example in Section 8 (both parts),
其中 是这个面分发给节点 4–7 的贡献(对流重构读四个状态)。两边的相对差是 9×10⁻¹⁶,在舍入误差以内。对所有面、源项与目标求和,就是本节开头的全局恒等式。
where is what this face scatters to nodes 4–7 (the convective reconstruction reads four states). The relative difference of the two sides is 9×10⁻¹⁶, within round-off. Summed over all faces, sources and the objective this is the global identity at the start of this section.
图 10 把整个反向扫描逐步播放出来,种子同样取 、(在本页实时计算,跟随页顶的算例与目标)。顺序与程序相反:先是被替换的五行(它们的种子交给约束,原始平衡行随后被掩码清零),再是目标与源项,然后面从 倒退到 。每一步把 沿当前面的模板分发到存储状态,并把设计伴随累加到六个分量上。条形是 :一个状态收到所有读取它的面之后就回到零附近,这正是伴随方程 逐行成立;下方六个条形是 占最终值 的比例:两个热源幅度在源项那一步得到, 与 一个面一个面地累积, 与 在约束那一步一次得到(三种目标都不显式含有它们)。
Figure 10 plays the whole reverse sweep step by step, again with the seeds , (computed live on this page for the case and objective selected at the top). The order is the reverse of the program: first the five replaced rows (their seeds go to the constraints, and the raw balance rows are then masked), then the objective and the sources, then the faces from back to . Each step scatters along the current face's stencil to the stored states and adds its design adjoint to the six components. The bars are : a state returns to near zero once every face that reads it has been reversed, which is the adjoint equation holding row by row; the six bars below are as a fraction of its final value : the two heat amplitudes arrive at the sources step, and build up face by face, and and arrive in one step at the constraints (none of the three objectives has an explicit term in them).
前向求解:一个设计方向一次线性求解
Tangent solve: one linear solve per design direction
从收敛方程得到设计 tangent
Obtain the design tangent from the converged equations
假定当前光滑分支已收敛且 非奇异。沿参数方向 改变设计,原始解也随之改变。这里要找的是 ,不是任选的向量。对每个零残差分量求导:
Assume the current smooth branch is converged and is nonsingular. Changing design in direction changes the primal solution. We seek , not an arbitrarily chosen vector. Differentiate each zero-residual component:
求一个参数的导数取 ;同时改变多个参数则取一般方向。先以零状态种子求出右端,再解线性系统,最后把求出的状态方向和同一个参数方向一起传入目标求导。目标的显式参数项不能遗漏。
Use for one parameter derivative, or a general direction for simultaneous parameter changes. Form the right-hand side with a zero state seed, solve the system, and finally differentiate the objective using both the solved state direction and the same parameter direction. Retain explicit objective parameter terms.
solve R_h(U,D)=0 to the primal tolerance L = exact_linearization(U,D) D_d = e_k # or any design direction rhs = -L.jvp(U_d=0, D_d).R_d # -C_h D_d v = solve_linear(A_times=L.jvp(U_d=., D_d=0).R_d, rhs=rhs, preconditioner=B_h) check norm(A_times(v)-rhs) J_d = L.jvp(U_d=v, D_d).J_d # j_U^T v + j_D^T D_d
该过程得到所有状态对一个设计方向的响应。六个独立方向原则上需要六次 tangent 求解,但可以复用同一基态线性化和预处理。下图按密度、动量密度、能量密度绘制解向量;它不是速度和温度响应,若需要原始量响应,必须再用第 8 节的转换 Jacobian。
This computes the response of every state entry to one design direction. Six independent directions generally require six tangent solves, while sharing the base linearization and preconditioner. The plot shows density, momentum-density and energy-density responses, not velocity and temperature responses; obtain those using Section 8's conversion Jacobian.
如果原始残差尚未收敛,求出的敏感度对应当前线性化方程,不一定等于精确稳态分支的导数。如果 接近奇异,即使线性残差很小,解误差仍可能被放大。此时需同时检查原始收敛、线性收敛和差分验证,而不能只看曲线是否平滑。
Before primal convergence, the sensitivity belongs to the current linearized equation and need not equal the exact steady-branch derivative. Near singularity, a small linear residual can still correspond to an amplified solution error. Check primal convergence, linear convergence and finite differences together rather than judging only smooth-looking curves.
右端 :每个设计方向从哪里进入
The right-hand side : where each design direction enters
由第 8 节第四、五步, 的六列可以直接读出(固定状态,只让一个设计分量变化):
Steps 4 and 5 of Section 8 give the six columns of directly (the state fixed, one design component varied):
其中被约束替换的五行取约束的偏导(其余为零);端面的黏性通量所在的行都被替换或只剩质量行,所以求和实际只到内部面。于是热源方向只动能量行, 动每个内部控制体的动量与能量行(以及出口两个约束行), 只动能量行(包括出口的热流约束行), 与 各只动一个约束行。出口热流约束 的导热系数含 与 ,所以它对二者的偏导 与 是同一类;加热算例的收敛解上 (到牛顿求解的精度),这两项随之为零,这一列里非零的是出口应力行的 。
where the five rows replaced by constraints take the partial derivatives of the constraints (the others vanish); the rows of the end faces' viscous fluxes are replaced or keep only their mass row, so the sums effectively run over interior faces. A heat direction therefore moves only energy rows, the momentum and energy rows of every interior control volume (and the two outlet constraint rows), only energy rows (the outlet heat-flux constraint row included), and and one constraint row each. The conductivity in the outlet heat-flux constraint contains and , so its derivatives and are of one kind; in the heated case at the converged solution (to the accuracy of the Newton solve), both vanish with it, and the nonzero entry of these columns is the of the outlet-stress row.
可是解出 之后,每个方向都改变了所有状态。图 11 用第 8 节的转换 Jacobian 把六个解出的 tangent 换成温度 ,并给出每个方向的 ;六次求解共 206 次 GMRES 迭代(每个方向 34、34、34、34、35、35 次)。
After solving , however, every direction changes every state. Figure 11 converts the six solved tangents to temperature, , with the conversion Jacobian of Section 8 and gives for each direction; the six solves take 206 GMRES iterations (34, 34, 34, 34, 35, 35 per direction).
伴随求解:一次求解,全部六个参数
Adjoint solve: one solve for all six parameters
有限维 Lagrangian:不是连续积分形式
A finite-dimensional Lagrangian, not a continuous integral
的每个分量对应一条最终离散残差行。它是有限维乘子向量,不是定义在连续空间上的函数。格点边界行若已改为约束,相应乘子就对应那条约束。这里采用加号约定,后面的负号均由此推导。
Each component of multiplies one final discrete residual row. It is a finite-dimensional multiplier vector, not a continuous spatial function. A node boundary row replaced by a constraint has a multiplier for that constraint. The plus convention here determines the signs below.
把每一项展开,再消去状态响应
Expand each term before eliminating the state response
在收敛解上 ,所以 Lagrangian 等于目标。即使乘子随设计改变,额外项 仍为零。选择乘子使未知状态响应的系数消失,就不再需要为每个参数单独求 。
At convergence , so the Lagrangian equals the objective. Even if multipliers vary with design, the extra term vanishes. Choose multipliers to cancel the coefficient of the unknown state response and avoid solving a separate for every parameter.
是六维总设计梯度, 是 维目标状态梯度,二者不共用字母 g。一个标量目标只需一次伴随求解;换目标一般要换右端并重新求解。换参数方向不改变伴随解,但若改变实际设计并重算原始流场,则必须重新线性化。
The total design gradient has six components; the state objective gradient has components. They no longer share the letter g. One scalar objective needs one adjoint solve; a different objective generally changes the right-hand side and requires another solve. Selecting another parameter direction leaves the adjoint unchanged, but changing the actual design and re-solving the primal requires relinearization.
这是 tangent 与 adjoint 对同一离散问题给出相同方向导数的代数证明。没有连续极限假设,也没有分部积分。如果改用减号定义离散 Lagrangian,乘子会整体换号;总梯度仍应一致。这个符号约定不等于连续场与离散乘子的逐点对应关系。
This algebra proves equality of tangent and adjoint directional derivatives for the same discrete problem. It uses neither a continuum limit nor integration by parts. A minus-sign discrete Lagrangian changes multiplier signs while preserving total gradients. That sign convention does not establish a pointwise correspondence between continuous fields and discrete multipliers.
jU = L.vjp(R_b=0, J_b=1).U_b AT_times(R_b) = L.vjp(R_b, J_b=0).U_b psi = solve_linear(AT_times, rhs=-jU, preconditioner=transpose_GS) check norm(AT_times(psi)+jU) g = L.vjp(R_b=psi, J_b=1).D_b # the design adjoint = g_h
三种目标的 与
and of the three objectives
伴随右端是 ,总梯度还要加 。温度跟踪的 已在第 7 节写出,。两个端点目标只读一个面上的量,所以 只有几个块非零,而且可能直接依赖设计:
The adjoint right-hand side is , and the total gradient adds . Section 7 wrote of temperature tracking, whose . The two endpoint objectives read quantities of one face only, so only a few blocks of are nonzero, and they may depend on the design directly:
格点页入口热流的两个显式项来自 ;入口温度只经约束行进入,没有显式项。页顶默认参数下 ,。制造解算例的出口温度目标也是 。
On the node page the two explicit terms of the inlet heat flux come from ; the inlet temperature enters only through its constraint row and has no explicit term. At the page defaults and . The outlet-temperature objective of the manufactured case is as well.
用真残差判断伴随求解
Judge the adjoint solve using the true residual
此处 是尚未完全收敛的迭代解, 是用它算出的梯度, 是精确 tangent 给出的方向导数;等式假定使用同一精确基态且 tangent 已精确求解。它说明伴随残差与敏感度幅度共同决定方向导数误差。下图的乘子分量是离散行权重,不能直接读成连续密度、速度或温度伴随场。
Here is an incompletely converged iterate, the gradient computed from it and the directional derivative of the exact tangent. The identity assumes the same exact base state and an exactly solved tangent. Adjoint residual and sensitivity magnitude jointly control directional-derivative error. The plotted multiplier components are weights on discrete rows, not direct continuous density, velocity or temperature adjoint fields.
乘子的含义:在一个控制体里加一点源项
What the multipliers mean: a small source in one control volume
在第 行加一个源项 ,即把 换成 ,再重新求解。对 求导,状态的导数 满足 ,于是
Add a source to row , that is, replace by , and re-solve. Differentiated with respect to , the state derivative satisfies , so
所以 是“每单位时间在控制体 加入一单位第 个守恒量(质量、动量或能量),目标改变多少”:伴随场是目标对源项的敏感度。热源梯度 就是能量行的敏感度按加热形状加权求和。图 12 画出三种目标的伴随场,并在三个节点重新求解加了源项的非线性问题来验证这个含义。温度跟踪的 :任何位置加热都使目标减小,中部最有效;出口温度的 ,越靠近出口加热,出口温度升得越多;入口热流的 在入口附近最大,那里的热量更多地逆着流动导出入口。第 16 节说明动量行与能量行的乘子在加密下逼近连续乘子 、。被约束替换的五行,乘子的含义是约束目标值的敏感度:把 改成 ,,正是 。
So is how much the objective changes when one unit of the -th conserved quantity (mass, momentum or energy) per unit time is added to control volume : the adjoint field is the sensitivity of the objective to sources. The heat-source gradient is the energy-row sensitivity weighted by the heating shape. Figure 12 draws the adjoint fields of the three objectives and checks this meaning by re-solving the nonlinear problem with a source added in three nodes. For temperature tracking : heating anywhere lowers the objective, most effectively in the middle; for the outlet temperature , and the closer to the outlet the heat is added, the more the outlet temperature rises; for the inlet heat flux is largest near the inlet, where more of the heat is conducted out through the inlet against the flow. Section 16 shows that the momentum and energy multipliers approach the continuous multipliers , under refinement. The five rows replaced by constraints carry the sensitivity to the constraint's target value: changing to gives , which is .
代价:tangent 按设计分量计,adjoint 按目标计
Cost: the tangent counts design components, the adjoint counts objectives
tangent 每个设计方向解一次 ,得到所有目标沿这个方向的导数;adjoint 每个目标解一次 ,得到这个目标对所有设计分量的导数。两种求解的每次迭代代价大致相同:两次矩阵自由乘积(一次做 Krylov 步,一次检查真残差;每次都沿同一张记录了 4247 个中间量的计算图前向或反向扫描一次,两者都把每个中间量访问一次)加一次 GS 扫描,收尾时再多一次乘积(转置的 GS 扫描每次还要重新分解各个 对角块,所以略贵)。例如温度跟踪的伴随求解,35 次迭代是 71 次乘积和 35 次 GS 扫描。图 13 用本页默认参数的实际迭代次数比较。
The tangent solves once per design direction and obtains the derivatives of every objective along it; the adjoint solves once per objective and obtains the derivatives with respect to every design component. An iteration costs about the same in both: two matrix-free products (one for the Krylov step, one for the true-residual check, each a forward or reverse sweep over the 4247 values recorded in the same computational graph, visiting every value once) plus one GS sweep, and one more product at the end (the transposed GS sweep also refactors each diagonal block on every call, so it is slightly dearer). The 35 iterations of the adjoint solve for temperature tracking, for example, are 71 products and 35 GS sweeps. Figure 13 compares the actual iteration counts at the page defaults.
设计导数:热源、物性与边界参数
Design derivatives: heating, transport and boundary parameters
六个参数经过哪些计算节点
Where the six parameters enter the graph
两个热源幅度进入能量源项; 同时改变应力、导热及黏性功; 改变导热;入口温度与出口参数通过第 4 节的闭合进入。参数偏导固定全部状态槽位,状态随后如何变化由 tangent 或 adjoint 处理,不能在局部偏导中重复加入。
Two heat amplitudes enter the energy source. The parameter changes stress, conduction and viscous work, while changes conduction. Inlet temperature and the outlet parameter enter Section 4's closures. Parameter partials hold all state slots fixed; tangent or adjoint accounts for the resulting state response, which must not be counted again inside local partials.
热源梯度可以逐行写出
Heat-source gradients can be written row by row
负号来自残差减去源项,不是人为指定的伴随符号。本页目标与约束对两个热源幅度均无显式依赖,因此公式没有额外项。格点入口和出口的能量行已被替换,对应源项种子先被掩码置零;格心则保留所有控制体能量平衡。
The minus sign comes from subtracting the source in the residual, not an imposed adjoint sign. Objectives and constraints have no explicit dependence on the two heat amplitudes, so no extra terms appear here. Replaced inlet and outlet energy rows on nodes have their source seeds masked to zero; cells retain every volume energy balance.
物性与边界参数使用完整反传
Use full reverse accumulation for transport and boundary parameters
这就是第 9 节以种子 、 反向扫描时在六个参数上累加出的设计伴随 。格心版最后一项为空,但边界通量对参数的影响已在中间项中。格点版入口温度约束对 的显式偏导为 −1;出口应力或温度约束对 的显式偏导同样为 −1。除此之外,还要保留物性和目标的直接参数路径。
This is the design adjoint that the reverse sweep of Section 9 accumulates on the six parameters for the seeds and . The last term is empty for cells, but their boundary-flux parameter dependence is already in the middle term. For nodes, the inlet temperature constraint has explicit derivative −1 with respect to , and the outlet stress or temperature constraint has derivative −1 with respect to . Retain transport and direct objective paths as well.
逐面写出的设计导数
The design derivatives written face by face
把第 8 节第四、五步的偏导代入 ,每个分量都能写成对面、控制体或约束行的求和,其中 由 经第 9 节的装配转置得到:
Substituting the partial derivatives of Steps 4 and 5 of Section 8 into writes each component as a sum over faces, control volumes or constraint rows, with obtained from by the transposed assembly of Section 9:
这是加热算例的写法(制造解算例的出口约束 、 不含 与 ,含 的两项消失, 改由温度约束行的 给出)。 已把五个约束行清零,两个端面因此不出现; 在收敛解上为零(到牛顿求解的精度:页顶默认参数下 ),所以含 的项可以忽略。页顶默认参数下 ,。图 14 把每一项画出来。
This is the heated case (the outlet constraints , of the manufactured case contain neither nor , the two terms with drop out, and comes from the temperature row, ). has already zeroed the five constraint rows, so the two end faces do not appear; vanishes at the converged solution (to the accuracy of the Newton solve: at the page defaults), so the terms with are negligible. At the page defaults and . Figure 14 draws every term.
下表把总梯度拆为显式目标项和残差反传项,并比较所选方向 的 tangent 结果 。若制造解源项在每次参数扰动后重新生成,比较的就不再是同一个函数;因此本页所有差分、复数计算和导数都使用基准模型冻结的制造源项。
The table separates direct objective and residual-reverse contributions and compares the tangent result for the selected direction . Regenerating manufactured forcing after every parameter perturbation would change the function being differentiated. All differences, complex calculations and derivatives therefore share the baseline model's frozen manufactured source.
有限差分与 Taylor 检验:重新求解,检查总导数
Finite differences and Taylor tests: re-solve and check total derivatives
有限差分必须重新解原始问题
Finite differences must re-solve the primal
为避免与空间网格尺度 混淆,本节把参数差分步长记为 。选参数 后分别形成 ,从基态作初值重新收敛原始方程,再计算 。只改变目标输入而固定流场,会验证偏导而不是总导数。
Use for the parameter step to distinguish it from mesh spacing . For parameter , form , use the base state as an initial guess, reconverge the primal, and evaluate . Perturbing only objective inputs while fixing the flow would test a partial derivative, not the total derivative.
光滑且收敛充分时,前后差分截断误差通常为一阶,中心差分为二阶。步长过大受到非线性截断误差影响,过小则放大原始求解误差和浮点相消。这里参数有不同物理含义,统一绝对步长只是教学扫描方式,不等于所有参数的最佳步长。
For sufficiently converged smooth problems, forward and backward truncation errors are usually first order and centred errors second order. Large steps incur nonlinear truncation error; small steps amplify primal solve error and cancellation. A common absolute step is a teaching sweep, not necessarily optimal for parameters with different meanings.
一般非零一阶导数下, 为一阶、 为二阶;零梯度目标的首项可能不同。下方可选所有参数、单个步长或完整步长扫描,并显示两个扰动问题的最终原始残差。判断时应寻找一段合理的收敛区间,而不是只取最小步长的一个数。
For a generic nonzero first derivative, is first order and second order; zero-gradient objectives can have different leading terms. Select any parameter, a single step or a complete sweep, with final primal residuals for both perturbed solves. Look for a credible convergence range rather than trusting one value at the smallest step.
四种误差要分开:空间离散误差由网格加密检查;非线性求解误差由原始残差检查;线性求解误差由 tangent/adjoint 真残差检查;求导实现由转置、差分与复数检查。点积相等只证明两条传播互为转置;共同遗漏同一项也可能通过(图 8)。
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 (Figure 8).
复数步长:用虚部检验同一个离散函数
Complex step: check the same discrete function through its imaginary part
复数步长验证同一个离散函数
Complex step checks the same discrete function
在当前正密度、正温度、无 limiter 的光滑分支,除法、平方根与 Sutherland 幂函数可沿邻近复数分支解析延拓。复数步长避免两个几乎相等的实目标相减,但不自动消除原始方程求解误差。复数状态必须满足复数残差,而不仅是把实解加上某个标签。
On the current positive-density, positive-temperature, unlimited smooth branch, division, square roots and the Sutherland power admit a nearby analytic complex continuation. Complex step avoids subtracting nearly equal real objectives, but does not automatically remove primal solve error. The complex state must satisfy the complex residual; attaching an imaginary part to the real solution is not enough.
本实现从已收敛实解出发,用独立复数算术评估整个残差与目标,再迭代校正缩放虚部残差。校正使用基态实 Jacobian 求解,这正是小虚部的一阶修正;最终目标仍由复数残差路径独立计算。它能检查该路径与 AD 导数是否一致,但不是对任意非解析代码的通用证明。
The implementation starts from the converged real state, evaluates the entire residual and objective using independent complex arithmetic, and corrects the scaled imaginary residual iteratively. Corrections use the real base Jacobian, appropriate to the small-imaginary-part linearization. The final objective is evaluated independently through the complex path. This checks consistency with AD derivatives, not arbitrary nonanalytic code.
复数路径不能对复数取绝对值、大小比较或裁剪后还声称保持解析性。当前有效性判断在实基态进行;如果以后加入 limiter 或激波捕捉,应单独分析分支,不直接沿用本节结论。运行检查后生成的表同时给出缩放虚部残差,防止很小的步长掩盖未收敛。
Complex absolute values, ordering comparisons or clipping generally destroy analyticity. Current admissibility checks use the real base state. Limiters or shock capture would require separate branch analysis rather than inheriting this result. The table produced by the check includes the scaled imaginary residual so that a tiny step cannot conceal nonconvergence.
精度阶:制造解上的一阶与二阶加密
Order of accuracy: first- and second-order refinement on a manufactured solution
本节实验:离散状态达到二阶,不等于目标或梯度也达到二阶。先运行制造解加密,读守恒量误差的实测阶,再到第 16 节比较梯度。
Experiment: Second-order states do not imply second-order objectives or gradients. Run the manufactured-solution refinement for the measured state rates, then compare gradients in Section 16.
空间精度与导数正确性是两件事
Spatial accuracy and derivative correctness are different tests
有限差分和点积检验在固定网格上检查导数;网格加密检查数值解逼近参考问题的速度。精确的一阶离散伴随仍然是一阶模型的导数,不会因为转置计算精确就自动变成二阶。
Finite differences and dot products test derivatives at fixed mesh; refinement tests how rapidly the numerical solution approaches a reference problem. An exact discrete adjoint of a first-order model remains that model's derivative; exact transposition does not confer second-order spatial accuracy.
把这些解析函数代入物理总通量并作解析空间求导,构造独立的制造源项;质量通量恒为 0.35,质量源为零。这一步是生成参考问题,不是用连续伴随代替离散求导。数值状态仍由本页残差求解。参考源项包含物性随温度的变化,不能把变物性情形当作常系数二阶导数。
Substitute these analytic functions into the physical net flux and differentiate analytically in space to manufacture an independent source. Mass flux is constant at 0.35, giving zero mass source. This constructs the reference problem; it does not substitute continuous adjoints for discrete differentiation. The numerical state is still obtained from this page's residual. Variable transport derivatives belong in the manufactured forcing and cannot be replaced by constant-coefficient second derivatives.
格点参考 是解析节点值。端点误差按半控制体体积计权。
The node reference is the analytic point value. Endpoint errors use half-volume weights.
下方独立运行制造解加密,分别计算一阶、二阶误差和实测阶数,保留当前物性选择。有限网格上的斜率受对流、扩散和边界误差共同影响;参考斜率线只是视觉标尺。应确认原始残差远小于空间误差,并观察连续多个加密层级,不能把单个粗网格斜率当作阶数证明。
The panel independently refines the manufactured case for first and second order, retaining current transport settings. Finite-grid rates reflect convection, diffusion and boundary errors together; reference slopes are visual guides. Check that primal residuals are much smaller than spatial errors and inspect several refinement levels rather than treating one coarse-grid slope as proof.
固定基准补充:=0.15、Pr=0.72、常物性;下表是离线实际计算的各级网格误差与相邻加密斜率,不随顶部参数改变。这些网格还没有完全进入渐近区:一阶斜率从 0.4–0.5 逐渐升向 1,二阶斜率高于 2 并随加密回落。它们只说明这个网格范围内误差的下降速度,不能单独当作渐近阶的证明。
Fixed supplementary baseline: =0.15, Pr=0.72, constant transport. The table gives measured offline errors on every grid and the rates between neighbouring grids; it does not follow the top controls. These grids are not yet fully asymptotic: first-order slopes rise from 0.4–0.5 towards 1, and second-order slopes exceed 2 and fall back under refinement. They describe how fast the error drops over this range, not a proof of the asymptotic order.
| 阶数order | N | 误差error | 实测阶measured rate |
|---|---|---|---|
| 1 | 8 | 1.16842e-1 | — |
| 1 | 16 | 8.89070e-2 | 0.3942 |
| 1 | 32 | 6.06293e-2 | 0.5523 |
| 1 | 64 | 3.79622e-2 | 0.6755 |
| 1 | 128 | 2.21712e-2 | 0.7759 |
| 2 | 8 | 2.07603e-2 | — |
| 2 | 16 | 4.39520e-3 | 2.2398 |
| 2 | 32 | 9.52454e-4 | 2.2062 |
| 2 | 64 | 2.16414e-4 | 2.1379 |
| 2 | 128 | 5.11940e-5 | 2.0797 |
伴随一致性:离散梯度与连续参考分开检查
Adjoint consistency: discrete gradients and the continuous reference checked separately
先说明本节比较的对象
Define what is being compared
离散点积测试验证实现是否互为转置;差分和复数步长验证是否对当前离散目标求了完整导数。本节问的是另一个问题:网格加密时,目标与总设计梯度是否逼近同一个物理边值问题。连续参考的完整推导和独立实现放在附录 A,正文不使用连续伴随微分方程求解 。
Discrete dot tests check transposition; finite differences and complex step check the complete derivative of the current discrete objective. This section asks a different question: do objective and total design gradients approach the same physical boundary-value problem under refinement? Appendix A contains the continuous derivation and independent implementation. The main discrete solve never uses a continuous adjoint differential equation to obtain .
连续参考先利用恒定质量流率消去密度,采用速度、温度、总动量通量和总能量通量四个变量。其四个连续乘子记作 ;正文的 有 个离散行乘子。两者的变量、维数和边界处理不同;本节下方的对照图比较目标与同一参数的总导数。乘子本身只有动量行与能量行可以逐点对应,推导如下。
The continuous reference eliminates density using constant mass flux and uses velocity, temperature, total momentum flux and total energy flux as four variables. Its four continuous multipliers are denoted , while contains discrete row multipliers. Variables, dimensions and boundary treatments differ; the comparison panel below compares objectives and total derivatives for the same parameter. Of the multipliers themselves, only the momentum and energy rows correspond pointwise, as derived next.
离散乘子与连续乘子的对应
Matching the discrete and continuous multipliers
离散残差是控制体上的积分平衡。动量行和能量行的数值总通量,正是附录 A.2 连续参考所用的两个变量:总动量通量 与总能量通量 。所以这两行就是 、 在控制体上的积分:
The discrete residual is an integrated balance over a control volume. The numerical total fluxes of the momentum and energy rows are exactly the two variables of the continuous reference in Appendix A.2: the total momentum flux and the total energy flux . These two rows are therefore and integrated over a control volume:
比较两个 Lagrangian 中的对应项。离散约定是 ,附录 A.3 的连续约定是 ,而 的 、 分量正是 、:
Compare the corresponding terms of the two Lagrangians. The discrete convention is and the continuous one in Appendix A.3 is , whose and components of are exactly and :
同一个约束的乘子只差这个符号,因此在光滑区域 、。残差没有除以控制体体积,所以两组乘子量级相同。另外两个连续乘子没有离散对应:、 属于本构关系 与 的热流关系,有限体积在黏性通量内部计算它们,不是残差行;质量行也没有对应,因为连续参考用固定的 消去了密度,没有质量方程。
Multipliers of the same constraint differ only by this sign, so in smooth regions and . The residual is not divided by the control volume, so both sets have the same magnitude. The other two continuous multipliers have no discrete counterpart: and belong to the constitutive relations and the heat-flux relation for , which the finite-volume scheme evaluates inside the viscous flux rather than as residual rows; the mass row has no counterpart either, because the continuous reference eliminates density with the fixed and has no mass equation.
下表对本页格式逐级加密,比较 与 ;“内部”取 ,“全域”取全部场行及其位置。连续乘子由附录 A.3–A.4 的独立 ODE 伴随在 768 步 RK4 上求得,每个网格点都是它的节点。
The table refines this page's scheme and compares and ; "interior" means , "global" all field rows, with the location of the maximum. The continuous multipliers come from the independent ODE adjoint of Appendices A.3–A.4 on 768 RK4 steps, and every mesh point is one of its nodes.
| 阶数Order | N | 内部 动量Interior momentum | 阶Rate | 内部 能量Interior energy | 阶Rate | 全域 动量Global momentum | 全域 能量Global energy |
|---|---|---|---|---|---|---|---|
| 二阶second order | 12 | 3.07e−4 | — | 6.85e−5 | — | 1.64e−3 (x = 0.917) | 1.68e−3 (x = 0.917) |
| 24 | 8.88e−5 | 1.79 | 2.18e−5 | 1.65 | 1.35e−3 (x = 0.958) | 1.90e−3 (x = 0.958) | |
| 48 | 2.80e−5 | 1.67 | 6.25e−6 | 1.80 | 1.21e−3 (x = 0.979) | 2.03e−3 (x = 0.979) | |
| 96 | 7.93e−6 | 1.82 | 1.72e−6 | 1.87 | 1.15e−3 (x = 0.990) | 2.10e−3 (x = 0.990) | |
| 一阶first order | 12 | 2.03e−3 | — | 8.72e−4 | — | 3.68e−3 (x = 0.917) | 2.51e−3 (x = 0.917) |
| 24 | 1.43e−3 | 0.51 | 4.68e−4 | 0.90 | 2.82e−3 (x = 0.958) | 2.52e−3 (x = 0.958) | |
| 48 | 8.96e−4 | 0.67 | 2.43e−4 | 0.95 | 2.12e−3 (x = 0.979) | 2.41e−3 (x = 0.979) | |
| 96 | 5.19e−4 | 0.79 | 1.23e−4 | 0.99 | 1.67e−3 (x = 0.990) | 2.32e−3 (x = 0.990) |
内部按格式的阶收敛(二阶约 2,一阶逐渐接近 1)。全域最大偏差都出现在紧邻出口约束的节点 ,而且不随加密减小:二阶从 到 ,动量只从 降到 ,能量反而从 增到 ,与乘子本身同一量级。出口的动量行和能量行已换成约束,相邻节点的平衡行承担了边界的作用,它的乘子不是连续场的取值。这与一维 Burgers 格点页入口的伴随边界层类似:总导数仍然收敛,六个参数导数的最大相对误差在二阶格点上从 降到 (最后一级实测阶 1.81),但逐点比较乘子时必须排除这些边界点。
The interior converges at the order of the scheme (about 2 at second order, approaching 1 at first order). The global maximum always sits at the node next to the outlet constraints, , and does not decrease under refinement: at second order from to momentum falls only from to and energy even grows from to , the same magnitude as the multipliers themselves. The outlet momentum and energy rows have been replaced by constraints, the balance rows of the neighbouring node take over the boundary role, and its multipliers are not values of the continuous field. This resembles the inlet adjoint layer of the 1-D Burgers node page: total derivatives still converge, the maximum relative error of the six parameter derivatives falling from to at second order (last measured rate 1.81), but a pointwise comparison of multipliers must exclude these boundary points.
残差缩放会改变离散乘子的数值
Residual scaling changes discrete multiplier values
以上在精确收敛状态成立, 是可逆行尺度矩阵。把平衡行除以控制体体积时,其乘子相应乘以体积;总设计梯度不变。即使 随参数改变,新增项也乘以零残差。边界约束行需要按自己的尺度处理,不能不加区分地套用体积权重。
These identities hold at exact convergence for an invertible row-scaling matrix . Dividing balance rows by volume multiplies their corresponding multipliers by volume, preserving the total design gradient. Parameter-dependent scaling adds terms multiplied by the zero residual. Boundary constraint rows need their own scaling; volume weights cannot be applied indiscriminately.
本页转置使用普通数组点积,不是带体积权的函数空间伴随。如果引入权矩阵,伴随算子需相应变换。连续参考也有 ODE 积分与射击误差;先提高参考分辨率,再比较离散加密误差,避免把参考误差平台误读为空间离散不一致。
This page uses ordinary array dot products, not a volume-weighted function-space adjoint. Introducing weight matrices requires transforming the adjoint operator. The continuous reference also has ODE integration and shooting errors; refine it before interpreting a discrete-refinement plateau as spatial inconsistency.
粗网格梯度误差可以不单调
Gradient error need not decrease monotonically on coarse grids
本节的表是格心基准,两页相同;格点页的梯度可在附录 A 按当前设置与连续参考比较。
The table in this section is the cell baseline, identical on both pages; on the node page, compare gradients with the continuous reference for the current settings in Appendix A.
固定基准为格心、一阶重构、局部加热 、常物性 、温度跟踪目标;比较对出口总应力参数 的总梯度,连续参考使用 512 个 ODE 步。离线实测绝对误差如下,不随顶部控件改变:
For a fixed baseline, use cells, first-order reconstruction, local heating , constant transport , and temperature tracking. Compare the total derivative with respect to outlet total stress against a 512-step ODE reference. These measured offline absolute errors do not follow the top controls:
| N | |
|---|---|
| 8 | 1.57965e−4 |
| 16 | 2.55325e−4 |
| 32 | 2.31363e−4 |
| 64 | 1.64320e−4 |
| 128 | 1.01779e−4 |
误差先增大,再随加密下降。该数据说明,粗网格上的偶然误差抵消不能当作渐近精度;它本身也不能证明伴随发散或连续不一致。第 13 节检查固定网格上的求导,第 15 节检查状态精度,本节检查目标与梯度的连续对照,三者不可互相替代。在格心页设置上述基准、选择出口参数并运行加密,可重现前四行;128 网格数据是单独的离线补充。
Error first increases, then falls with refinement. Accidental coarse-grid error cancellation does not establish asymptotic accuracy; this sequence alone proves neither adjoint divergence nor continuum inconsistency. Section 13 checks differentiation at a fixed mesh, Section 15 checks state accuracy, and this section compares objectives and gradients with a continuum reference. These tests are not interchangeable. On the cell page, select the stated baseline and outlet parameter, then run refinement to reproduce the first four rows; N=128 is a separate offline supplement.
GS 固定点与 GS 预处理 GMRES
GS fixed point and GS-preconditioned GMRES
GS 缺陷校正解的是精确系统
GS defect correction targets the exact system
设目标线性系统为 :tangent 时 ,adjoint 时 。每轮先用精确算子求残差,再用较便宜的 GS 映射修正。不能只把 解得很准,就宣称解好了原系统。
Write the target system as : use for tangents and for adjoints. Each iteration computes the residual with the exact operator, then applies the cheaper GS map. Accurately solving alone does not solve the original system.
这是固定线性系统下的收敛条件;当前有限参数试验不是一般收敛定理。黏性耦合、重构和边界可能使一次 GS 近似不足。预算用尽表示本次未收敛,页面仍报告精确算子的真残差。
This is a convergence condition for the fixed linear system, not a general theorem inferred from the exposed parameter cases. Viscous coupling, reconstruction and boundaries can make one GS approximation insufficient. Exhausting the budget means this run has not converged; the page still reports the exact-system residual.
转置 GS 必须反向扫描
Transposed GS requires a reverse sweep
注意求和用 ,不是 。先处理后面的块,才能读到已经求出的分量;每个对角块也要转置。直接对原 GS 程序改循环方向但不转置块或累加路径,不会得到正确的伴随预处理。
The sum uses , not . Later blocks must be solved first so their components are available, and every diagonal block must be transposed. Reversing only the loop direction without transposing blocks and accumulation paths does not produce the adjoint preconditioner.
GMRES:用 GS 作右预处理
GMRES with GS as a right preconditioner
GMRES 不再只沿单个残差修正,而是在多个 Krylov 方向张成的空间中最小化残差。 是 Arnoldi 过程的 上 Hessenberg 矩阵(上横线在本页留给反向模式导数,所以这里用下横线)。本实现每个新方向先做一次固定的 GS 预处理,再调用精确乘积;保存预处理后的方向来恢复解。Arnoldi 正交化和小型最小二乘代价也属于求解成本。
GMRES minimizes residuals over a space of multiple Krylov directions instead of taking only one residual correction at a time. Here is the upper Hessenberg matrix of the Arnoldi process (underlined, because this page reserves the overbar for reverse-mode derivatives). Each new direction receives one fixed GS preconditioning application followed by an exact operator product. Preconditioned directions are stored to recover the solution. Arnoldi orthogonalization and the small least-squares problem also contribute to cost.
b = -jU; x = 0
# GS correction
repeat:
r = b - exact_AT(x)
stop if norm(r) < tolerance
x += 0.65 * reverse_block_GS(r)
# GMRES
for each Arnoldi direction v:
z = reverse_block_GS(v)
w = exact_AT(z)
orthogonalize w; update least-squares residual
recover x from stored z directions
confirm norm(b - exact_AT(x))下方在同一收敛流场上比较 GS、GMRES 和 LU,计时包含各自准备。GMRES 有基于正交化的残差估计,也计算恢复解的真残差;最终判据应看真残差。小网格上 LU 可能更快,附录 C 明确展示其显式组装与分解。这里不把线性迭代次数与外层 Newton 次数混为一谈。
The panel compares GS, GMRES and LU on the same converged flow, including each method's preparation. GMRES has an orthogonalization-based residual estimate and also evaluates the recovered solution's true residual; final acceptance should use the latter. LU can be faster on this small mesh; Appendix C explicitly shows assembly and factorization. Linear iterations are distinct from outer Newton iterations.
总结:从守恒状态到经过验证的设计梯度
Summary: from conservative states to verified design gradients
把整条计算链连起来
Connect the entire computational chain
本页从有限个守恒状态出发:重构决定对流面状态,原始量转换和差分决定黏性面数据,物性决定应力和热流,面装配与边界覆盖定义最终残差。目标是另一个有限维标量函数。任何一条状态或参数依赖漏掉,最终伴随就会求解另一套导数问题。
The lesson starts with finitely many conservative states. Reconstruction defines convective traces; primitive conversion and differences define viscous face data; transport defines stress and heat flux; face assembly and boundary overwrites define the final residual. The objective is another finite-dimensional scalar function. Omitting any state or parameter dependency changes the derivative problem solved by the adjoint.
这四个式子分别是原始方程、设计 tangent( 沿设计方向 )、离散 adjoint 和总设计梯度(以 、 反向扫描得到的 )。matrix-free 只改变乘积的实现方式;GS、GMRES、LU 只改变线性系统的求解方式;空间一阶/二阶则会改变残差及其精确 Jacobian。三者不能互相替代。
These four equations are the primal, the design tangent ( along the design direction ), the discrete adjoint and the total design gradient (the of the reverse sweep seeded with , ). Matrix-free evaluation changes how products are computed. GS, GMRES and LU change how a linear system is solved. First- versus second-order space discretization changes the residual and its exact Jacobian. These choices are not interchangeable.
建议按层核对:先看局部面求导与点积,再看全局边界掩码与转置,再看线性真残差,然后做完整重算的差分和复数验证,最后做空间加密与独立连续参考对照。每个检查回答不同问题,没有一个单独检查能替代全部验证。
Check the layers in order: local face derivatives and dot products, global boundary masking and transposition, true linear residuals, fully re-solved finite differences and complex step, then spatial refinement and independent continuous comparison. Each answers a different question; none replaces all the others.
当前实现是固定一维网格上的光滑层流基础算例。走向二维非结构网格时,需要扩展多维梯度重构、面法向、黏性边界和几何求导;走向 RANS 时还需湍流模型及其完整耦合导数。这些是后续目标,不应与本页已经验证的范围混淆。
The present implementation is a smooth laminar foundation on a fixed one-dimensional mesh. Two-dimensional unstructured grids require multidimensional gradients, face normals, viscous boundaries and geometry derivatives. RANS additionally requires turbulence equations and their complete coupled derivatives. These are subsequent goals, distinct from the scope verified here.
附录 A:独立连续参考与完整伴随
Appendix A: independent continuous reference and complete adjoint
A.1 本附录才使用连续场和空间微分
A.1 Continuous fields and spatial derivatives belong here
本附录构造独立连续参考。用 表示连续守恒场,用 表示三分量形式连续乘子;约化 ODE 的四分量乘子另记 。正文的 仍专指有限维离散量。本附录中的积分、空间导数及分部积分不参与正文离散转置的定义。
This appendix constructs an independent continuous reference. Denote the conservative field by and its three-component formal continuous multiplier by . The four-component multiplier of the reduced ODE is . The main text's remain finite-dimensional discrete quantities. Integrals, spatial derivatives and integration by parts here do not define the main discrete transpose.
先考虑体积分目标密度 不依赖状态空间导数的情形;端点目标保留为 。把残差以加号接入:;用通量 Jacobian 线性化残差,并两次分部积分。以下梯度下标表示行偏导,连续空间导数明确写作 或下标 x。
First let the volume objective density be independent of spatial state derivatives, retaining endpoint objectives . Adjoin the residual with a plus sign, ; linearize the residual with the flux Jacobians and integrate by parts twice. Subscripts below (as in , , ) denote partial derivatives, written as row vectors; spatial derivatives are explicitly written as or subscript x.
这给出形式体方程与边界项,还没有自动给出可求解的边值问题。黏性通量的质量分量为零, 奇异,不能把它当作三个独立二阶方程的可逆最高阶系数。代码实际采用下面的约化系统,并对该系统推导连续伴随。
These are formal interior equations and boundary terms, not yet an automatically solvable boundary-value problem. The viscous mass flux is zero, making singular; it is not an invertible highest-order coefficient for three independent second-order equations. The implemented continuous reference instead uses the reduced system below and its own derived adjoint.
参数变化还产生体积分 ;端点还含上式的 与边界约束变化。尤其黏度和 Prandtl 数影响通量,不能只对源项求导。下面约化系统的 和端点约束导数统一包含这些依赖。
Parameter variation also gives the volume contribution ; endpoints also contain the displayed and boundary-constraint variations. Viscosity and Prandtl number affect fluxes, so differentiating only the source is insufficient. The reduced system below includes these dependencies through and endpoint derivatives.
逐步推导: 的秩与约化后的阶数
Step by step: the rank of and the order of the reduced problem
黏性通量是 ,,。梯度对 是线性的:,,其中 、:
The viscous flux is with and . The gradients are linear in : and , where and :
的第一行为零;后两行中 的第三个分量为零而 的不为零,两者线性无关,所以 。于是三条守恒律的阶数是:质量方程只含一阶导数,动量、能量方程分别对 、 是二阶。总阶数正好等于原始问题的边界条件个数:
The first row of is zero; in the other two rows the third component of is zero while that of is not, so they are linearly independent and . The mass equation therefore contains only first derivatives, while the momentum and energy equations are second order in and . The total order equals the number of primal boundary conditions:
质量方程可以直接积分:,积分常数用掉入口的一个条件(本页 、)。代入 后剩下 、 两个二阶方程;把 、 换成通量 、,就是 A.2 的四维一阶系统,入口、出口各两个条件。格点页的五个约束行(入口三行、出口两行)正是这 个条件。
The mass equation integrates directly to ; the integration constant uses one inlet condition (here and ). With , two second-order equations in and remain; replacing and by the fluxes and gives the four-dimensional first-order system of A.2, with two conditions at each end. The five constraint rows of the node page (three at the inlet, two at the outlet) are exactly these conditions.
A.2 消去恒定质量流率,建立四维 ODE
A.2 Eliminate constant mass flux to form a four-dimensional ODE
是总动量与总能量通量。入口给定 ,未知入口通量为两个射击变量。出口加热条件为 ;制造解条件为 。本页不把入口密度或速度作为设计变量,因此质量流率固定。
The variables are total momentum and energy flux. Inlet conditions are , leaving two unknown inlet fluxes as shooting variables. The heated outlet imposes ; the manufactured outlet imposes . Inlet density and velocity are not design variables here, so mass flux stays fixed.
用 RK4 前向积分,再用两变量 Newton 修正入口通量以满足出口条件。射击 Jacobian 来自 ODE 对初值的变分方程。保存轨迹及右端值,使用三次 Hermite 插值供反向伴随积分取值。较强加热使用从零热源逐步增加的延拓;积分越出正值亚声速分支时报告失败,不裁剪状态。
Integrate forward with RK4 and use a two-variable Newton update of inlet fluxes to satisfy outlet conditions. Shooting derivatives come from ODE initial-value variations. Stored trajectories and right-hand sides provide cubic Hermite interpolation for backward adjoint integration. Stronger heating uses continuation from zero heating. Leaving the positive subsonic branch reports failure rather than clipping states.
A.3 连续目标、积分号与符号约定
A.3 Continuous objectives, integrals and signs
温度跟踪使用 、零端点目标;出口温度使用 ;入口热流使用 。这里乘的是 ,与正文的加号离散残差约定不是同一个对象。对 作一次分部积分,分别收集体内、入口和出口的系数,得到:
Tracking uses with zero endpoint objectives; outlet temperature uses and inlet heat flux uses . The multiplier acts on , a different object from the main discrete residual convention. Integrating by parts once and collecting interior and endpoint coefficients gives:
逐步推导:伴随在每一端给两个条件
Step by step: two adjoint conditions at each end
A.3 的两个端点方程各有四个分量,而 、 各只有两个未知数;消去它们,每端剩两个只含 的条件。入口 , 的两行是 、,所以入口方程的后两个分量不含 :
Each endpoint equation of A.3 has four components, while and have only two unknowns each; eliminating them leaves two conditions on at each end. At the inlet , the rows of are and , and the last two components of the inlet equation contain no :
加热出口 。由 与 逐项求导,再在出口用 、 与 :
At the heated outlet . Differentiate and term by term, then use , and at the outlet:
把 的两行 与 代入 ,逐分量写出:
Substitute the two rows of , and , into component by component:
前两式确定 ,代入后两式就是出口的两个伴随条件。所以伴随和原始问题一样,每端两个条件,但给定的是互补的分量:原始问题在入口给定 、,伴随在入口给定与自由通量 、 共轭的 、(图 A.1)。制造解算例的出口条件是 、,同理出口的伴随条件落在 、 上。
The first two determine ; substituted into the last two they are the two outlet adjoint conditions. Like the primal problem, the adjoint thus has two conditions at each end, but on complementary components: the primal prescribes and at the inlet, the adjoint prescribes there the components and conjugate to the free fluxes and (Figure A.1). In the manufactured case the outlet conditions are on and , and likewise the outlet adjoint conditions fall on and .
A.4 求端点乘子并保留完整梯度
A.4 Solve endpoint multipliers and retain the full gradient
入口自由变量是两个通量,所以入口条件首先要求 、。从出口反向积分一个由目标驱动的特解和两个齐次响应,再用这两条入口条件解 的 系统,形成实际终值并重新积分。入口另外两个条件确定 。
The two inlet fluxes are free, so first impose and . Integrate one objective-forced particular solution and two homogeneous responses backward from the outlet. Use these two inlet conditions to solve a system for , form the actual terminal value and integrate again. The remaining inlet conditions determine .
入口温度项可化为 ;出口参数项必须保留 。入口热流目标会同时改变入口伴随条件与显式目标项。反向积分梯度时积分步长为负,代码最终取负号恢复从 0 到 1 的积分方向,这与任意更改伴随符号无关。
The inlet-temperature contribution reduces to ; retain the outlet parameter term . An inlet heat-flux objective changes both inlet adjoint conditions and direct objective terms. Gradient quadrature marched backward has negative integration steps, so the implementation negates it to recover the 0-to-1 integral, not to change the adjoint convention arbitrarily.
这是先推导连续 ODE 伴随再作数值积分,不是 RK4 程序的离散转置,也不调用有限体积残差。它仍共用物性及源项定义,独立性体现在方程、边界求解与导数路线。下方可调整 ODE 步数,检查射击残差、连续伴随边界残差及完整重算连续差分,再与正文总设计梯度对照。
This derives a continuous ODE adjoint before numerical integration; it is neither the discrete transpose of RK4 nor a call to the finite-volume residual. Transport and source definitions are shared, while equations, boundary solving and derivative routes are distinct. Vary ODE steps below, inspect shooting and continuous adjoint boundary residuals, and compare fully re-solved continuous differences before comparing total gradients with the main discrete model.
solve the continuous shooting problem for z(x) compute f_z, f_D and objective-density partials along the trajectory march one forced adjoint and two homogeneous responses backward solve the two inlet free-flux conditions for outlet multipliers march the actual adjoint and gradient quadrature backward reverse the quadrature sign and add both endpoint contributions verify by independently re-solving the continuous BVP after perturbation
逐步推导:三种目标从哪里驱动伴随
Step by step: where each objective drives the adjoint
三种目标只在 、、 中出现一处,伴随方程的其余部分完全相同。温度跟踪 给出体内源项 ,两端的 为零,所以入口 。出口温度 只有 ,体内方程是齐次的,伴随完全由出口条件驱动。入口热流 由上面的 得到:
Each objective appears in exactly one of , and ; the rest of the adjoint problem is identical. Temperature tracking, , gives the interior source and zero endpoint terms, so at the inlet. The outlet temperature has only ; the interior equation is homogeneous and the adjoint is driven entirely by the outlet condition. The inlet heat flux follows from above:
图 A.2(c) 的入口值正是这两个数。入口温度的导数是 A.4 的 。出口温度目标没有 ,所以 :入口温度升高 1,出口温度只升高约 0.97。入口热流目标的 还依赖 、,它们进入 ,不改变上面两个条件。
These are the inlet values in Figure A.2(c). The inlet-temperature derivative is from A.4. The outlet-temperature objective has no , so : raising the inlet temperature by 1 raises the outlet temperature by only about 0.97. The inlet heat-flux objective's also depends on and ; these enter and leave the two conditions above unchanged.
附录 B:黏性通量的局部求导
Appendix B: local viscous-flux differentiation
B.1 对一个实际面检查完整链式法则
B.1 Check the complete chain rule on an actual face
选择面编号后,表中显示该面的离散梯度、物性、应力和热流及其 tangent(代码名 x_d)。当前状态种子固定为 ,净通量的伴随种子固定为 ;控件改变面编号,不改变种子。局部反传返回全局状态槽位上的贡献,因为边界和二阶重构可能依赖多个邻点。
Select a face to display its discrete gradients, transport, stress and heat flux with their tangents (code names x_d). The fixed state seed is and the adjoint seed of the net flux is . The control changes the face, not the seeds. Local reverse accumulation returns contributions to global state slots because boundaries and second-order reconstruction can depend on several neighbours.
这个局部点积只检查状态路径,参数种子 。它不能替代物性参数或热源参数的完整差分检查。边界面也可选,但必须按实际边界闭合解释表中数据;格点端点的梯度是第 4 节的三点单侧差分;随后入口三行与出口动量、能量两行被约束替换,出口质量行仍是平衡行。
This local dot product checks only state paths, with the parameter seed . It does not replace full differences for transport or heat-source parameters. Boundary faces can be selected, but their values follow actual closures; node endpoint gradients use the three-point one-sided differences of Section 4; the three inlet rows and the outlet momentum and energy rows are then replaced by constraints, while the outlet mass row stays a balance row.
逐步推导:面模板决定 的带宽
Step by step: face stencils fix the bandwidth of
第 个控制体的残差只含它两侧两个面的通量(再减去源项),所以残差块 依赖的状态块,是这两个面模板的并集。内部面的模板取自图 B.1(a)(b):
The residual of control volume contains only the fluxes of its two faces (minus sources), so the state blocks that residual block depends on are the union of the two face stencils. For interior faces, Figure B.1(a)(b) gives:
按标量计,块 的第一行(行号 )最远用到第 列,最后一行(行号 )最远用到第 列,其中一阶 、二阶 。所以上、下带宽都是 :一阶 5,二阶 8。
Counting scalars, the first row of block (row ) reaches column and its last row (row ) reaches column , with at first order and at second. Both bandwidths are therefore : 5 at first order and 8 at second.
格点页还要看两端。入口节点的三行被约束 、、 替换,只依赖节点 0。出口加热约束 、 用三点单侧差分(图 B.1(d)),依赖节点 到 :出口约束行(行号 、)最远到第 列,相距 。所以一阶矩阵的下带宽是 8 而不是 5,转置后就是附录 C 中 的上带宽 8;二阶时内部已经是 8,不再增加。
The node page also needs the two ends. The inlet node rows are replaced by the constraints on , and and depend on node 0 only. The heated outlet constraints and use three-point one-sided differences (Figure B.1(d)) and depend on nodes to : the outlet constraint rows ( and ) reach column , a distance of . The first-order matrix therefore has lower bandwidth 8, not 5, which becomes the upper bandwidth 8 of in Appendix C; at second order the interior already has 8 and nothing grows.
B.2 热流目标的显式参数导数
B.2 Direct parameter derivatives of the heat-flux objective
固定状态时,格点梯度与面温度都固定;格心入口面温度和边界梯度仍显式依赖入口温度参数。分别设置状态种子 与参数种子 ,才能辨认 和 。总梯度必须同时包含显式目标与残差反传两部分。
At fixed state, node gradients and face temperatures are fixed. At the cell inlet, face temperature and boundary gradient still depend explicitly on the prescribed temperature parameter. Separate the state seed and the parameter seed to identify and . The total gradient retains both direct objective and residual-reverse contributions.
B.3 可微范围
B.3 Differentiability scope
本页没有 limiter,开放参数限制在光滑正值分支,物性完整求导。越界或 Newton 失败会清空旧结果并报告失败。若加入截断、裁剪、激波捕捉或分支选择,需要重新定义求导对象和可微范围;当前点积与差分结果不能自动延伸到那些场景。
There is no limiter, parameters target a smooth positive branch, and transport is fully differentiated. Invalid parameters or Newton failure clear stale results and report failure. Clipping, shock capture or branch selection would require a new derivative contract and differentiability analysis; current dot and difference checks do not automatically extend to those settings.
附录 C:显式矩阵与直接伴随求解
Appendix C: explicit matrices and direct adjoint solves
C.1 这里显式组装,不采用 matrix-free 直接求解
C.1 Explicit assembly for the direct solve
每次 JVP 以单位状态种子 、参数种子 给出 ,即一列。把完整精确矩阵转置后,再作带部分主元的 LU。这里存储了稠密矩阵,所以直接求解路径明确不是 matrix-free;一阶和二阶都使用各自实际残差的精确矩阵,不拿预处理矩阵替代。
Each JVP with a unit state seed and the parameter seed gives , one column. Transpose the complete exact matrix, then factor it by partial-pivot LU. This stores a dense matrix and is explicitly not a matrix-free solve. Both spatial orders use their actual residual's exact matrix, not the preconditioning matrix.
是行置换矩阵,与 GS 的 无关; 是上三角因子,与流场状态 无关。每步选当前列绝对值最大的主元并交换整行,包括之前保存的下三角乘数。随后前代、回代,最后用原始精确转置接口检查真残差。
The row permutation is unrelated to the GS matrix , and upper factor is unrelated to flow state . Each stage selects the largest-magnitude pivot in the current column and swaps complete rows, including previously stored lower multipliers. Forward and backward substitution follow, then the original exact transpose interface checks the true residual.
for j in state slots:
A[:,j] = L.jvp(U_d=unit(j), D_d=0).R_d
M = transpose(A); rhs = -objective_state_gradient
for k = 0..nd-1:
pivot = argmax(abs(M[k:nd,k]))
swap complete rows and permutation
eliminate below pivot; store multipliers
y = forward_substitution(L, permuted_rhs)
psi = backward_substitution(U, y)
check norm(exact_AT(psi) + jU)小网格适合直接法:组装后可观察每一行,分解后多个目标可复用因子;流场、参数、阶数或边界类型改变就要重新组装。稠密 LU 的存储和运算量分别随未知量数平方、立方增长,不能由本页小模型的速度推断大规模非结构问题也应采用稠密直接法。滑块显示消元阶段;下方解向量表始终显示完成分解后的解,不是滑块阶段的未完成解。
Small meshes suit a direct method: inspect each assembled row and reuse factors for several objectives. Reassemble after changes in flow, parameters, order or boundary type. Dense LU storage and work grow quadratically and cubically with the number of unknowns, so small-model timings do not prescribe dense direct methods for large unstructured problems. The slider shows elimination stages; the solution table always shows the completed solve, not a partial solution at the selected stage.
逐步推导:主元为什么要换行,换行带来多少填充
Step by step: why pivoting swaps rows here, and how much fill it creates
由 块组成(本页 N = 12 时 39 维)。附录 B 的面模板给出带宽:一阶下、上带宽为 5、8(上带宽 8 来自出口约束行的三点单侧差分),二阶都是 8。与 Burgers 不同,对角元并不总是本列剩余元素中绝对值最大者,所以部分主元真的会交换行:一阶 12 次、二阶 26 次。一阶第一次换行发生在 :当前对角元的绝对值是 6.65,第 10 行同列元素的绝对值是 8.15,于是交换这两行。
consists of blocks (dimension 39 here for N = 12). The face stencils of Appendix B give the bandwidths: 5 below and 8 above at first order (the upper bandwidth 8 comes from the three-point one-sided differences of the outlet constraint rows), and 8 on both sides at second order. Unlike Burgers, the diagonal is not always the largest remaining entry of its column, so partial pivoting really swaps rows: 12 times at first order and 26 at second. The first first-order swap happens at : the current diagonal entry has magnitude 6.65 and row 10 has 8.15 in the same column, so the two rows are exchanged.
换行最多让 变宽多少:带宽为 、 时,第 步的主元只能来自第 到 行(更下面的行在第 列为零)。被换到第 行的行,非零元至多到第 列,所以 的上带宽不超过 。
How much wider can swaps make ? With bandwidths and , the pivot of step can only come from rows to (rows further down are zero in column ). A row moved to position has nonzeros at most up to column , so the upper bandwidth of never exceeds .
一阶有行从下面的块行换上来,带着更靠右的非零元: 的上带宽从 8 增加到 9(不超过 ),消元还在原来为零的位置产生 29 个新的非零元(填充)。二阶有行从下面的块行换上来,带着更靠右的非零元: 的上带宽从 8 增加到 11(不超过 ),消元还在原来为零的位置产生 77 个新的非零元(填充)。图 C.1 是两种阶数的 ,图 C.2 是一阶分解后的因子。
At first order rows move up from block rows below, bringing entries further right: the upper bandwidth of grows from 8 to 9 (never beyond ), and elimination creates 29 new nonzeros where the matrix had zeros (fill). At second order rows move up from block rows below, bringing entries further right: the upper bandwidth of grows from 8 to 11 (never beyond ), and elimination creates 77 new nonzeros where the matrix had zeros (fill). Figure C.1 shows for both orders and Figure C.2 the first-order factors.
逐步推导:一次分解同时服务 tangent 与 adjoint
Step by step: one factorisation serves both the tangent and the adjoint
C.1 分解的是 。若改为分解 ,同一组因子也能解伴随:由 得 ,伴随只是把两次三角求解的顺序和方向换过来:
C.1 factors . Had it factored , the same factors would also solve the adjoint: gives , and the adjoint merely swaps the order and direction of the two triangular solves:
逐步推导:后向误差换算成前向误差
Step by step: turning the backward error into a forward error
用 C.1 检查的真残差定义后向误差:
Define the backward error from the true residual checked in C.1:
它恰好是使 成为某个扰动系统精确解的最小相对扰动(Rigal–Gaches):存在 、 使 ,,。精确解满足 ,两式相减得 ,取范数:
It is exactly the smallest relative perturbation for which solves a perturbed system (Rigal–Gaches): there are and with , and . The exact solution satisfies ; subtracting gives , and taking norms:
本页 N = 12(格点,温度跟踪目标):一阶 、;二阶 、。伴随的相对前向误差不超过约 。作为检查,用补偿点积算残差、迭代细化三次得到 ,LU 解的实际相对误差一阶是 、二阶是 ,都在界内。
Here N = 12 (node, temperature-tracking objective): first order and ; second order and . The relative forward error of the adjoint is therefore at most about . As a check, three steps of iterative refinement with residuals from a compensated dot product give ; the actual relative error of the LU solution is at first order and at second, both within the bound.
完整可运行实现Complete runnable 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);
/* 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);
/* Plane 1D laminar compressible Navier–Stokes. R = Fc_right-Fc_left-Fv_right+Fv_left-S. */
(function(root){'use strict';
const Q=typeof module!=='undefined'?require('../q1d/reconstruction/math'):root.QMath;
const N=typeof module!=='undefined'?require('../experiments/numerics'):root.EduNumerics;
const {norm,dot,real,complex,tape,gmres}=Q,{zeros,assemble,transpose,factor,solve:lu,mv}=N;
const cp=3.5,cv=2.5,mass=.35;
function physics(o,variable){const {c,add,sub,mul,div,pow}=o;
const prim=U=>{const rho=U[0],u=div(U[1],rho),T=div(sub(div(U[2],rho),mul(c(.5),mul(u,u))),c(cv));return {rho,u,T,p:mul(rho,T)};};
const cons=(rho,u,T)=>[rho,mul(rho,u),mul(rho,add(mul(c(cv),T),mul(c(.5),mul(u,u))))];
const transport=(T,b)=>{const mu=variable?mul(b[2],div(mul(c(1.4),pow(T,1.5)),add(T,c(.4)))):b[2];return {mu,kappa:div(mul(c(cp),mu),b[3])};};
const flux=U=>{const q=prim(U);return [U[1],add(mul(U[1],q.u),q.p),mul(q.u,add(U[2],q.p))];};
return {prim,cons,transport,flux};
}
function exact(x){const u=mass+.03*Math.sin(Math.PI*x)+.01*Math.sin(2*Math.PI*x),T=1+.05*Math.sin(Math.PI*x)+.02*Math.sin(2*Math.PI*x);return [mass/u,mass,(mass/u)*(cv*T+.5*u*u)];}
function manufactured(x,mu0,Pr,variable){const s=Math.sin(Math.PI*x),c=Math.cos(Math.PI*x),u=mass+.03*s+.01*Math.sin(2*Math.PI*x),T=1+.05*s+.02*Math.sin(2*Math.PI*x),ux=.03*Math.PI*c+.02*Math.PI*Math.cos(2*Math.PI*x),tx=.05*Math.PI*c+.04*Math.PI*Math.cos(2*Math.PI*x),uxx=-.03*Math.PI**2*s-.04*Math.PI**2*Math.sin(2*Math.PI*x),txx=-.05*Math.PI**2*s-.08*Math.PI**2*Math.sin(2*Math.PI*x);
const mu=variable?mu0*1.4*T**1.5/(T+.4):mu0,mut=variable?mu*(1.5/T-1/(T+.4)):0,k=cp*mu/Pr,kt=cp*mut/Pr,tau=4/3*mu*ux,taux=4/3*(mut*tx*ux+mu*uxx);
return [0,mass*ux+mass*(tx/u-T*ux/u**2)-taux,mass*(cp*tx+u*ux)-ux*tau-u*taux-kt*tx*tx-k*txx];}
function model({node=false,n=12,order=2,variable=false,mode='heat',objective='tracking',mu=.15,Pr=.72}={}){
if(n<4)throw Error('At least four intervals required');const h=1/n,m=node?n+1:n,nd=3*m,x=Array.from({length:m},(_,i)=>(i+(node?0:.5))*h),fx=node?[0,...Array.from({length:n},(_,i)=>(i+.5)*h),1]:Array.from({length:n+1},(_,i)=>i*h),vol=x.map((_,i)=>fx[i+1]-fx[i]);
const params=[mode==='heat'?.03:0,0,mu,Pr,1,1],names=['a₀','a₁','μ_ref','Pr','T_in',mode==='heat'?'p_b':'T_out'];
function source(o,xx,b){const {c,add,mul}=o,base=mode==='mms'?manufactured(xx,mu,Pr,variable):[0,0,0];return [c(0),c(base[1]),add(c(base[2]),add(mul(b[0],c(Math.sin(Math.PI*xx)**2)),mul(b[1],c(Math.sin(2*Math.PI*xx)**2))))];}
function weights(i,xx,q=order){if(q===1)return [[i,1]];const d=(xx-x[i])/h;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,-d/2],[i+1,d/2]];}
function evaluate(o,U,b=params,q=order){const {c,add,sub,mul,div,pow}=o,ph=physics(o,variable),R=Array.from({length:nd},()=>c(0)),local=[],sources=[],qstates=Array.from({length:m},(_,i)=>ph.prim(U.slice(3*i,3*i+3)));
const sum=a=>a.reduce((s,v)=>add(s,v),c(0)),lin=(a,w)=>sum(w.map(([i,v])=>mul(c(v),a[i]))),rec=(i,xx)=>[0,1,2].map(k=>lin(U.filter((_,j)=>j%3===k),weights(i,xx,q))),at=(key,i)=>qstates[i][key];
const inlet=ph.cons(c(1),c(mass),b[4]);
for(let f=0;f<=m;f++){const L=f-1,Ri=f,xx=fx[f];let ul,ur,face,ux,tx;
if(f>0&&f<m){ul=rec(L,xx);ur=rec(Ri,xx);face=ph.prim([0,1,2].map(k=>mul(c(.5),add(U[3*L+k],U[3*Ri+k]))));ux=mul(c(1/h),sub(at('u',Ri),at('u',L)));tx=mul(c(1/h),sub(at('T',Ri),at('T',L)));}
else if(node){const i=f===0?0:m-1;ul=ur=U.slice(3*i,3*i+3);face=ph.prim(ul);const w=f===0?[[0,-1.5/h],[1,2/h],[2,-.5/h]]:[[m-1,1.5/h],[m-2,-2/h],[m-3,.5/h]];ux=lin(qstates.map(s=>s.u),w);tx=lin(qstates.map(s=>s.T),w);}
else if(f===0){ul=ur=inlet;face=ph.prim(inlet);ux=mul(c(1/h),sum([mul(c(-8/3),face.u),mul(c(3),at('u',0)),mul(c(-1/3),at('u',1))]));tx=mul(c(1/h),sum([mul(c(-8/3),face.T),mul(c(3),at('T',0)),mul(c(-1/3),at('T',1))]));}
else {const ext=key=>sub(mul(c(1.5),at(key,m-1)),mul(c(.5),at(key,m-2)));const rho=ext('rho'),u=mode==='mms'?c(mass):ext('u'),T=mode==='mms'?b[5]:ext('T');ul=ur=ph.cons(rho,u,T);face=ph.prim(ul);ux=mode==='mms'?mul(c(1/h),sum([mul(c(8/3),u),mul(c(-3),at('u',m-1)),mul(c(1/3),at('u',m-2))])):c(0);tx=mode==='mms'?mul(c(1/h),sum([mul(c(8/3),T),mul(c(-3),at('T',m-1)),mul(c(1/3),at('T',m-2))])):c(0);}
const tr=ph.transport(face.T,b);let tau=mul(c(4/3),mul(tr.mu,ux)),heat=mul(c(-1),mul(tr.kappa,tx));
if(!node&&f===m&&mode==='heat'){tau=sub(face.p,b[5]);ux=div(tau,mul(c(4/3),tr.mu));}
let fc=ph.flux(ul),alpha=c(0);if(f>0&&f<m){const a=ph.prim(ul),bb=ph.prim(ur),fr=ph.flux(ur);alpha=mul(c(.5),sum([a.u,bb.u,pow(mul(c(1.4),a.T),.5),pow(mul(c(1.4),bb.T),.5)]));fc=fc.map((v,k)=>sub(mul(c(.5),add(v,fr[k])),mul(mul(c(.5),alpha),sub(ur[k],ul[k]))));}
const fv=[c(0),tau,sub(mul(face.u,tau),heat)],H=fc.map((v,k)=>sub(v,fv[k]));
H.forEach((v,k)=>{if(L>=0)R[3*L+k]=add(R[3*L+k],v);if(Ri<m)R[3*Ri+k]=sub(R[3*Ri+k],v);});local.push({x:xx,ul,ur,fc,fv,H,ux,tx,tau,heat,mu:tr.mu,kappa:tr.kappa,u:face.u,T:face.T,p:face.p,alpha});
}
let J=c(0);for(let i=0;i<m;i++){const mid=(fx[i]+fx[i+1])/2,half=vol[i]/2,S=[c(0),c(0),c(0)];for(const z of [-1/Math.sqrt(3),1/Math.sqrt(3)]){const xx=mid+half*z,ss=source(o,xx,b);ss.forEach((v,k)=>S[k]=add(S[k],mul(c(half),v)));const T=ph.prim(rec(i,xx)).T,d=sub(T,c(1+.04*Math.sin(Math.PI*xx)));if(objective==='tracking')J=add(J,mul(c(.5*half),mul(d,d)));}S.forEach((v,k)=>R[3*i+k]=sub(R[3*i+k],v));sources.push(S);}
if(objective==='outlet')J=local[m].T;if(objective==='heatflux')J=local[0].heat;
const raw=R.slice(),constraints=[];if(node){const put=(row,v)=>{R[row]=v;constraints.push(row);};put(0,sub(qstates[0].rho,c(1)));put(1,sub(qstates[0].u,c(mass)));put(2,sub(qstates[0].T,b[4]));if(mode==='mms'){put(3*(m-1)+1,sub(qstates[m-1].u,c(mass)));put(3*(m-1)+2,sub(qstates[m-1].T,b[5]));}else{put(3*(m-1)+1,sub(sub(local[m].p,local[m].tau),b[5]));put(3*(m-1)+2,local[m].heat);}}
return {R,J,raw,local,sources,constraints};
}
function linearize(U,b=params,q=order){const t=tape(),u=U.map(t.op.input),p=b.map(t.op.input),out=evaluate(t.op,u,p,q),vals=a=>a.map(i=>t.val[i]);
const extract=(a,d)=>Object.fromEntries(Object.entries(a).map(([k,v])=>[k,k==='x'?v:Array.isArray(v)?v.map(i=>d[i]):d[v]]));
/* Tapenade-style names: x_d is the tangent (dot) of x, x_b its adjoint (bar). jvp(U_d,D_d) returns the tangents
R_d, J_d, ... under the keys R, raw, J, local, sources; vjp(R_b,J_b) returns U_b and D_b under the keys U and p. */
return {R:vals(out.R),J:t.val[out.J],jvp:(U_d,D_d=zeros(b.length))=>{const x_d=t.forward([...U_d,...D_d]);return {R:out.R.map(i=>x_d[i]),raw:out.raw.map(i=>x_d[i]),J:x_d[out.J],local:out.local.map(a=>extract(a,x_d)),sources:out.sources.map(a=>a.map(i=>x_d[i]))};},vjp:(R_b,J_b=0)=>{const x_b=t.reverse([...out.R,out.J],[...R_b,J_b]);return {U:x_b.slice(0,nd),p:x_b.slice(nd)};},constraintVjp:R_b=>t.reverse(out.constraints.map(i=>out.R[i]),out.constraints.map(i=>R_b[i])).slice(0,nd),faceVjp:(f,H_b)=>t.reverse(out.local[f].H,H_b).slice(0,nd)};
}
function precondition(U,b=params){const P=assemble(linearize(U,b,1),nd),blocks=Array.from({length:m},(_,i)=>factor(P.slice(3*i,3*i+3).map(r=>r.slice(3*i,3*i+3))));
// One forward block GS sweep B=(diag(P)+L)^-1; reverse sweep is exactly B^T.
const apply=(r,tr=false)=>{const z=zeros(nd);for(let it=0;it<m;it++){const i=tr?m-1-it:it,rr=r.slice(3*i,3*i+3);for(let j=tr?i+1:0;tr?j<m:j<i;j++)for(let k=0;k<3;k++)for(let l=0;l<3;l++)rr[k]-=(tr?P[3*j+l][3*i+k]:P[3*i+k][3*j+l])*z[3*j+l];const block=tr?factor(transpose(P.slice(3*i,3*i+3).map(r=>r.slice(3*i,3*i+3)))):blocks[i],v=lu(block,rr);v.forEach((a,k)=>z[3*i+k]=a);}return z;};return {P,apply};}
function linearSolve(L,rhs,pre,trans=false,method='gmres',tol=1e-11){const op=v=>trans?L.vjp(v).U:L.jvp(v).R;
if(method==='lu'){const A=assemble(L,nd),x=lu(factor(trans?transpose(A):A),rhs);return {x,history:[norm(rhs),norm(op(x).map((v,i)=>v-rhs[i]))],converged:true,A};}
if(method==='gmres')return gmres(op,rhs,r=>pre.apply(r,trans),tol,nd);
let x=zeros(nd),history=[];for(let it=0;it<1500;it++){const ax=op(x),r=rhs.map((v,i)=>v-ax[i]),rn=norm(r);history.push(rn);if(rn<tol)return {x,history,converged:true};if(!Number.isFinite(rn)||rn>1e8)break;const d=pre.apply(r,trans);x=x.map((v,i)=>v+.65*d[i]);}return {x,history,converged:false};}
function valid(U,b=params){try{const z=evaluate(real,U,b);return z.local.every(f=>[f.ul,f.ur].every(v=>{const s=physics(real,variable).prim(v);return s.rho>.1&&s.T>.3&&s.u>.05&&s.u<.9*Math.sqrt(1.4*s.T);}));}catch{return false;}}
function initial(){return x.flatMap(xx=>mode==='mms'?exact(xx):[1,mass,cv+.5*mass**2]);}
function solve(b=params,start,method='gmres',tol=2e-12){let U=start?start.slice():initial();const history=[],states=[];for(let it=0;it<35;it++){const L=linearize(U,b),rn=norm(L.R);history.push(rn);states.push(U.slice());if(rn<tol)return {U,history,states,converged:true};const pre=precondition(U,b),v=linearSolve(L,L.R.map(x=>-x),pre,false,method,2e-13);if(!v.converged)throw Error('Primal linear solve failed');let step=1,ok=false;while(step>1e-8){const trial=U.map((x,i)=>x+step*v.x[i]);if(valid(trial,b)&&norm(evaluate(real,trial,b).R)<(1-1e-4*step)*rn){U=trial;ok=true;break;}step*=.5;}if(!ok)throw Error('No admissible Newton correction');}throw Error('Primal residual did not converge');}
function sensitivity(sol,b=params,k=0,method='gmres'){const L=linearize(sol.U,b),pre=precondition(sol.U,b),D_d=zeros(b.length);D_d[k]=1;const tangent=linearSolve(L,L.jvp(zeros(nd),D_d).R.map(x=>-x),pre,false,method),adjoint=linearSolve(L,L.vjp(zeros(nd),1).U.map(x=>-x),pre,true,method);if(!tangent.converged||!adjoint.converged)throw Error('Derivative solve did not converge');return {L,pre,tangent,adjoint,gradient:L.vjp(adjoint.x,1).p,directional:L.jvp(tangent.x,D_d).J};}
function complexGradient(sol,b=params,k=0){const h=1e-25,p=b.map((v,i)=>[v,i===k?h:0]),U=sol.U.map(v=>[v,0]),L=linearize(sol.U,b),pre=precondition(sol.U,b);for(let it=0;it<4;it++){const z=evaluate(complex,U,p),r=z.R.map(v=>v[1]/h);if(norm(r)<1e-10)return {gradient:z.J[1]/h,residual:norm(r)};const s=linearSolve(L,r.map(v=>-v),pre,false,'gmres',1e-13);if(!s.converged)throw Error('Complex correction failed');U.forEach((v,i)=>v[1]+=h*s.x[i]);}const z=evaluate(complex,U,p),residual=norm(z.R.map(v=>v[1]/h));if(residual>1e-9)throw Error('Complex residual not converged');return {gradient:z.J[1]/h,residual};}
return {node,n,m,nd,h,x,fx,vol,order,mode,variable,objective,params,names,source,weights,initial,valid,solve,linearize,precondition,linearSolve,sensitivity,complexGradient,evaluate:(U,b=params)=>evaluate(real,U,b),evaluateWith:evaluate,physics:physics(real,variable)};
}
const api={model,physics,exact,manufactured,cp,cv,mass};if(typeof module!=='undefined')module.exports=api;else root.NS1D=api;
})(typeof window!=='undefined'?window:globalThis);
/* Independent spatial ODE BVP and continuous ODE adjoint. No FV residual/Jacobian calls. */
(function(root){'use strict';const Q=typeof module!=='undefined'?require('../q1d/reconstruction/math'):root.QMath,N=typeof module!=='undefined'?require('../experiments/numerics'):root.EduNumerics,C=typeof module!=='undefined'?require('./core'):root.NS1D;
const {real,tape,norm,dot}=Q,{zeros,factor,solve:lu}=N,m=C.mass,cp=C.cp;
const add=(a,b,s=1)=>a.map((v,i)=>v+s*b[i]);
function rk(f,x,y,h){const a=f(x,y),b=f(x+h/2,add(y,a,h/2)),c=f(x+h/2,add(y,b,h/2)),d=f(x+h,add(y,c,h));return y.map((v,i)=>v+h*(a[i]+2*b[i]+2*c[i]+d[i])/6);}
function equations(model,o,x,z,b){const {c,add,sub,mul,div}=o,[u,T,P,H]=z,tr=C.physics(o,model.variable).transport(T,b),tau=sub(add(mul(c(m),u),div(mul(c(m),T),u)),P),ux=div(tau,mul(c(4/3),tr.mu)),ht=mul(c(m),add(mul(c(cp),T),mul(c(.5),mul(u,u)))),heat=add(sub(H,ht),mul(u,tau)),tx=div(mul(c(-1),heat),tr.kappa),S=model.source(o,x,b),d=sub(T,c(1+.04*Math.sin(Math.PI*x))),j=model.objective==='tracking'?mul(c(.5),mul(d,d)):c(0);return {f:[ux,tx,S[1],S[2]],j,heat,tau};}
function boundary(model,o,z,b){const {c,sub,mul}=o;return model.mode==='mms'?[sub(z[0],c(m)),sub(z[1],b[5])]:[sub(sub(z[2],mul(c(m),z[0])),b[5]),equations(model,o,1,z,b).heat];}
function derivatives(model,x,z,b){const t=tape(),zz=z.map(t.op.input),bb=b.map(t.op.input),out=equations(model,t.op,x,zz,bb),ids=[...out.f,out.j],jac=ids.map(i=>t.reverse([i],[1]));return {f:out.f.map(i=>t.val[i]),j:t.val[out.j],A:jac.slice(0,4).map(r=>r.slice(0,4)),B:jac.slice(0,4).map(r=>r.slice(4)),jz:jac[4].slice(0,4),jp:jac[4].slice(4)};}
function endpoint(model,z,b,which){const t=tape(),zz=z.map(t.op.input),bb=b.map(t.op.input),phi=which===0&&model.objective==='heatflux'?equations(model,t.op,0,zz,bb).heat:which===1&&model.objective==='outlet'?zz[1]:t.op.c(0),g=boundary(model,t.op,zz,bb),d=t.reverse([phi],[1]);return {phi:t.val[phi],phiz:d.slice(0,4),phip:d.slice(4),g:g.map(i=>t.val[i]),G:g.map(i=>t.reverse([i],[1]).slice(0,4)),Gp:g.map(i=>t.reverse([i],[1]).slice(4))};}
function integrate(model,b,initial,steps,variations=false){let y=[m,b[4],...initial];if(variations)y.push(...[0,0,0,0,1,0,0,1]);const states=[y.slice(0,4)],f=(x,y)=>{const d=derivatives(model,x,y.slice(0,4),b);return variations?[...d.f,...d.A.flatMap(r=>[0,1].map(k=>r.reduce((s,v,j)=>s+v*y[4+2*j+k],0)))]:d.f;};for(let i=0;i<steps;i++){y=rk(f,i/steps,y,1/steps);if(!y.every(Number.isFinite)||y[0]<=.02||y[1]<=.2||y[0]>.95*Math.sqrt(1.4*y[1]))throw Error('Continuous trajectory outside positive subsonic branch');states.push(y.slice(0,4));}return {states,Y:variations?Array.from({length:4},(_,i)=>y.slice(4+2*i,6+2*i)):null};}
function solve(model,b=model.params,steps=256,warm=null){if(!warm&&model.mode==='heat'&&Math.max(Math.abs(b[0]),Math.abs(b[1]))>.04){const base=b.slice();base[0]=base[1]=0;let r=solve(model,base,steps);for(let k=1;k<=6;k++){const next=b.slice();next[0]*=k/6;next[1]*=k/6;r=solve(model,next,steps,r.states[0].slice(2));}return r;}let guess=[m*m+1,m*(cp*b[4]+.5*m*m)];if(model.mode==='mms'){const z=C.exact(0),q=C.physics(real,model.variable).prim(z),mu=C.physics(real,model.variable).transport(q.T,b);const tau=4/3*mu.mu*.05*Math.PI;guess=[m*m+1-tau,m*(cp+.5*m*m)-m*tau-mu.kappa*.09*Math.PI];}
if(warm)guess=warm.slice();let run,history=[];for(let it=0;it<20;it++){run=integrate(model,b,guess,steps,true);const ep=endpoint(model,run.states.at(-1),b,1),rn=norm(ep.g);history.push(rn);if(rn<2e-12)break;const A=ep.G.map(r=>[0,1].map(k=>r.reduce((s,v,j)=>s+v*run.Y[j][k],0))),d=lu(factor(A),ep.g.map(v=>-v));let step=1,ok=false;while(step>1e-7){try{const trial=add(guess,d,step),r=integrate(model,b,trial,steps),err=norm(endpoint(model,r.states.at(-1),b,1).g);if(err<rn){guess=trial;ok=true;break;}}catch{}step*=.5;}if(!ok)throw Error('Continuous shooting failed');}
if(history.at(-1)>2e-11)throw Error('Continuous boundary residual not converged');
const states=run.states,fs=states.map((z,i)=>equations(model,real,i/steps,z,b).f);
function at(x){const i=Math.min(steps-1,Math.max(0,Math.floor(x*steps))),t=x*steps-i,h=1/steps;return states[i].map((v,k)=>(2*t**3-3*t*t+1)*v+(t**3-2*t*t+t)*h*fs[i][k]+(-2*t**3+3*t*t)*states[i+1][k]+(t**3-t*t)*h*fs[i+1][k]);}
let integral=0;for(let i=0;i<steps;i++){const x=i/steps,h=1/steps;integral+=h/6*(equations(model,real,x,states[i],b).j+4*equations(model,real,x+h/2,at(x+h/2),b).j+equations(model,real,x+h,states[i+1],b).j);}
return {states,at,steps,history,J:integral+endpoint(model,states[0],b,0).phi+endpoint(model,states.at(-1),b,1).phi};}
function adjoint(model,ref,b=model.params){const n=ref.steps,end=endpoint(model,ref.states.at(-1),b,1),start=endpoint(model,ref.states[0],b,0);
const march=(terminal,forcing=true,withGradient=false)=>{let y=[...terminal,...(withGradient?zeros(b.length):[])],values=Array(n+1);values[n]=terminal;const f=(x,y)=>{const d=derivatives(model,x,ref.at(x),b),v=Array.from({length:4},(_,i)=>-d.A.reduce((s,r,j)=>s+r[i]*y[j],0)-(forcing?d.jz[i]:0));if(withGradient)v.push(...b.map((_,i)=>d.jp[i]+d.B.reduce((s,r,j)=>s+r[i]*y[j],0)));return v;};for(let i=n;i>0;i--){y=rk(f,i/n,y,-1/n);values[i-1]=y.slice(0,4);}return {y,values};};
const a=march(end.phiz),r=end.G.map(v=>march(v,false)),rhs=[2,3].map(i=>-start.phiz[i]-a.y[i]),mat=[2,3].map(i=>r.map(v=>v.y[i])),eta=lu(factor(mat),rhs),terminal=end.phiz.map((v,i)=>v+end.G.reduce((s,r,j)=>s+eta[j]*r[i],0)),out=march(terminal,true,true),l0=out.y.slice(0,4),gradient=b.map((_,i)=>start.phip[i]+end.phip[i]-out.y[4+i]+eta.reduce((s,v,j)=>s+v*end.Gp[j][i],0)+(i===4?l0[1]+start.phiz[1]:0));
return {values:out.values,eta,gradient,boundaryResidual:Math.hypot(l0[2]+start.phiz[2],l0[3]+start.phiz[3])};}
const api={solve,adjoint,equations,derivatives,boundary,endpoint,rk};if(typeof module!=='undefined')module.exports=api;else root.NSContinuous=api;
})(typeof window!=='undefined'?window:globalThis);
(function(root){'use strict';const C=typeof module!=='undefined'?require('./core'):root.NS1D,N=typeof module!=='undefined'?require('../experiments/numerics'):root.EduNumerics;
function reference(m,i){if(m.node)return C.exact(m.x[i]);const a=m.fx[i],b=m.fx[i+1],r=[0,0,0],z=[-.9602898564975363,-.7966664774136267,-.525532409916329,-.1834346424956498,.1834346424956498,.525532409916329,.7966664774136267,.9602898564975363],w=[.1012285362903763,.2223810344533745,.3137066458778873,.362683783378362,.362683783378362,.3137066458778873,.2223810344533745,.1012285362903763];z.forEach((v,j)=>C.exact((a+b)/2+(b-a)*v/2).forEach((s,k)=>r[k]+=w[j]*s/2));return r;}
function accuracy(options,grids=[8,16,32,64]){const rows=[];for(const order of [1,2]){let prev;for(const n of grids){const m=C.model({...options,mode:'mms',n,order}),s=m.solve(),e=Math.sqrt(m.x.reduce((ss,_,i)=>ss+m.vol[i]*reference(m,i).reduce((sum,v,k)=>sum+(v-s.U[3*i+k])**2,0),0));rows.push({order,n,error:e,rate:prev?Math.log2(prev/e):null,residual:s.history.at(-1)});prev=e;}}return rows;}
/* The players' steps. Tapenade-style labels: x_d is the tangent (dot) of x, x_b its adjoint (bar); the tangent
player seeds U_d = v, D_d = 0, the adjoint player R_b with J_b = 0. */
function trace(m,sol,kind){const U=sol.U,L=m.linearize(U),raw=m.evaluate(U),U_d=U.map((_,i)=>Math.sin(i+1)),R_b=U.map((_,i)=>Math.cos(2*i+1)),tan=L.jvp(U_d),out=N.zeros(m.nd),steps=[];const step=(zh,en,face,details,updates=[])=>{for(const [i,value,replace]of updates)out[i]=replace?value:out[i]+value;steps.push({zh,en,face,details,values:out.slice()});},nm=s=>kind==='tangent'?s.replace(/^[A-Za-z]+/,a=>a+'_d'):s;
step('清零输出数组','Zero output',0,[]);
if(kind==='adjoint'){const Rraw_b=R_b.slice();raw.constraints.forEach(i=>Rraw_b[i]=0);if(m.node)step('反传约束并屏蔽旧平衡行','Reverse constraints and mask old balance rows',0,raw.constraints.map(i=>['R_b['+i+']',R_b[i]]),L.constraintVjp(R_b).map((x,i)=>[i,x]));for(let f=m.m;f>=0;f--){const H_b=[0,1,2].map(k=>(f>0?Rraw_b[3*(f-1)+k]:0)-(f<m.m?Rraw_b[3*f+k]:0)),back=L.faceVjp(f,H_b);step('形成净面通量种子','Form net-face seeds',f,H_b.map((v,k)=>['H_b['+k+']',v]));if(f>0&&f<m.m){const z=raw.local[f],tau_b=-H_b[1]-z.u*H_b[2],q_b=H_b[2],u_b=-z.tau*H_b[2];step('反传黏性功与热通量','Reverse viscous work and heat flux',f,[['u_b (viscous)',u_b],['tau_b',tau_b],['q_b',q_b]]);step('反传应力、梯度与输运系数','Reverse stress, gradients and transport',f,[['mu_b',4/3*z.ux*tau_b],['ux_b',4/3*z.mu*tau_b],['kappa_b',-z.tx*q_b],['tx_b',-z.kappa*q_b]]);}step('反传通量、物性、梯度和重构,累加状态','Reverse flux, transport, gradients and reconstruction; accumulate state',f,back.map((v,i)=>['U_b['+i+'] +=',v]).filter(a=>Math.abs(a[1])>1e-15),back.map((v,i)=>[i,v]));}}
else{for(let f=0;f<=m.m;f++){const z=kind==='tangent'?tan.local[f]:raw.local[f];step('收集并重构左右状态','Gather and reconstruct traces',f,[...z.ul.map((v,k)=>[nm('Uminus['+k+']'),v]),...z.ur.map((v,k)=>[nm('Uplus['+k+']'),v])]);step('计算梯度和输运系数','Evaluate gradients and transport',f,['ux','tx','mu','kappa'].map(k=>[nm(k),z[k]]));step('计算应力、热流及能量黏性通量','Evaluate stress, heat flux and viscous work',f,[[nm('tau'),z.tau],[nm('q'),z.heat],...z.fv.map((v,k)=>[nm('Fv['+k+']'),v])]);step('计算对流与净通量','Evaluate convective and net flux',f,[...z.fc.map((v,k)=>[nm('Fc['+k+']'),v]),...z.H.map((v,k)=>[nm('H['+k+']'),v])]);const updates=[];z.H.forEach((v,k)=>{if(f>0)updates.push([3*(f-1)+k,v]);if(f<m.m)updates.push([3*f+k,-v]);});step('向左右控制体分发','Scatter to neighbouring volumes',f,[],updates);}
const src=kind==='tangent'?tan.sources:raw.sources;src.forEach((ss,i)=>step('减去体积源项','Subtract integrated source',i,ss.map((v,k)=>[nm('S['+k+']'),v]),ss.map((v,k)=>[3*i+k,-v])));if(m.node)step('替换边界约束行','Replace boundary rows',0,[],raw.constraints.map(i=>[i,kind==='tangent'?tan.R[i]:raw.R[i],true]));}
const expected=kind==='adjoint'?L.vjp(R_b).U:kind==='tangent'?tan.R:raw.R;return {steps,expected,error:N.norm(out.map((v,i)=>v-expected[i]))};}
const api={reference,accuracy,trace};if(typeof module!=='undefined')module.exports=api;else root.NSStudy=api;
})(typeof window!=='undefined'?window:globalThis);