算例是一张环形(O 型)混合网格:内圈是物面,外圈是远场特征边界,贴壁一层用四边形、外层用三角形。物面称为无粘壁面,在通量内部用一个弱闭合处理:每个壁面半面上令 h=0。它是让边循环最简单的离散闭合,不是这个方程在物面上的物理条件(第 4 节);粘性壁面是另一对页面——二维标量对流扩散方程——的题目。未知量放在网格节点上,控制体是围绕节点的中位对偶体。本页先介绍方程和它的作用,再把对偶控制体是怎么“围”出来的一步步画清楚,然后把同一个边循环依次改写成 matrix-free 前向(计算 Av)和 matrix-free 伴随(计算 ATw)。每个循环都可以逐句播放。
The test case is an annular (O-type) hybrid mesh: the inner ring is the body surface, the outer ring a far-field characteristic boundary, with quadrilaterals in the layer against the wall and triangles outside it. The body is called the inviscid wall and is closed weakly, inside the flux: every wall half-face gets h=0. That is a discrete closure that keeps the edge loop simple, not the physical boundary condition of this equation at the body (Section 4); a viscous wall is the subject of another pair of pages, the two-dimensional scalar advection–diffusion equation. The unknowns live on the mesh nodes, and the control volume is the median dual carved out around each node. After introducing the equation and what it is for, this page draws, step by step, how that dual control volume is built, and then rewrites the same edge loop into matrix-free forward (computing Av) and matrix-free adjoint (computing ATw). Every loop can be played statement by statement.
阅读方式。本页有四个边值问题,各自单独计算,数值从不混用:
How to read this page. Four boundary-value problems appear on this page, each computed on its own; their numbers are never mixed:
新增部分用到的 Newton 法、GMRES、预处理的转置和计算磁带,在第一次出现的地方各有几句说明;GMRES 的逐步讲解见一维 Burgers 页面第 17 节。新增表格采用从 0 开始的代码索引:未知量 i 就是原网格图中的第 i+1 个节点;计算面按逐个单元走边时第一次遇到的顺序编号,与主线的面号不同(例如二阶时新增表中的面 3 是主线的 f17;节点页的面号还随精度改变,因为二阶把对偶面拆成段),所以表格同时给出主线面号 f#。下方的精度开关只作用于新增面板,原有播放器保持原样。
Newton's method, GMRES, the transposed preconditioner and the computational tape used by the added blocks are each explained in a few sentences where they first appear; GMRES is taught step by step in Section 17 of the 1-D Burgers page. Added tables use zero-based code indices: unknown i is node i+1 of the original mesh diagrams; computational faces are numbered in the order the cells first meet their edges, which differs from the main line's face numbers (for instance, at second order face 3 of the added tables is the main line’s f17; on the node page the numbering also changes with the order, since second order splits dual faces into segments), so the tables also give the main-line label f#. The order selector below controls only the added panels; the original players are unchanged.
本页的未知量是平面上的一个标量场 。它不是速度,本页也不给它指定具体的物理含义;它服从一条守恒律:在任意一块固定区域 里, 的总量只能通过边界流进流出,单位时间穿过边界的量由通量 的法向分量给出( 是边界的外法向):
The unknown of this page is a scalar field in the plane. It is not a velocity, and the page gives it no particular physical meaning; it obeys a conservation law: in any fixed region the total amount of changes only by what flows across the boundary, and the amount crossing the boundary per unit time is the normal component of the flux ( the outward normal of the boundary):
由散度定理,边界积分等于 在 上的积分;区域可以任意取,所以被积函数本身必须为零,这就是守恒律的微分形式。
By the divergence theorem the boundary integral is the integral of over ; the region is arbitrary, so the integrand itself must vanish, and that is the differential form of the conservation law.
本页要离散的是二维标量守恒律:
The page discretises a two-dimensional scalar conservation law:
通量 F 对 u 是非线性的(∂F/∂u=uβ),所以离散之后得到的 R(u) 同样非线性——这正是后面要迭代求解、并在收敛状态 u* 处线性化的原因。
The flux F is nonlinear in u (with ∂F/∂u=uβ), so the discrete R(u) is nonlinear too — which is exactly why it is solved below by iteration and linearised at the converged state u*.
本页求的是定常解 ,要离散的就是 。时间导数只在第 3 节用来写出有限体积形式,第 6 节直接解离散方程 。 是常矢量,由乘积法则 ,对光滑解方程也可以写成
This page computes the steady solution, , so the equation to discretise is . The time derivative serves only to write the finite-volume form in Section 3; Section 6 solves the discrete equations directly. Since is a constant vector, the product rule gives , and for a smooth solution the equation can also be written
它说 以速度 被输运:方向处处是 ,快慢由 自己决定。沿任一条平行于 的直线取弧长 ,,方程就成了一维 Burgers 方程 ,不同的直线之间互不影响。所以它是一维 Burgers 页面方程(去掉源项)在二维的推广:非线性对流不变,只多了一个空间方向。定常时 ,凡 的地方, 沿每条这样的直线保持不变。
It says that is transported with the velocity : the direction is everywhere, the speed is set by itself. Along any straight line parallel to , with arc length , , and the equation becomes the one-dimensional Burgers equation , with no interaction between different lines. So it is the equation of the 1-D Burgers page (without its source) carried into two dimensions: the same nonlinear convection, with one more space direction. In a steady state : wherever , is constant along each such line.
图 1.1 把本页的问题画在一张图上。(a) 是区域和边界:物体与远场圆之间的环形区域,远场分成入流和出流两段,物面是琥珀色的八边形,紫色圆点是 24 个节点。(b) 是本页算出的基准解 (第 2 节起讲怎样算):它在 0.786 到 1.536 之间,离来流值 1 最远的都在贴着物面的一层。下面两小节解释这张图:哪里给边界条件,以及本页的问题由哪些数据组成。
Figure 1.1 draws this page’s problem in one picture. (a) is the domain and its boundaries: the annulus between the body and the far-field circle, the far field split into an inflow and an outflow part, the body an amber octagon, and the 24 nodes as violet dots. (b) is the baseline solution that the page computes (Section 2 onwards shows how): it lies between 0.786 and 1.536, and the values furthest from the free stream 1 all sit in the layer against the body. The next two subsections explain the picture: where the boundary takes data, and which data make up the problem.
叫做特征速度: 的一个小扰动以这个速度移动,方向是 ,大小是 。在边界上要看它的法向分量:记 为指向区域外的法向,信息穿过边界的速度是 。
is called the characteristic velocity: a small disturbance of moves with it, in the direction and at the speed . At a boundary its normal component is what counts: with the normal pointing out of the domain, information crosses the boundary at the speed .
本页的解处处为正,所以只需看 的符号,判据是纯几何的。远场是半径 1.9 的圆:上游一侧 ,是入流,给定来流值 ;下游一侧是出流,不给条件。本页的 16 个远场半面中 8 个入流、8 个出流。第 4 节把这两种情形写成边界半面上的右状态。
The solution of this page is positive everywhere, so only the sign of matters and the test is purely geometric. The far field is a circle of radius 1.9: on the upstream side , an inflow boundary with the free-stream value given; on the downstream side an outflow boundary with no condition. Of this page’s 16 far-field half-faces, 8 are inflow and 8 outflow. Section 4 turns the two cases into the right state of a boundary half-face.
物面(内圈)不同。 是常矢量,物体是闭曲线,所以绕物体一圈 必然正负都有(这里 指出区域,也就是指向物体内部):迎风一侧 ,从远场来的特征线在这里离开区域;背风一侧 ,特征线从物面进入区域。本页 16 个壁面半面中 10 个在迎风一侧、6 个在背风一侧。“不可穿透”的物理条件 会在 的地方都要求 ,没有正值解能满足它。本页对内圈采用离散闭合:每个壁面半面上令数值通量 。它让边循环最简单,但不是这个方程在物面上的物理条件;第 4 节说明它在迎风一侧多给了一个条件、没有连续极限。各节末尾“新增(选读)”的模型把内圈也按特征方向处理。
The body (the inner ring) is different. is a constant vector and the body a closed curve, so takes both signs around it (here points out of the domain, that is, into the body): on the windward side and the characteristics arriving from the far field leave the domain there; on the leeward side and characteristics enter the domain from the wall. Of this page’s 16 wall half-faces, 10 are on the windward side and 6 on the leeward side. A physical no-penetration condition would require wherever , which no positive solution satisfies. This page closes the inner ring with a discrete closure instead: the numerical flux is set to on every wall half-face. That keeps the edge loop as simple as it can be, but it is not the physical boundary condition of this equation at the body; Section 4 explains that it over-determines the windward side and has no continuum limit. The optional “Added” model at the end of the sections treats the inner ring by characteristics as well.
图 1.2 把这两件事画出来。(a) 是基准解上的特征速度 :箭头全都平行,长短随 变,这就是非线性。(b) 是每个边界半面上的 :负的是入流,正的是出流;远场的点紧贴着光滑远场圆的曲线,物面的点在迎风与背风之间变号。
Figure 1.2 draws both. (a) is the characteristic velocity of the baseline solution: the arrows are all parallel and their lengths change with , which is the nonlinearity. (b) is on every boundary half-face: negative for inflow, positive for outflow; the far-field points hug the curve of the smooth far-field circle, and the wall points change sign between the windward and the leeward side.
在流体力学里,它是二维非线性对流的一个简单模型。真实的无黏流动(Euler 方程)被流动带着走的是质量、动量和能量,输运速度就是流速本身,而且是一组方程;这里只有一个标量,输运方向固定为 ,只保留“输运快慢取决于被输运的量”这一个非线性。沿 看它就是 Burgers 方程:非定常时快的部分会追上慢的部分,波形变陡,可以形成激波;本页只求定常解,定常时特征线是互相平行的直线,不会相交。这类标量模型常用来在处理方程组之前,先检验数值通量、边界处理和伴随程序。
In fluid dynamics it is a simple model of nonlinear convection in two dimensions. In a real inviscid flow (the Euler equations) the flow carries mass, momentum and energy, the transport velocity is the flow velocity itself, and the equations form a system; here a single scalar is carried in the fixed direction , and only one nonlinearity is kept: the speed of transport depends on the quantity transported. Along it is the Burgers equation: in unsteady flow faster parts overtake slower ones, waves steepen and shocks can form; this page computes steady solutions only, whose characteristics are parallel straight lines that never cross. Scalar models of this kind are commonly used to test numerical fluxes, boundary treatments and adjoint codes before moving on to systems.
为什么用标量方程,而不是 Euler / N–S。本页要讲的是离散伴随的结构:边循环怎么写、怎么对它逐项求导、怎么把它转置。真实的 Euler 方程在每个对偶面上的通量是 4 维(二维)或 5 维(三维)的向量,它的局部导数是 4×4 或 5×5 的块,再加上 Roe 的特征分解、限制器、N–S 的粘性项与湍流模型,∂h/∂uL 就从一个数变成一个矩阵——这些细节会把“收集—计算—分发”这条主线淹没。换成标量守恒律之后,两个局部导数 ∂h/∂uL、∂h/∂uR 各自只是一个数,整条链路才能逐句播放、逐项和差分对照。但骨架完全一样:边循环的写法、边界条件只在一维通量内部起作用、前向与伴随的转置关系,在真实求解器里逐字成立——把标量换成状态向量、把局部导数换成块矩阵即可。
Why a scalar equation rather than Euler / N–S. What this page is about is the structure of the discrete adjoint: how the edge loop is written, how it is differentiated term by term, how it is transposed. In the real Euler equations the flux on each dual face is a 4-vector (two-dimensional) or 5-vector (three-dimensional) whose local derivatives are 4×4 or 5×5 blocks, and with Roe’s characteristic decomposition, limiters, the N–S viscous terms and a turbulence model on top, ∂h/∂uL turns from a number into a matrix — details that would drown the "gather — compute — scatter" thread. With a scalar conservation law the two local derivatives ∂h/∂uL and ∂h/∂uR are each just one number, so the whole chain can be played statement by statement and checked term by term against differences. But the skeleton is identical: how the edge loop is written, the fact that boundary conditions act inside the one-dimensional flux, and the transpose relation between forward and adjoint all hold verbatim in a real solver — replace the scalar by a state vector and the local derivatives by block matrices.
在本课程里,它是第一个二维页面,也是从一维 Burgers 走向二维非结构网格的一步。一维时面就是点,法向只有 ,网格等距;这里每条网格边对应一个对偶面(从边中点连到相邻单元中心的折线),法向 由节点坐标算出,网格是四边形和三角形混合的非结构网格,边界是闭曲线,哪里入流、哪里出流由几何决定。法向依赖节点坐标,所以坐标本身成了设计变量,梯度里出现几何导数 与 (第 12 节)。方程仍是标量,面上的局部导数仍是一个数,每一步都可以逐句播放、逐项核对。
In this course it is the first two-dimensional page, the step from 1-D Burgers to unstructured meshes in two dimensions. In one dimension a face is a point, its normal is and the mesh is uniform; here each mesh edge carries a dual face (the polyline from the edge midpoint to the centres of the neighbouring cells) with a normal computed from the node coordinates, the mesh is an unstructured mix of quadrilaterals and triangles, and the boundaries are closed curves whose inflow and outflow parts are decided by the geometry. The normals depend on the node coordinates, so the coordinates themselves become design variables and the gradient gains the geometric derivatives and (Section 12). The equation is still scalar, the local derivatives on a face are still single numbers, and every step can be played statement by statement and checked term by term.
图 1.3 把本页放在整个网站的路线上,每个方框都链接到那一页的第 1 节。上排是标量方程:从一维 Burgers 到本页,多了一个空间方向;再到二维对流扩散,加上扩散项,物面就可以、也必须给一个 Dirichlet 条件。下排是方程组:拟一维 Euler、二维 Euler、二维层流 N–S、二维 RANS–SA。同一列的两页维数相同、物理相近:本页对应二维 Euler,对流扩散对应层流 N–S。每点的未知量从标量的 1 个增加到方程组的 3–5 个,但每一页的残差都由面循环组装,前向与伴随沿同一个循环求导。另有一维层流 N–S 页,在一维 Euler 方程上加入黏性和热传导。
Figure 1.3 places this page on the route through the site; every box links to Section 1 of its page. The upper row holds the scalar equations: from 1-D Burgers to this page one space direction is added; on to 2-D advection–diffusion a diffusion term is added, which lets, and requires, the body take a Dirichlet condition. The lower row holds the systems: quasi-1-D Euler, 2-D Euler, 2-D laminar N–S and 2-D RANS–SA. The two pages in one column share the dimension and the kind of physics: this page matches 2-D Euler, advection–diffusion matches laminar N–S. The unknowns per point grow from 1 for a scalar to 3–5 for a system, yet every page assembles its residual with a face loop, and the tangent and the adjoint differentiate that loop. The 1-D laminar N–S page, besides, adds viscosity and heat conduction to the one-dimensional Euler equations.
本页只回答一个问题:设计变量 改变一点,目标 改变多少,也就是梯度 的全部 49 个分量。页首的全页路线图把后面各节串成一条线:前向(切线)每个设计分量要解一次线性方程,49 个分量就要解 49 次;伴随只解一次转置方程 ,再做一次乘积就得到全部分量(第 11、12 节)。
This page answers one question: how much does the objective change when the design variables change a little, that is, what are all 49 components of the gradient ? The page roadmap at the top strings the later sections together: the tangent (forward) route solves one linear system per design component, 49 solves for 49 components; the adjoint route solves one transposed system and gets every component with one more product (Sections 11 and 12).
要在计算机上回答这个问题,先得把方程写到网格上,因为计算机只能存有限个数。第 2 节先搭网格:节点、围绕节点的对偶控制体、对偶面和它们的法向;第 3 节再把方程在每个控制体上积分,化成每个对偶面上的一个数,也就是数值通量。
To answer it on a computer the equation must first be written on a mesh, since a computer stores only finitely many numbers. Section 2 builds the mesh (the nodes, the dual control volumes around them, the dual faces and their normals); Section 3 then integrates the equation over each control volume and reduces it to one number per dual face: the numerical flux.
第 1 节的方程对区域里的每一点都成立,计算机却只能存有限个数。本节先搭网格,再围绕每个节点造出对偶控制体:每个节点存一个未知量,相邻两个控制体的公共边界就是对偶面;最后看对偶控制体的外法向为什么首尾相接后闭合。
The equation of Section 1 holds at every point of the domain, but a computer stores only finitely many numbers. This section builds the mesh and then carves a dual control volume around every node: each node stores one unknown, and the common boundary of two neighbouring control volumes is a dual face. Last, it shows why the outward normals of a dual control volume close when placed head to tail.
算例是一张绕物体的环形 O 型网格,3 圈节点、每圈 8 个,共 24 个节点。贴着物面的一层用四边形(8 个),外面一层用三角形(16 个)——这正是实际 CFD 常用的混合网格:近壁面用规则的四边形层,外场用三角形。物体本身是非对称的:它有弯度,而且相对远场圆心有偏置,所以离散解、类似阻力的目标函数(第 7 节)和形状梯度都不带任何对称性。两圈边界的意义完全不同:
The test case is an annular O-type mesh around a body: 3 rings of nodes, 8 to a ring, 24 nodes in all. The layer against the body uses quadrilaterals (8 of them) and the layer outside it triangles (16) — precisely the hybrid mesh that real CFD uses: regular quadrilateral layers near the wall, triangles in the far field. The body itself is asymmetric: it has camber and is offset from the centre of the far-field circle, so the discrete solution, the drag-like objective (Section 7) and the shape gradient carry no symmetry whatever. The two rings of boundary mean quite different things:
有限体积法需要一组不重叠、铺满全域的控制体。把未知量放在单元里,控制体就是单元本身(格心格式);也可以把未知量放在节点上,这时控制体必须另外造出来,做法是围绕每个节点“切”出一块,这就是中位对偶控制体(图 2.1)。本页采用后者,所以 N=24。
A finite-volume method needs a set of control volumes that do not overlap and cover the whole domain. Put the unknowns in the cells and the control volume is the cell itself (the cell-centred scheme); put them on the nodes instead and the control volumes have to be constructed, by "cutting" a piece out around every node — the median-dual control volume (Figure 2.1). This page takes the latter, so N=24.
构造规则只有一条,而且对三角形和四边形完全一样:在每个单元内,把每条边的中点与单元中心相连。这些线段把单元分成若干块,分别归属它的各个顶点;把所有相邻单元里属于同一个节点的小块拼起来,就是这个节点的控制体。图 2.2 可以逐步播放这个过程,也可以点击任意节点换一个看(试试壁面节点、远场节点和内部节点的区别)。
There is only one construction rule, and it is exactly the same for triangles and quadrilaterals: inside each cell, join the midpoint of every edge to the cell centre. Those segments cut the cell into pieces, each belonging to one of its vertices; collect the pieces belonging to the same node across all its neighbouring cells and you have that node’s control volume. Figure 2.2 plays the process step by step, and any node can be clicked to see another one (try the difference between a wall node, a far-field node and an interior node).
网格的每一条边 (i,j) 对应一个对偶面:它由相邻单元各贡献的一段拼成——内部边有两个相邻单元,所以对偶面是折线 xc1→m→xc2;边界边只有一个单元,只贡献一段。面的法向由各段旋转 90° 后相加得到(图 2.3):
Every edge (i,j) of the mesh corresponds to one dual face, assembled from one segment contributed by each adjacent cell — an interior edge has two adjacent cells, so the dual face is the polyline xc1→m→xc2; a boundary edge has only one cell and contributes a single segment. The face normal is obtained by rotating each segment through 90° and adding them (Figure 2.3):
符号取成让 nij 由节点 i 指向节点 j。这样一条边只需要存一个法向向量。|nij| 是这条折线的等效直线长度——等于折线两端之间的距离(内部边就是两个单元中心;边界边的对偶面只有一段,两端是边中点与那一个单元中心),而不是折线自身的长度(算例中折线最多长 13.4%)。通量只用到矢量 nij,所以边循环和格心格式的面循环形式完全一样。
The sign is chosen so that nij points from node i to node j. An edge then needs to store just one normal vector. |nij| is the equivalent straight-face length of that polyline — the distance between its two ends (for an interior edge, the two cell centres; the dual face of a boundary edge has a single segment, running from the edge midpoint to its one cell centre), not the polyline's own length, which runs up to 13.4% longer in this mesh. The flux only ever uses the vector nij, which is why the edge loop has exactly the same form as the face loop of a cell-centred scheme.
四边形的一个重要差别。中点—中心连线把三角形恰好分成面积相等的三块,所以三角形网格上 Vi=13Σ(相邻三角形面积)。但对一般四边形,四块面积并不相等(只有平行四边形才是四分之一)。所以控制体体积必须把各个子块的面积逐个加起来,不能套用固定分数——图 2.3(b) 给出了算例中真实四边形的四块占比。
One important difference for quadrilaterals. The midpoint-to-centre lines cut a triangle into three pieces of exactly equal area, so on a triangular mesh Vi=13Σ(area of the adjacent triangles). For a general quadrilateral, though, the four pieces are not equal (only a parallelogram gives quarters). So the control-volume area has to be accumulated piece by piece and cannot be taken as a fixed fraction — Figure 2.3(b) gives the four shares for a real quadrilateral from the test case.
Vi 在哪里用到。要说清楚:第 5–14 节的定常问题里,R、A、diag(A)、L 与 dR/dX 都用不到 Vi。残差只是对偶面通量之和,几何只通过对偶面的法向进入(第 12 节);Vi 乘的是时间导数 Vidui/dt(第 3 节)。本页只有第 15 节用到它:构造解的源项按 s(xi)Vi 进入每个控制体,误差范数也按 Vi 加权。一般来说,非定常问题、伪时间推进的局部时间步和体积分源项都要用到 Vi;上面逐块累加的做法就是为这些场合准备的,在本页只服务于第 15 节。
Where Vi is used. To be clear: in the steady problem of Sections 5–14 none of R, A, diag(A), L or dR/dX uses Vi. The residual is a sum of dual-face fluxes, and the geometry enters only through the dual-face normals (Section 12); Vi multiplies the time derivative, Vidui/dt (Section 3). On this page only Section 15 uses it: the manufactured source enters each control volume as s(xi)Vi, and the error norm is weighted by Vi. In general, unsteady problems, the local time step of pseudo-time marching and volume source terms all need Vi; the piece-by-piece accumulation above is there for those, and on this page it serves Section 15 alone.
内部节点的控制体被相邻单元内的线段完整围住,边界节点却不是:它的控制体在区域边界上是“敞开”的,必须用节点到相邻边界边中点的两条半边补上(图 2.4)。这两条半边就是边界半面,外法向取该边界边外法向的一半:
The control volume of an interior node is enclosed completely by the segments inside its neighbouring cells; a boundary node’s is not — it is left "open" along the domain boundary and has to be closed with the two half-edges running from the node to the midpoints of its adjacent boundary edges (Figure 2.4). Those two half-edges are the boundary half-faces, and their outward normal is half the outward normal of the boundary edge:
边界边 (a,b) 的两个端点各得一份。算例中内外圈各有 8 条边界边,共 16 条,所以有 32 个边界半面(16 个壁面 + 16 个远场);加上 48 条边对应的 48 个内部对偶面,边循环一共要走 80 个面。
Each of the two endpoints of a boundary edge (a,b) receives one share. The test case has 8 boundary edges on the inner ring and 8 on the outer, 16 in all, giving 32 boundary half-faces (16 wall + 16 far-field); together with the 48 interior dual faces of the 48 edges, the edge loop walks 80 faces in total.
图 2.5 可以点击任意节点或对偶面,查看它的两侧节点、法向与类型。
Figure 2.5 lets you click any node or dual face and inspect the nodes on either side of it, its normal and its type.
闭合性。每个控制体是一个封闭多边形,所以它全部外法向之和为零。选中任一节点,把它“作为 i”的面取 +n、“作为 j”的面取 −n(边界半面取 +n)相加,结果都是 (0,0)——上面图 2.2 的信息栏里可以直接读到(“Σ 外法向”一行)。这一点对四边形和三角形同样成立。
Closure. Every control volume is a closed polygon, so all its outward normals sum to zero. Select any node, add +n for the faces where it acts as i and −n for those where it acts as j (boundary half-faces take +n), and the result is always (0,0) — the construction demo above (Figure 2.2) reads it out directly, on its "Σ outward normals" row. This holds for quadrilaterals and triangles alike.
上面“闭合”一段说,每个控制体所有面的外法向加起来为零。下面把它推一遍,并看它换来的结果:均匀来流满足除壁面节点以外的每一行残差(图 2.6)。
The “Closure” paragraph above says that the outward normals of every control volume sum to zero. Here is the derivation, and what it buys: a uniform stream satisfies every residual row except those of the wall nodes (Figure 2.6).
第一步,本页的法向都是某个线段转 90° 得到的。记
Step 1. Every normal on this page is some segment turned by 90°. With
对偶面 的法向是它的一两段折线(边中点 到相邻单元中心 )各自转 90° 之和, 让它从 指向 ;边界半面的法向是半条边界边转 90°。
the dual-face normal of edge is the sum of its one or two segments (from the edge midpoint to the neighbouring cell centres ), each turned by 90°, with making it point from to ; a boundary half-face normal is half a boundary edge turned by 90°.
第二步,节点 的对偶控制体的边界是一条闭合折线:它由这些折线段和(对边界节点)两段半边组成。逆时针走一圈时,( 若 , 若 )正好是 作用在走过的那几段上:“从 指向 ”就是“指向对偶控制体外面”。
Step 2. The boundary of the dual cell of node is a closed polyline made of these segments and, for a boundary node, the two half-edges. Walking it counter-clockwise, ( if , if ) is exactly applied to the segments walked: “from to ” means “out of the dual cell”.
第三步,一条闭合折线的线段向量相加,中间的点两两抵消:
Step 3. The segment vectors of a closed polyline telescope:
是对偶多边形的顶点(节点本身、边中点、单元中心),最后一个点回到第一个点。
where are the vertices of the dual polygon (the node itself, edge midpoints, cell centres) and the last one returns to the first.
第四步,看它换来了什么。取均匀状态 。凡是跑通量的对偶面两侧都是 :内部对偶面两侧相同,入流半面 ,出流半面 。于是耗散项 为零,通量只剩中心项,对没有壁面的节点用第三步:
Step 4, what it buys. Take the uniform state . Every dual face that runs the flux sees on both sides: interior dual faces trivially, inflow half-faces because , outflow half-faces because . The dissipation vanishes and only the central term is left; for a node with no wall half-face, Step 3 gives
第五步,壁面节点少了壁面那两项:壁面上 ,而不是 ( 是壁面半面)。把缺的项从第三步的零里减掉:
Step 5. A wall node misses the wall terms: on the wall instead of ( the wall half-face). Subtracting the missing terms from the zero of Step 3:
这里 、 是节点 的两个壁面半面。
Here and are the two wall half-faces of node .
所以均匀来流只在 8 个壁面节点上不满足离散方程, 离开 全是由这几行引起的。这也是第 6 节的 Jacobi 迭代从 出发时看到的第一个残差:它只在壁面节点上非零。
So the uniform stream fails the discrete equations only in the 8 wall nodes, and every departure of from is caused by these rows. It is also the first residual the Jacobi iteration of Section 6 meets when it starts from : non-zero only at the wall nodes.
核对:全部 24 个对偶控制体中 最大为 2.5×10−16;在 处,16 个非壁面节点的 最大为 1.7×10−16,8 个壁面节点与第五步公式之差最大为 5.6×10−17(本页代码在构建时计算)。
Check: the largest over all 24 dual cells is 2.5×10−16; at the largest in the 16 nodes away from the wall is 1.7×10−16, and the 8 wall nodes differ from the formula of Step 5 by at most 5.6×10−17 (computed by this page’s code at build time).
网格、对偶控制体和对偶面已经齐了。第 3 节把第 1 节的方程在每个对偶控制体上积分:积分之后只剩穿过各个对偶面的通量,空间离散于是归结为一个问题——一个对偶面上的通量怎么算。
The mesh, the dual control volumes and the dual faces are in place. Section 3 integrates the equation of Section 1 over each dual control volume: after integration only the fluxes through the dual faces remain, so the spatial discretisation comes down to one question, how to compute the flux on one dual face.
仍使用原来的 24 个节点、8 个四边形和 16 个三角形,内外边界的坐标与连接关系不变。改变的是边界的数学含义和空间重构,不是重建一个新的几何算例。新增面板里的数组从 0 开始编号:未知量 i 就是原网格图中的第 i+1 个节点。计算面按逐个单元走边时第一次遇到的顺序编号,与主线的面号不同(主线把内部面、壁面面和远场面分组排序),所以新增表格在面号后的括号里同时给出主线面号 f#。
The original 24 nodes, eight quadrilaterals and sixteen triangles remain unchanged, including boundary coordinates and connectivity. We change the boundary interpretation and spatial reconstruction, not the geometry. Arrays in the added panels are numbered from 0: unknown i is node i+1 of the original mesh diagrams. Computational faces are numbered in the order the cells first meet their edges, which differs from the main line (it sorts interior, wall and far-field faces into groups), so the added tables give the main-line label f# in brackets after each face number.
未知量放在原始节点。每个对偶控制体由节点、边中点和原始单元中心连接而成;内部对偶面可能由两段折线组成。一阶沿用合成法向的旧通量;二阶在各段上分别重构和积分,不能把不同位置的面值用一个中点代替。
Unknowns live at primal nodes. Dual volumes join nodes, edge midpoints and primal-cell centres. An interior dual face may have two segments. First order retains the original summed-normal flux; second order reconstructs and integrates each segment separately. Distinct trace locations cannot be represented by a single midpoint.
图中保留原始网格,格点页叠加对偶线段。选择一个未知量和显示变量(状态 u、误差 u − u_ref 或伴随 ψ);表格列出该未知量的位置、体积 V、状态、参考值与最小二乘梯度。计算面及其积分点在图 3.3 中查看。
The diagram retains the primal grid and overlays dual segments on the node page. Select an unknown and a field (the state u, its error u − u_ref or the adjoint ψ); the table lists that unknown’s position, volume V, state, reference value and least-squares gradient. Faces and their quadrature points are shown in Figure 3.3.
第 2 节搭好了对偶控制体和对偶面。本节把第 1 节的方程在每个对偶控制体上积分,把体积分换成穿过各个对偶面的通量之和,再给出一个对偶面上的数值通量:它只看面两侧的两个状态。
Section 2 built the dual control volumes and the dual faces. This section integrates the equation of Section 1 over each dual control volume, turns the volume integral into a sum of fluxes through the dual faces, and then gives the numerical flux on one dual face, which looks only at the two states on either side of it.
在节点 i 的对偶控制体上积分并用高斯公式,体积分变成对偶控制体边界上的通量之和:
Integrating over the dual control volume of node i and applying the divergence theorem turns the volume integral into a sum of fluxes over the boundary of that dual control volume:
到这里,空间离散剩下的唯一问题就是:一个对偶面上的 He 怎么算。
At this point the only question left in the spatial discretisation is how to evaluate He on a single dual face.
为了方便说明,本页采用一阶空间离散。面上的左右状态直接取两侧节点当前的值,不做任何重构——也就是把解看成分片常数,因此空间精度是一阶的。真实求解器会在这一步加上二阶重构(MUSCL 或最小二乘梯度)再配一个限制器,那会带来两件事:一是模板从“两侧节点”扩大到邻居的邻居,面通量的局部导数不再只涉及两个节点;二是限制器里有 min、max 这类切换,在切换点上不可微;切换点之外它一般可微,而且导数不为零。所以限制器不能照搬第 4 节远场入流/出流开关的做法:那个开关 sign(vn) 在冻结状态附近是常数,导数本来就是零,冻结它不丢掉任何东西,因而是精确的;把限制器在 u* 处冻结,丢掉的却是一个非零的导数,得到的就不再是真实 Jacobian 的转置(一维 Burgers 页面附录 D.2 说明了限制器的导数为什么不能丢)。但边循环、前向、伴随的结构一点不变:仍然是收集 → 算一维通量 → 分发,仍然靠转置得到伴随。一阶的代价只体现在精度上,不体现在本页要讲的那套代数上。
For ease of explanation this page uses a first-order spatial discretisation. The left and right states on a face are taken straight from the current values at the two nodes, with no reconstruction at all — the solution is treated as piecewise constant, so the spatial accuracy is first order. A real solver would add second-order reconstruction here (MUSCL or a least-squares gradient) together with a limiter, and that brings two things: the stencil grows from "the two nodes" to the neighbours’ neighbours, so the local derivatives of a face flux involve more than two nodes; and a limiter contains switches such as min and max, so it is not differentiable at a switch, while away from the switches it usually is, with a non-zero derivative. So a limiter cannot be treated like the far-field inflow/outflow switch of Section 4: that switch, sign(vn), is constant near the frozen state, its derivative is zero anyway, and freezing it drops nothing, so it is exact; freezing a limiter at u* drops a non-zero derivative, and what results is no longer the transpose of the true Jacobian (Appendix D.2 of the 1-D Burgers page explains why a limiter's derivative must be kept). But the structure of the edge loop, of the forward mode and of the adjoint does not change at all: still gather → evaluate the one-dimensional flux → scatter, still transpose to get the adjoint. First order costs accuracy only, and none of the algebra this page is about.
通过一个面的通量只和法向分量 F·n 有关。沿法向看,面两侧就是一维问题:左边常数状态 uL,右边常数状态 uR(图 3.1)。数值通量取左右平均 − 常数耗散系数 ×(右状态 − 左状态):
The flux through a face depends only on the normal component F·n. Along the normal, the two sides of the face form a one-dimensional problem: a constant state uL on the left and a constant state uR on the right (Figure 3.1). The numerical flux is taken as the average of the two sides − a constant dissipation coefficient × (right state − left state):
记 vn=β·n,由 F(u)·n=12u2vn,中心项就是 14vn(uL2+uR2)。在图 3.2 里拖动滑块,观察两项如何组成通量。以下把对偶面上的这个数记作 h=H(uL,uR)。
Writing vn=β·n and using F(u)·n=12u2vn, the central term is 14vn(uL2+uR2). Drag the sliders in Figure 3.2 to watch how the two terms make up the flux. From here on this number on a dual face is written h=H(uL,uR).
ε 取多大:单调性条件。第 6 节会写出这个通量对两侧状态的两个局部导数:cL=12vnuL+ε|n|,cR=12vnuR−ε|n|。单调的通量要求 cL≥0、cR≤0:左状态增大时通量不减,右状态增大时通量不增。不论 vn 的符号如何,只要 ε≥12|u||β·n̂|(n̂=n/|n|)两式就都成立;要对任意方向的面都成立,就是 |u|≤2ε/|β|=1.43。常数 ε=0.8 只在这个范围里保证单调。本页的 u* 在节点 6 上是 1.536,已经越过这个界:对偶面 (6,14) 上 cL=−0.018,是本算例唯一一个符号不对的局部导数(格心页面的 u* 最大是 1.299,没有越界)。第 15 节会看到,网格加密以后迎风壁面上的 u 一层比一层大,越界的面也越来越多。
How large ε must be: the monotonicity condition. Section 6 writes out the two local derivatives of this flux with respect to the two states: cL=12vnuL+ε|n| and cR=12vnuR−ε|n|. A monotone flux needs cL≥0 and cR≤0: the flux must not decrease when the left state grows, nor increase when the right state grows. Whatever the sign of vn, both hold when ε≥12|u||β·n̂| (with n̂=n/|n|); for faces of every orientation that is |u|≤2ε/|β|=1.43. The constant ε=0.8 guarantees monotonicity only in that range. This page's u* is 1.536 at node 6, beyond the bound: on the dual face (6,14), cL=−0.018, the one local derivative of the wrong sign in this test case (the cell-centred page's u* reaches 1.299 at most and stays inside). Section 15 shows the windward wall values growing with every refinement, and more and more faces crossing the bound.
到这里,每个内部对偶面都有了两侧节点的状态,数值通量可以直接算。边界半面只有一侧的节点(R=0),缺的右状态要由第 1 节的边界数据补上:远场入流半面取 u∞,出流半面取节点自己的值,壁面半面则直接令 h=0。第 4 节把这三种情形写进同一个一维通量。
Every interior dual face now has the states of its two nodes, and its numerical flux can be evaluated. A boundary half-face has a node on one side only (R=0); its missing right state comes from the boundary data of Section 1: the free stream u∞ on far-field inflow half-faces and the node’s own value on outflow half-faces, while a wall half-face gets h=0. Section 4 writes the three cases into the same one-dimensional flux.
方程和内部数值通量形式保持不变:β=(1,0.5)、ε=0.8。u 是正的标量,不是二维速度。非线性来自 u²;特征方向与 β 一致,特征速度大小随 u 改变。此处只研究光滑正解,不加入激波或限制器。
Keep the equation and interior flux form: β=(1,0.5), ε=0.8. The positive scalar u is not a two-dimensional velocity. Nonlinearity comes from u²: characteristics follow β, with speed depending on u. This example studies smooth positive solutions only, without shocks or limiters.
一阶把梯度设为零,直接使用控制体值。二阶由相邻控制体做距离平方反比加权(w_ij=1/‖d_ij‖²)的最小二乘,求一个 2×2 系统;每条直线段采用两点 Gauss 积分。若 u 是线性函数,梯度应精确恢复到舍入误差;这是重构实现的第一道检查。节点坐标改变时,d、w、M、b 以及重构位置全部参与求导。
First order sets the gradients to zero and uses the control-volume values directly. Second order uses inverse-distance-squared weighted least squares over adjacent control volumes, solving a 2×2 system, followed by two-point Gauss quadrature on each straight segment. Linear fields should reproduce their gradients to roundoff. Geometry derivatives include d, w, M, b and reconstruction locations.
for unknown i:
M = sum(w * outer(dx,dx))
b = sum(w * dx * (u[j]-u[i]))
grad[i] = solve2(M,b) if order==2 else [0,0]
for face segment:
for Gauss point xq:
uL = u[L] + dot(grad[L],xq-x[L])
uR = reconstruct_neighbor_or_apply_boundary(xq)
H += weight * numerical_flux(uL,uR,normal)为什么梯度来自一个小线性系统?把每个邻点的增量 u_j−u_i 近似为 d_ij·∇_hu_i,最小化加权平方误差。令对 ∇_hu_i 的导数为零,就得到上面的正规方程。二维至少需要两个不共线的方向;det M 接近零意味着邻域无法可靠确定某个方向的梯度。交互表列出实际 M、行列式和各邻点增量。
Why a small linear system? Approximate each neighbour jump u_j−u_i by d_ij·∇_hu_i and minimise the weighted squared mismatch. Setting its derivative with respect to ∇_hu_i to zero gives the normal equations above. Two dimensions require at least two non-collinear directions; a nearly zero determinant means that the stencil cannot reliably resolve one gradient component. The panel shows the actual matrix, determinant and neighbour jumps.
通量的第一项近似物理输运,第二项抑制相邻状态的非物理振荡。ε 固定为 0.8,不对谱半径取最大值,因此这里没有波速选择分支。二阶改的是两个面迹的近似及积分;不会把 ε 简单除以网格尺度。边界值和耗散同样逐积分点求值。
The first flux term approximates physical transport; the second damps nonphysical inter-state oscillation. The fixed ε=0.8 avoids a spectral-radius maximum branch. Second order changes only the two face traces and the quadrature; ε is not rescaled with the mesh size. Boundary data and dissipation are evaluated at each quadrature point.
内圈和外圈的处理方式完全不同:外圈按特征方向给数据,内圈用一个离散闭合。两者都只在上一节那个一维通量内部起作用:边界半面没有“右节点”,远场由边界条件给出右状态 uR,无粘壁面则直接令 h=0。把各个对偶面的通量拼成残差的边循环在第 5 节展开——它本身一行都不用改。
The inner and outer rings are treated quite differently: the outer ring takes data by the characteristic direction, the inner ring gets a discrete closure. Both act only inside the one-dimensional flux of the previous section: a boundary half-face has no "right node", so at the far field the boundary condition supplies the right state uR, while the inviscid wall sets h=0 outright. The edge loop that assembles the fluxes of all the dual faces into the residual is unfolded in Section 5 — it needs no change at all.
本页把内圈叫作无粘壁面,并在它的每个半面上直接令数值通量为零:
This page calls the inner ring the inviscid wall and sets the numerical flux on every one of its half-faces to zero:
这是离散问题的一个闭合,选它只为一件事:让边循环尽可能简单——壁面半面不给 R、A、AT 任何贡献。它不是这个方程在物面上的物理条件,这一点要先说清楚:它在物面的迎风一侧多给了一个条件,也没有连续极限。本方程的输运速度 uβ 会穿过物面:β 是常向量,沿闭合的物面 β·n 必然有正有负。零物理通量 12u2β·n=0 在 β·n≠0 的地方要求 u=0。在迎风一侧(β·n>0,n 指向物体),特征线从远场带着来流值 u∞=1 到达物面,并在那里离开计算域:再要求 u=0,问题就过度确定了。在背风一侧(β·n<0),特征线从物面进入计算域,u=0 是一个退化的入流值:特征速度 uβ·n 在那里为零。所以带这个壁面的连续问题没有光滑的正值解,离散解在加密时也不趋于任何极限:第 15 节实测,网格每加密一层,Lh 大约翻一倍(图 4.2)。
This is a closure of the discrete problem, chosen for one reason: it keeps the edge loop as simple as it can be — a wall half-face contributes nothing to R, A or AT. It is not the physical boundary condition of this equation at the body, and that is best said up front: it over-determines the windward side of the body and has no continuum limit. The transport velocity uβ of this equation crosses the body: β is a constant vector, so β·n takes both signs around the closed wall. A zero physical flux 12u2β·n=0 forces u=0 wherever β·n≠0. On the windward side (β·n>0, with n pointing into the body) the characteristics arrive from the far field carrying the free-stream value u∞=1 and leave the domain there: demanding u=0 as well over-determines the problem. On the leeward side (β·n<0) the characteristics enter the domain from the wall, and u=0 is a degenerate inflow value: the characteristic speed uβ·n vanishes there. So the continuous problem with this wall has no smooth positive solution, and the discrete solutions approach no limit under refinement either: Section 15 measures Lh roughly doubling with every refinement (Figure 4.2).
于是壁面半面不改变残差——它只负责把控制体封口(图 4.1)。这一点在第 5 节的演示里可以直接看到:播放到壁面半面时,h=0(图 5.2)。
So a wall half-face does not change the residual — its only job is to seal the control volume (Figure 4.1). This is directly visible in the demo of Section 5: when the play reaches a wall half-face, h=0 (Figure 5.2).
这并不影响本页的主线。第 5–14 节的残差、Jacobian、切线、伴随与梯度,都是这个固定离散问题在给定网格上的精确导数,与它有没有连续极限无关,而本页要讲的正是这个离散伴随。有连续极限的内边界见各节末尾的“新增”扩展(选读):那里内外边界都按特征方向处理。
None of this touches the main line of the page. The residual, the Jacobian, the tangent, the adjoint and the gradient of Sections 5–14 are exact derivatives of this fixed discrete problem on the given mesh, whether or not it has a continuum limit, and that discrete adjoint is what the page is about. An inner boundary with a continuum limit is the optional “Added” extension at the end of each section, where both boundaries are treated by characteristics.
图 4.2 左边沿 β 画出特征线:从远场入流边进入的特征线,一部分绕过物体、从远场出流边离开,另一部分撞上迎风一侧的壁面(实线箭头);背风一侧的壁面发出特征线(虚线)。右边的滑块取本页离散问题在逐层加密网格上的解:迎风一侧的壁面值随加密增大,第 0 层就超过第 3 节的单调性界 1.43,Lh 也随之大约翻倍;到第 2 层(288 个节点),从 u0≡1 出发的 Jacobi 迭代就不再收敛。
The left of Figure 4.2 draws the characteristics along β: of those entering through the far-field inflow faces, some pass the body and leave through the outflow faces, the others run into the windward side of the wall (solid arrows); the leeward side of the wall emits characteristics (dashed). The slider on the right steps through this page's discrete problem on the refined meshes: the windward wall values grow with refinement and exceed the monotonicity bound 1.43 of Section 3 already on level 0, and Lh roughly doubles with them; at level 2 (288 nodes) the Jacobi iteration from u0≡1 no longer converges.
远场要按特征方向决定用哪一侧的信息。标量守恒律 F(u)=12u2β 的波速是 uvn(vn=β·n,n 指向域外)。算例中 u>0,所以 sign(uvn)=sign(vn),判据是纯几何的:
At the far field the characteristic direction decides which side supplies the information. The wave speed of the scalar law F(u)=12u2β is uvn (with vn=β·n and n pointing out of the domain). In the test case u>0, so sign(uvn)=sign(vn) and the test is purely geometric:
算例的 8 条远场边里 4 条入流、4 条出流(图 4.3)。开关在冻结状态 u* 处固定(frozen switch),前向与伴随都在这个固定的分支上线性化。
Of the 8 far-field edges in the test case, 4 are inflow and 4 outflow (Figure 4.3). The switch is fixed at the frozen state u* (a frozen switch), and both the forward and the adjoint mode linearise on that fixed branch.
这是把边界条件收在右状态与一维通量这一层的好处:边循环的骨架——收集、算通量、分发——与边界种类无关(代码里这些分支都在 flux 一侧,下面第 5 节的清单把它们逐行摊开,只为能逐句播放)。
That is the benefit of keeping the boundary conditions at the level of the right state and the one-dimensional flux: the skeleton of the edge loop — gather, evaluate, scatter — does not depend on how many kinds of boundary there are (in the code these branches all sit on the flux side; the listing of Section 5 below spells them out line by line only so they can be played statement by statement).
为什么这里没有无滑移壁面。本页的方程是纯对流的(无粘):边界上只能在特征进入区域的地方给数据。物面是一条闭合曲线,β 是常数,所以 β·n 沿物面必然有正有负(本算例 u>0,特征速度 uβ·n 也是如此);在特征离开区域的那部分物面上再规定 u=uw,问题就多给了一个条件:没有扩散,那里没有任何机制能把壁面值带进流场。上面的零通量闭合 h=0 在迎风一侧恰好也多给了这样一个条件(它等于要求 u=0);本页保留它,只把它当作离散闭合。无滑移这类 Dirichlet 壁面条件属于带扩散项的方程——拉普拉斯算子是二阶的,它让 u=uw 在整条物面上都适定。所以壁面上的 Dirichlet 条件放在另一对页面里讲:二维标量对流扩散方程(格点)。
Why there is no no-slip wall here. The equation on this page is purely convective (inviscid): boundary data can be given only where characteristics enter the domain. The body is a closed curve and β is constant, so β·n must take both signs along it (with u>0 in this test case, so does the characteristic speed uβ·n); prescribing u=uw where characteristics leave the domain gives the problem one condition too many: without diffusion nothing can carry the wall value into the flow there. The zero-flux closure h=0 above gives exactly such an extra condition on the windward side (there it amounts to demanding u=0); the page keeps it only as a discrete closure. A Dirichlet wall such as no-slip belongs to an equation with diffusion — the Laplacian is second order, and it makes u=uw well posed along the whole wall. So the Dirichlet wall is the subject of another pair of pages: the two-dimensional scalar advection–diffusion equation (node-centred).
上面说开关在收敛解处冻结,前向与伴随都在这个分支上线性化。下面一步步说明:这对状态导数是精确的,对几何导数也在一个算得出来的范围内精确(图 4.4)。
Above, the switch is frozen at the converged state and both the tangent and the adjoint are linearised on that branch. Step by step: this is exact for the state derivatives, and exact for the geometric derivatives within a margin that can be computed (Figure 4.4).
第一步,远场半面的右状态由一个开关给出(core_node.js 的 local()):
Step 1. The right state of a far-field half-face comes from a switch (local() in core_node.js):
第二步,沿法向的物理通量是 ,它对 的导数 就是特征速度; 的地方信息从区域外面进来。本页远场上 (远场节点中 最小是 0.955),所以 :开关只看几何,不看 。
Step 2. Along the normal, the physical flux is ; its derivative with respect to , , is the characteristic speed, and information enters the domain where . On this page at the far field (the smallest in a far-field node is 0.955), so : the switch looks at the geometry, never at .
第三步,于是在固定网格上,每个面对所有 都停在同一个分支上,右状态是 的线性函数,导数就是分支上的常数:
Step 3. On a fixed mesh every face therefore stays on one branch for every , the right state is a linear function of , and its derivatives are the constants of that branch:
开关本身是 的阶梯函数,对 的导数处处为零,所以“冻结开关”没有丢掉任何一项:第 8、9 节的前向与伴随是这个离散残差的精确导数。
The switch itself is a step function of whose derivative with respect to is zero everywhere, so freezing it drops nothing: the tangent and the adjoint of Sections 8 and 9 are exact derivatives of this discrete residual.
第四步,几何导数要多想一步。代码只在建半面时判断一次开关(每个半面存下的 inflow 标志;setCoords 移动坐标时不重判),所以差分与复数步长求导的一直是这一个分支上的函数。它与“每次都重判”的函数只在某个远场半面的 变号时才不同。把一个节点移动一段有限的位移 ,相邻远场半面的法向变化 ,于是 。
Step 4. Geometric derivatives need one more thought. The code decides the switch once, when the half-faces are built (the inflow flag stored on each half-face; setCoords moves the coordinates without deciding again), so finite differences and the complex step always differentiate the function on that one branch. It differs from a function that re-decides only where some far-field changes sign. Moving one node by a finite displacement changes the normal of an adjacent far-field half-face by , so .
第五步,本页远场半面上 最小是 ,所以只要满足下式,就没有半面换分支:
Step 5. The smallest on this page’s far field is , so no half-face changes branch as long as
第 13 节的差分步长 ,连同它的步长扫描里最大的 ,都在这个范围之内;第 14 节的复数步长只动虚部,而开关看的是实部。
The difference step of Section 13, and even the largest step of its sweep, are inside this range; the complex step of Section 14 moves only the imaginary part, while the switch looks at the real part.
核对:16 个远场半面在基准几何上存下的开关与 的符号全部一致(入流 8 个、出流 8 个);;上面的界是 0.103(本页代码在构建时计算)。
Check: the switch stored on the 16 far-field half-faces agrees with the sign of at the base geometry (8 inflow, 8 outflow); ; the bound above is 0.103 (computed by this page’s code at build time).
新增模型不使用零通量分支;本节上方保留主线的模型供对照。保持固定 β 时,封闭边界不可能处处与 β 相切;零物理通量 u²β·n/2=0 会在 β·n≠0 的位置要求 u=0,与光滑正值解不相容(这正是上方主线所说的:零通量闭合在迎风一侧多给了一个条件)。新增模型的内边界允许标量通量通过,称为“内边界”,不再称为壁面。法向始终指向计算域外,内边界法向因此指向孔洞。
The added model does not use the zero-flux branch; the main-line model above is kept for comparison. A fixed β cannot be tangent everywhere to a closed contour, so a zero physical flux u²β·n/2=0 would require u=0 wherever β·n≠0, which no smooth positive solution satisfies (this is what the main line above says: the zero-flux closure over-determines the windward side). The inner boundary of the added model lets the scalar flux through; it is called the inner boundary, not a wall. Normals point out of the computational domain, hence into the hole at the inner boundary.
入流给定函数 u_in,出流由内部解决定;不在出流再指定一个 Dirichlet 值。内外入流都取同一个解析场,因而特征线穿过孔洞后重新进入计算域时不会引入不匹配的数据。二阶在每个 Gauss 点计算 u_in,不把它冻结为面中心常数。u_in 的平均水平就是主线的来流值 u∞,它是设计变量 D=(u∞, X) 的一个分量(新增面板按存储顺序给参数编号:先 48 个坐标,u∞ 在最后,编号 48);几何或入流方向的切线必须包含 (点表示前向导数,见主线第 8 节)。
Specify u_in on inflow and let the interior determine outflow; do not impose another Dirichlet value there. Both inner and outer inflow sample the same analytic field, avoiding incompatible data when a characteristic re-enters after crossing the hole. Second order evaluates u_in at each Gauss point rather than freezing it at the face centre. The mean level of u_in is the main line’s free-stream value u∞, a component of the design D = (u∞, X) (the added panels number the parameters in storage order: the 48 coordinates first, u∞ last, index 48); the tangent along a geometry or inflow direction includes (the dot is the forward derivative, Section 8 of the main line).
本页每个节点存储 1 个未知量。以下用 表示把所有位置按代码顺序排成的列向量(标量问题每个位置只有一个分量)。它是一个有限维向量,不是连续函数。物理方程中的空间导数只用于说明模型;求离散伴随时,求导对象是本页实际装配的有限维函数。
Each node stores 1 unknown. Below, stacks all locations in code order (a scalar problem has one component per location). It is a finite-dimensional vector, not a continuous function. Spatial derivatives in the physical equations explain the model; discrete differentiation acts on the actual finite-dimensional functions assembled on this page.
是本页全部设计变量排成的向量:来流值 与全部节点坐标 (新增的光滑模型另有自己的参数,见新增部分)。第 10、11 节把 写成具体的 ,第 12 节写成 ,例如 ;第 7 节的 就是这里的状态梯度。
stacks all design variables of this page: the free-stream value and all node coordinates (the added smooth model has its own parameters; see the added blocks). Sections 10 and 11 write as the concrete and Section 12 as , e.g. ; the of Section 7 is the state gradient here.
这里 、 都是列向量,它们的转置才是偏导数行。求导采用自动微分(AD)的记号:程序变量 上加点的 是前向(tangent)模式沿给定种子方向的导数,加横线的 是反向(adjoint)模式累加到 上的权重,例如 与 。两者都是对本页有限维代码求导;变分符号 只用于附录 A、B 的连续伴随里连续函数的变分。
Both and are columns; their transposes are the derivative rows. Derivatives use the notation of automatic differentiation (AD): a dot on a program variable is its forward (tangent) derivative along the chosen seed direction, and a bar is the weight accumulated into in reverse (adjoint) mode, for example and . Both differentiate the finite-dimensional code of this page; the variation symbol is kept for variations of continuous functions in the continuous adjoints of Appendices A and B.
残差计算对每条边做三件事:从两端节点收集状态,调用一维通量计算 h,再把 h 分发回两个节点——节点 i 加、节点 j 减。同一个 h 以相反符号进入两个控制体,这就是守恒。
Computing the residual does three things on every edge: gather the states from the two nodes, call the one-dimensional flux to compute h, and scatter h back to the two nodes — added at node i, subtracted at node j. The same h enters the two control volumes with opposite signs, and that is conservation.
反过来看一个节点:它的残差就是自己所有对偶面的通量之和,符号由它在每个面上是 i 还是 j 决定(图 5.1)。内部节点的面数等于它的相邻节点数,随网格而变——这是节点中心格式与格心格式在数据结构上的主要差别。
Seen from a node instead: its residual is the sum of the fluxes of all its own dual faces, each signed by whether the node is i or j on that face (Figure 5.1). An interior node has as many faces as it has neighbouring nodes, which varies across the mesh — the main data-structure difference between a node-centred and a cell-centred scheme.
图 5.2 逐句执行这个循环。网格上:紫色是当前对偶面,箭头从节点指向面(收集)、再从面指向节点(分发)。面板底部是两条数组:上面是输入的节点状态,下面是正在累加的残差。可以按 ▶ 连续播放,也可以用“下一步”或键盘左右键逐句走,或用右上角的下拉框直接跳到某个面(例如跳到一个壁面半面,看无粘壁面的 h=0)。
Figure 5.2 executes this loop statement by statement. On the mesh: violet is the current dual face, and the arrows run from the nodes to the face (gather) and back from the face to the nodes (scatter). At the bottom of the panel are two arrays: above, the input node states; below, the residual being accumulated. You can press ▶ to play continuously, step through statement by statement with "next" or the left and right arrow keys, or jump straight to a face with the dropdown at the top right (for example to a wall half-face, to see h=0 on an inviscid wall).
上面说同一个 以相反的符号进入两个节点,这就是守恒。把所有节点的残差加起来,就能看到它的整体形式。
Above, the same enters two nodes with opposite signs, and that is conservation. Adding the residuals of all nodes shows its global form.
第一步,面循环的分发写成一个带符号的关联矩阵 (第 8 节“局部导数怎样组成全局 Jacobian”一段用的也是它):
Step 1. Write the scatter of the face loop as a signed incidence matrix (the same as in the block “How local derivatives form the global Jacobian” of Section 8):
其余元素为零。第二步,交换求和次序:
All other entries are zero. Step 2, exchange the order of summation:
内部对偶面那一列里恰有一个 和一个 ,列和为零;边界半面没有右节点,列和为 。第三步,所以对任意 (不只是收敛解)
An interior dual face’s column holds exactly one and one and sums to zero; a boundary half-face has no right node, and its column sums to . Step 3: for every , not only the converged one,
壁面上 ,不出现。第四步,在收敛解处每一行都是零,于是远场通量之和也是零:从入流半面进来的通量()恰好从出流半面出去(图 5.3)。
the wall has and drops out. Step 4: at the converged state every row is zero, so the far-field fluxes sum to zero as well; what enters through the inflow half-faces () leaves through the outflow half-faces (Figure 5.3).
核对:对一个没有收敛的测试状态 ,左边是 −0.4135,两边之差 2.8×10−16;在 处,入流合计 −1.9641,出流合计 1.9641,总和 −2.2×10−16(本页代码在构建时计算)。
Check: for an unconverged test state the left side is −0.4135 and the two sides differ by 2.8×10−16; at the inflow total is −1.9641, the outflow total 1.9641, and the sum −2.2×10−16 (computed by this page’s code at build time).
残差仍是流出通量的积分和,不除以体积。本例没有源项。内部面向左加 H、向右减 H,因此全域求和时内部贡献抵消,只剩边界通量。二阶先计算所有梯度,再做面循环;格点面分段的每一份贡献都向同一对控制体守恒分发。
The residual remains the sum of integrated outward fluxes, without volume normalisation. There is no source. Each internal face adds H to the left and subtracts H from the right, so global summation cancels interior contributions and leaves boundary flux. Second order computes gradients before the face loop; every dual-face segment conservatively scatters to the same adjacent volumes.
grad = least_squares(u, coordinates)
R[:] = 0
for f in computational_faces:
H = integrate_reconstructed_flux(f,grad)
R[f.L] += H
if f.R exists: R[f.R] -= H
# No special zero-flux branch on the inner boundary.第 5 节的 residual(u) 就是离散残差 R(u)∈ℝN,每个分量对应一个节点。它的 Jacobian 矩阵 A=∂R/∂u 是 N×N 的(算例中 24×24)。本节先把它显式写出来,看清它的结构;下面求解 R(u)=0 只用到它的对角元,第 8、9 节再讲怎样不形成 A 也能乘它。
The residual(u) of Section 5 is the discrete residual R(u)∈ℝN, with one component per node. Its Jacobian A=∂R/∂u is N×N (24×24 in the test case). This section writes it out explicitly, to show its structure; solving R(u)=0 below needs only its diagonal, and Sections 8 and 9 show how to multiply by A without ever forming it.
A 的每一份贡献都来自一个对偶面:对第 3 节那个一维通量求局部导数,
Every contribution to A comes from one dual face: differentiate the one-dimensional flux of Section 3 locally,
再把这两个数按和残差完全相同的符号累加到 A 的四个位置:ALL+=cL、ALR+=cR、ARL−=cL、ARR−=cR。边界半面只有 L 一侧,所以只落在 ALL 上。
and accumulate those two numbers into four entries of A with exactly the signs the residual used: ALL+=cL, ALR+=cR, ARL−=cL, ARR−=cR. A boundary half-face has only an L side, so it lands on ALL alone.
出流半面的导数为什么会“合并”。出流时 uR=uL,同一个 uL 同时进入通量的两个槽位,链式法则要把两个局部导数相加,耗散项正好抵消:
Why the derivatives "merge" on an outflow half-face. At outflow uR=uL, so the same uL enters both slots of the flux; the chain rule adds the two local derivatives and the dissipation terms cancel exactly:
于是边界半面对 A 的贡献全都只落在 ALL 上,边界条件决定的只是这一项的大小:无粘壁面 cL=cR=0;入流 uR=u∞ 是常数,cR=0,只剩 cL;出流按上式合并成 vnuL。图 6.1 在真实网格上对照壁面节点与外圈节点。
So a boundary half-face reaches A only through ALL, and the boundary condition decides only how large that entry is: an inviscid wall gives cL=cR=0; at inflow uR=u∞ is a constant, so cR=0 and only cL remains; at outflow the two merge into vnuL as above. Figure 6.1 compares a wall node with the outer nodes on the real mesh.
这也把第 3 节的一阶、无重构落到了实处:算例中 A 共 120 个非零元,没有一个落在相邻节点之外(每个节点有 3–5 个相邻节点)。
This also pins down the first-order, no-reconstruction claim of Section 3: in the test case A has 120 non-zeros and not one of them falls outside the neighbouring nodes (each node has 3–5 of them).
上面已经把 residual(u) 和它的 Jacobian 都造出来了,却还没有说这个非线性方程组是怎么解开的。本页的 primal 方程、切线方程和伴随方程(切线在第 10 节,伴随在第 11 节)用的是同一个 Jacobi 迭代:每一轮算一次残差,再逐节点除以 Jacobian 的对角元:
Both residual(u) and its Jacobian have now been built, but nothing has yet been said about how the nonlinear system is solved. On this page the primal equation, the tangent equation and the adjoint equation (Sections 10 and 11) are all solved by one and the same Jacobi iteration: every sweep evaluates the residual once and divides it, node by node, by the diagonal of the Jacobian:
diag(A) 不需要把 A 造出来:diagonal(u) 就是残差的那个边循环,每个面只把 cL 加到左节点、把 −cR 加到右节点,也就是它在 A 里的四个元素中落在对角线上的那两个。所以一轮只有一次 residual 和一次 diagonal:不形成矩阵,也不解线性方程组,每个节点各自更新。初值取均匀来流 u0≡1,判据是残差的均方根 ‖R‖<10−13(图 6.2)。
diag(A) does not need A to be formed: diagonal(u) is the residual's own edge loop, in which each face adds only cL to its left node and −cR to its right node, the two of its four entries in A that land on the diagonal. So a sweep is one residual and one diagonal: no matrix is formed and no linear system is solved; every node is updated on its own. The initial guess is the uniform free stream u0≡1, and the criterion is the rms of the residual, ‖R‖<10−13 (Figure 6.2).
为什么用 Jacobi,而不用 LU。本页只有 24 个未知量,理论上最快的办法是直接分解:把 A 整个造出来,用 Newton 法,每一步做一次稠密 LU 分解,k=4 时残差就已低于 10−13。可那样就说明不了 matrix-free。Jacobi 是最简单的迭代法:对 A 它只要对角元,其余的一切都经由残差作用进来(切线方程里是 residual_d,伴随方程里是 residual_b),这正是 matrix-free 的意思。代价是线性收敛:在解附近,误差每一轮乘以迭代矩阵 I−diag(A)−1A,速率由它的谱半径决定,本算例是 0.797,判据在 k=122 时满足。切线与伴随的迭代矩阵与它同谱,所以进入渐近阶段以后,三条收敛曲线以同一个速率下降(第 10、11 节)。
Why Jacobi, and not LU. With only 24 unknowns the fastest method in principle is a direct factorisation: form the whole of A, run Newton, and factorise with one dense LU per step; the residual is below 10−13 by k=4. But that would show nothing about matrix-free. Jacobi is the simplest iterative method there is: of A it needs only the diagonal, and everything else comes in through the residual (through residual_d in the tangent equation and residual_b in the adjoint equation), which is exactly what matrix-free means. The price is linear convergence: near the solution the error is multiplied every sweep by the iteration matrix I−diag(A)−1A, at a rate set by its spectral radius, 0.797 here, and the criterion is met at k=122. The tangent and adjoint iteration matrices have the same spectrum, so once in the asymptotic phase all three convergence curves fall at the same rate (Sections 10 and 11).
与一维 Burgers 页面的区别。一维 Burgers 页面用二阶重构,用 Gauss–Seidel 迭代和 GS 预处理的 GMRES 求解(GMRES 在那里的第 17 节讲)。本页退回一阶和 Jacobi,是为了让二维的边循环保持最小:一阶时每个面只连接两个节点,Jacobian 的每一行只有它自己和它的邻居;Jacobi 的每一轮只调用一次残差和一次对角元,所有节点各自更新,结果与编号顺序无关。代价是收敛慢(上面的谱半径),本页的网格很小,付得起。各节末尾的“新增”扩展(选读)再回到二阶重构与 GMRES。
How this differs from the 1-D Burgers pages. The 1-D Burgers pages use second-order reconstruction and solve with Gauss–Seidel iteration and GS-preconditioned GMRES (GMRES is taught in their Section 17). This page steps back to first order and Jacobi to keep the two-dimensional edge loop at its smallest: at first order every face couples just two nodes, so a row of the Jacobian holds only the unknown itself and its neighbours; and a Jacobi sweep is one residual and one diagonal, every node updated on its own, with a result that does not depend on the numbering. The price is slow convergence (the spectral radius above), affordable on a mesh this small. The optional “Added” extension at the end of each section returns to second-order reconstruction and GMRES.
固定轮次,但会检查。代码固定跑 300 轮,而不是一满足判据就退出:余下的轮次把 u* 一直带到舍入误差平台(从 k=153 起),页面上每一个实测常数都是在这些轮次下量出来的;得到的 u* 与 Newton 法的解最多差 4.4×10−16。真正要紧的是有没有检查:它记录判据在第几轮满足,若 300 轮内从未满足就直接报错,而不会把一个未收敛的状态当成 u* 返回。
A fixed sweep count, but a checked one. The code runs a fixed 300 sweeps rather than exiting at the criterion: the remaining sweeps take u* down to its round-off floor (from k=153 on), and every measured constant on this page was produced under those sweeps; the u* it returns is within 4.4×10−16 of Newton's. What actually matters is whether it is checked: it records the sweep at which the criterion was met, and raises an error if 300 sweeps never meet it, rather than returning an unconverged state as u*.
用 Jacobi 迭代求解 R(u)=0 得到收敛解 u*:
The Jacobi iteration solves R(u)=0 for the converged state u*:
此后 u* 冻结:前向与伴随都是在这个状态上的线性化。图 5.2 播放到最后,就能看到残差逐面累加、最终抵消为零的过程。
From here on u* is frozen: the forward and adjoint modes are both linearisations about this state. Play Figure 5.2 to the end to watch the residual accumulate face by face and finally cancel to zero.
一阶保留原来的 Jacobi 形式 u←u−diag(A)⁻¹R,diag(A) 是一阶残差 Jacobian 的对角。边界数据与主线不同,收敛流场也就不同,主线给出的常数并不适用于新增模型。二阶精确 Jacobian 包含重构邻点,不能只沿用原来的两个面邻点导数。
First order retains u←u−diag(A)⁻¹R with the first-order residual Jacobian diagonal. The boundary data differ from the main line’s, so the converged field differs too, and the main line’s quoted numbers do not carry over. The second-order exact Jacobian includes reconstruction neighbours; the original two face-state derivatives alone are insufficient.
Newton 法、GMRES 与预处理,用最少的话说。二阶残差 R(u) 仍是非线性的。Newton 法在当前 u 处把 R 线性化:解线性方程组 AΔu=−R 得到修正 Δu,再令 u←u+αΔu;α 从 1 开始,只要新状态不全为正、或真残差没有下降,就把 α 减半(回溯)。线性方程组用 GMRES 解:它每一步只要一次乘积 Av(与主线第 8 节 residual_d 同一类的前向运算),在已经得到的方向张成的子空间里找使残差最小的组合,所以同样不必组装 A。预处理 B 先把每个新方向“近似地解一下”:这里是用一阶近似 Jacobian P 做三次 Gauss–Seidel 扫描,GMRES 实际处理的是 AB,好的 B 让 GMRES 用少得多的方向就收敛。GMRES 的逐步讲解见一维 Burgers 页面第 17 节。
Newton, GMRES and preconditioning, in a few words. The second-order residual R(u) is still nonlinear. Newton’s method linearises R at the current u: it solves the linear system AΔu=−R for a correction Δu and sets u←u+αΔu, where α starts at 1 and is halved (backtracking) as long as the new state is not all positive or the true residual has not decreased. The linear system is solved by GMRES, which needs one product Av per step (a forward operation of the same kind as residual_d in Section 8 of the main line) and picks, in the span of the directions produced so far, the combination with the smallest residual; so A is never assembled here either. A preconditioner B first solves each new direction approximately — here three Gauss–Seidel sweeps with the first-order approximate Jacobian P — so GMRES works with AB, and a good B lets it converge with far fewer directions. GMRES is taught step by step in Section 17 of the 1-D Burgers page.
二阶使用带回溯的 Newton 修正,线性系统由 GS 预处理 GMRES 求解。预处理仅保留一阶近似,执行三次 GS 扫描;外层乘积始终使用完整二阶残差的精确前向算子。回溯检查未知量和面重构状态为正,并要求真残差下降。精确算子没有组装成全局矩阵。
Second order uses backtracked Newton corrections solved by GS-preconditioned GMRES. Three GS sweeps of a first-order approximation form the preconditioner; outer products always use the exact derivative of the second-order residual. Backtracking requires positive unknowns and face traces and a decrease in the true residual. The exact global matrix is not assembled.
先看一阶的一次 Jacobi:所有分量都从同一轮旧状态计算 R 与 diag(A),然后同时更新。它不同于立即覆盖分量的 GS。对内部面,两个状态导数为 c_L 和 c_R;左侧对角加 c_L,右侧对角减 c_R。入流外侧 u_in 与状态无关;出流 u⁺=u⁻,必须把 c_L+c_R 合并到同一个对角项。
A first-order Jacobi sweep evaluates R and diag(A) from the same old state, then updates all components simultaneously. GS instead immediately reuses updated components. For an interior face, add c_L to the left diagonal and −c_R to the right diagonal. Inflow u_in is independent of the state; outflow has u⁺=u⁻, so c_L+c_R belongs on a single diagonal entry.
二阶中,P 是按一阶残差组装的稀疏近似。每次预处理从 z=0 开始做三次 GS,形成一个固定线性映射 B;GMRES 对 A B 构造 Krylov 子空间,得到修正 Δu=B y。A v 由完整二阶前向计算给出。即使 P 忽略重构邻点,收敛时检查的仍是完整系统 AΔu+R,而不是近似系统 PΔu+R。
For second order, P is the sparse first-order approximate Jacobian. Each preconditioner application starts at z=0 and performs three GS sweeps, defining a fixed linear map B. GMRES builds its Krylov space for A B and forms Δu=B y. Exact A v comes from the full second-order tangent. Although P omits reconstruction neighbours, convergence checks AΔu+R, not PΔu+R.
if order == 1:
R, d = residual_and_diagonal(u_old)
u_new[:] = u_old[:] - R[:] / d[:]
else:
A(v) = exact_residual_jvp(u,v)
B(r) = three_GS_sweeps(first_order_P,r,start=0)
du = right_preconditioned_GMRES(A,-R,B)
alpha = backtrack_until_positive_and_residual_decreases()
u_new = u_old + alpha*du取类似阻力的表面积分作为目标函数 L:把物面上的“压力” 12u2 投影到来流方向再沿物面积分——它与气动优化里的阻力形式相同:
Take a drag-like surface integral as the objective L: project the "pressure" 12u2 on the body onto the free-stream direction and integrate along the surface — the same form as drag in aerodynamic optimisation:
这里的物面是无粘壁面。L 只依赖壁面上的状态,所以 g=(∂L/∂u)T 只在壁面节点上非零(1–8 号)。后面会看到,伴随解 ψ 却遍布整个流场:物面上的量通过一连串面通量受到上游的影响。
The body here is an inviscid wall. L depends only on the states at the wall, so g=(∂L/∂u)T is non-zero only on the wall nodes themselves, nodes 1–8. As will be seen, the adjoint solution ψ nevertheless spreads over the whole field: quantities on the body are influenced from upstream through a chain of face fluxes.
为什么要投影到来流方向。n·e 是有正负的:迎风面与背风面的贡献方向相反。因为物体封闭,Σ壁面n=0,所以均匀流场给出的 L 恒等于零——它完全来自物面上 u 的不均匀分布。这正是它比“载荷大小之和 Σ12u2|n|”更像真实气动量的地方。图 7.1 把每个壁面半面的贡献和它们的总和画了出来。
Why project onto the free-stream direction. n·e has a sign: the windward and leeward contributions point opposite ways. Because the body is closed, Σwalln=0, so a uniform field gives L≡0 — it comes entirely from the non-uniform distribution of u over the body. That is exactly what makes it more like a real aerodynamic quantity than "the sum of load magnitudes Σ12u2|n|". Figure 7.1 draws the contribution of every wall half-face and their sum.
要记住 L 是什么。L 是这个离散问题的一个泛函,形式上像阻力。但本页的零通量壁面没有连续极限(第 4 节),L 也就不收敛到任何物理阻力:第 15 节实测,网格每加密一层,Lh 大约翻一倍(0.693、1.524)。第 8–14 节算的是这个固定离散 L 的精确梯度,这一点不受影响。
Keep in mind what L is. L is a functional of this discrete problem, drag-like in form. But the zero-flux wall has no continuum limit (Section 4), so L does not converge to any physical drag: Section 15 measures Lh roughly doubling with every refinement (0.693, 1.524). What Sections 8–14 compute is the exact gradient of this fixed discrete L, and that is not affected.
第一步, 是壁面半面上的和,每一项只含该半面左节点的值:
Step 1. is a sum over the wall half-faces, and each term contains only the value of that half-face’s left node:
第二步,对 求偏导。 与 都不含 ,只有 的项留下,平方项的导数是 :
Step 2. Differentiate with respect to . Neither nor contains ; only the terms with survive, and the square differentiates to :
第三步,每个壁面节点是两个壁面半面(两侧壁面边各一半)的 ,所以 (、 是这两个半面);其余 16 个节点 。这就是 core_node.js 的 dJdu:g[f.L − 1] += u[f.L − 1] · (n·e),对每个壁面半面执行一次。
Step 3. Every wall node is the of two wall half-faces (half of each neighbouring wall edge), so with , these two half-faces, and in the other 16 nodes. This is dJdu in core_node.js: g[f.L − 1] += u[f.L − 1] · (n·e), once per wall half-face.
第四步,,所以 的符号就是 的符号:迎风一侧为正,背风一侧为负;在迎风转背风的节点(3、8),两个半面的 异号,由两者之和决定。它的分布画在第 11 节(图 11.2)。
Step 4. Since , the sign of is that of : positive on the windward side, negative on the leeward side; where the wall turns from windward to leeward (nodes 3 and 8) the two half-faces differ in the sign of and their sum decides. Its distribution is drawn in Section 11 (Figure 11.2).
核对: 有 8 个非零元;与逐个 作中心差分(步长 )的 相比,最大差 8.4×10−11(本页代码在构建时计算)。
Check: has 8 non-zeros; against central differences of in each (step ) the largest difference is 8.4×10−11 (computed by this page’s code at build time).
不能只对连续积分写一个形式导数就结束。先确定代码实际使用的求积点、权重、重构状态及边界值,再对这个标量表达式求导。一般的求积和及其链式法则是:
A formal derivative of a continuous integral is not enough. First identify the quadrature points, weights, reconstructed states and boundary values actually used by the code, then differentiate that scalar expression. A general quadrature sum and its chain rule are:
本页的 是这种加权和:每个壁面半面一项(每条壁面边被两端节点各分一半,共 16 个半面),,;面法向随节点坐标变化,所以 同时依赖状态与设计变量。
The of this page is such a weighted sum: one term per wall half-face (each wall edge is halved between its two end nodes, 16 half-faces in all), and ; the face normal moves with the node coordinates, so depends on both the state and the design variables.
求 时固定所有设计变量;求 时固定全部离散状态槽位。固定解析目标函数并不一定意味着移动网格上的目标采样值不变:如果采样点随设计移动,还要保留该位置依赖;固定网格及固定采样值时,相应项才为零。目标中的体积权重已经进入 ,不应在伴随右端再乘一次体积。
Hold every design variable fixed for , and every stored state entry fixed for . A fixed analytic target function need not have fixed samples on a moving mesh: if the evaluation locations move with the design, keep that dependence; the corresponding terms vanish only when the mesh and samples are fixed. Objective volume weights already enter and must not be multiplied into the adjoint right-hand side again.
伴随右端本身也可以由一次反向扫描得到:以 为种子,把上面的求积和逐项反过来执行,
The adjoint right-hand side is itself obtained by a reverse sweep: seed and run the terms of the quadrature sum above backwards,
从 、 开始,扫完全部求积点后 、:一个状态被多个求积点读取,所以要先置零、再累加。前向的 与反向的这两个结果满足同一个对偶关系 。
Starting from and , after all quadrature points and : a state is read by several quadrature points, so its adjoint is zeroed first and then accumulated. The forward and these two reverse results satisfy one duality, .
保留原来的有方向内边界积分形式,但不再解释为不可穿透物面的真实受力。令 e=β/|β|,目标 L 是内边界上 u²/2 的投影积分。离散目标 L_h 用边界状态 u⁺:入流面上是给定的 u_in,出流面上 u⁺=u⁻(二阶时是重构的面迹)。一阶取控制体值,二阶用同样的 Gauss 积分,目标的导数也要穿过重构。这与附录 B 的连续推导一致:给定入流数据时,入流边界上的状态不随设计改变(附录中的变分在那里为零),目标在那里不依赖状态。
Keep the original direction-projected inner-boundary integral, but no longer interpret it as a physical force on an impermeable body. With e=β/|β|, the objective L integrates u²/2 against e·n over the inner boundary. The discrete objective L_h uses the boundary state u⁺: the prescribed u_in on inflow faces and u⁺=u⁻ on outflow faces (the reconstructed trace at second order). First order takes the control-volume value, second order the same Gauss quadrature, and the derivatives of the objective pass through the reconstruction too. This matches the continuous derivation of Appendix B: with the inflow data fixed, the state on the inflow boundary does not change with the design (the appendix’s variation vanishes there), so the objective does not depend on the state there.
为什么入流面上要用 u⁺。本页之前的版本在入流面上也用内侧迹 u⁻。那样离散目标在入流边界上依赖状态,连续目标却不依赖,离散伴随就在内边界上只连着入流半面的节点(入流与出流交界处的两个特征角点不计,见附录 B)上多出一个不随加密消失的误差。实测一阶第 0–4 层,这些节点上的 max|ψ_h−ψ_ref| 用 u⁻ 时是 0.412、0.559、0.656、0.710、0.738,越加密越大;改用 u⁺ 后是 0.017、0.009、0.008、0.007、0.007(附录 B 的表)。
Why u⁺ on the inflow faces. An earlier version of this page used the interior trace u⁻ on inflow faces too. Then the discrete objective depends on the state on the inflow boundary while the continuous one does not, and the discrete adjoint picks up an error on the inner-boundary nodes whose half-faces are all inflow (the two characteristic corners between inflow and outflow are left out; see Appendix B) that refinement never removes. Measured at first order on levels 0–4: on those nodes, max|ψ_h−ψ_ref| is 0.412, 0.559, 0.656, 0.710, 0.738 with u⁻, growing with refinement, and 0.017, 0.009, 0.008, 0.007, 0.007 with u⁺ (the table in Appendix B).
由于解析场在孔洞内部也满足散度为零,封闭内边界上的积分恰好为零:连续的 L 与它对全部设计变量的导数 dL/dD 都是零。粗网格上的 L_h 与 dL/dD 一般不为零,它们就是离散误差,加密时应当趋于零。附录 B 的表逐层给出实测。二阶第 0–4 层:L_h 为 1.74×10⁻²、3.30×10⁻²、1.60×10⁻²、2.93×10⁻³、4.68×10⁻⁴,dL/du∞ 为 1.16×10⁻²、2.74×10⁻²、8.41×10⁻³、1.61×10⁻⁴、4.10×10⁻⁵,48 个基准网格坐标上的 max|dL/dX| 为 1.68×10⁻¹、1.42×10⁻¹、5.56×10⁻²、1.65×10⁻²、4.42×10⁻³。一阶趋零慢得多:L_h 为 3.38×10⁻²、4.38×10⁻²、4.33×10⁻²、3.85×10⁻²、2.90×10⁻²,max|dL/dX| 为 1.71×10⁻¹、1.86×10⁻¹、1.79×10⁻¹、1.50×10⁻¹、1.07×10⁻¹。这一退化的连续目标不是优化算例的有效性能指标;在这里用来检验实际的离散求导链,以及它在加密时是否趋向连续值。
The analytic field is divergence-free even inside the hole, so the closed inner-boundary integral is exactly zero: the continuous L and its derivative dL/dD with respect to every design variable vanish. On a coarse mesh L_h and dL/dD generally do not; they are discretisation errors and should vanish under refinement. The table in Appendix B gives the measurements level by level. Second order, levels 0–4: L_h is 1.74×10⁻², 3.30×10⁻², 1.60×10⁻², 2.93×10⁻³, 4.68×10⁻⁴, dL/du∞ is 1.16×10⁻², 2.74×10⁻², 8.41×10⁻³, 1.61×10⁻⁴, 4.10×10⁻⁵, and max|dL/dX| over the 48 coordinates of the base mesh is 1.68×10⁻¹, 1.42×10⁻¹, 5.56×10⁻², 1.65×10⁻², 4.42×10⁻³. First order goes to zero much more slowly: L_h is 3.38×10⁻², 4.38×10⁻², 4.33×10⁻², 3.85×10⁻², 2.90×10⁻² and max|dL/dX| is 1.71×10⁻¹, 1.86×10⁻¹, 1.79×10⁻¹, 1.50×10⁻¹, 1.07×10⁻¹. This degenerate continuous objective is not a useful optimisation performance metric; here it tests the actual discrete differentiation chain, and whether it approaches the continuous values under refinement.
设计变量 X∈ℝM 取网格坐标(本页取全部节点坐标,外形由其中壁面节点那部分决定)。对 R(u(X),X)=0 用链式法则得 Adu/dX=−∂R/∂X,于是
The design variables X∈ℝM are the mesh coordinates (all node coordinates on this page; the shape is the wall node part of them). Applying the chain rule to R(u(X),X)=0 gives Adu/dX=−∂R/∂X, hence
先算右边的乘积,要对每个设计变量各解一次线性方程组(M 次);先算左边的乘积,只需解一次伴随方程 ATψ=−(∂L/∂u)T,然后
Evaluate the right-hand pair first and one linear system has to be solved per design variable (M of them); evaluate the left-hand pair first and only a single adjoint equation ATψ=−(∂L/∂u)T is needed, after which
形状优化中 M 常达数千,而伴随的求解次数始终是 1(每个目标函数一次)。
In shape optimisation M often runs to thousands, while the adjoint always needs exactly one solve (one per objective).
把这条乘积链画出来就一目了然(图 8.1):同一串矩阵,只是加括号的方式不同。先乘右边那一对,得到的中间量有 M 列,于是要解 M 次;先乘左边那一对,得到的中间量只有一行,于是只解一次——这一行就是 −ψT。
Drawing the product chain makes it obvious (Figure 8.1): the same string of matrices, bracketed two different ways. Multiply the right-hand pair first and the intermediate has M columns, so it costs M solves; multiply the left-hand pair first and the intermediate is a single row, so it costs one — and that row is −ψT.
对任意矩阵 A 和向量 v,w,恒有
For any matrix A and vectors v,w, it always holds that
满足这一关系的算子称为伴随算子,对实矩阵就是转置。前向乘积把“状态上的扰动”映射成“残差的变化”;伴随乘积方向相反,把“残差上的权重”映射成“状态上的敏感度”。
An operator satisfying this relation is called the adjoint; for a real matrix it is the transpose. The forward product maps "a perturbation of the state" to "the change in the residual"; the adjoint product runs the other way, mapping "a weight on the residual" to "a sensitivity on the state".
第 8、9 两节要回答的就是一个问题:怎样在不形成矩阵的情况下计算这两个乘积。
Sections 8 and 9 answer a single question: how to compute these two products without forming the matrix.
实际网格的 N 可达 105–108。GMRES 等 Krylov 迭代解法从不读取单个矩阵元素,只需要“给一个向量、返回乘积”的函数(Jacobi 另外只需要对角元)。matrix-free 就是不形成、不存储 A,直接写函数 residual_d(u, v) 返回 Av、residual_b(u, w) 返回 ATw。
On a real mesh N can reach 105–108. Krylov solvers such as GMRES never read an individual matrix entry; all they need is a function that takes a vector and returns the product (Jacobi needs only the diagonal besides). matrix-free means exactly that: never form or store A, and instead write residual_d(u, v) returning Av and residual_b(u, w) returning ATw.
记号。从这里起,前向与伴随都用自动微分的记号:程序变量 x 的前向导数(切线)记作 ẋ,反向导数(伴随)记作 x̄。前向乘积的种子是 u̇=v,结果是 Ṙ=Av;伴随乘积的种子是 R̄=w,结果是 ū=ATw。两者满足对偶关系 R̄TṘ=ūTu̇,也就是上面的 ⟨w,Av⟩=⟨ATw,v⟩。代码沿用 Tapenade 的命名:x_d 就是 ẋ,x_b 就是 x̄(residual_d、flux_d、h_d;residual_b、h_b、uL_b)。变分符号 δ 只出现在附录的连续伴随里,表示连续函数的变分;正文里的点和横线是对离散代码的前向与反向求导。
Notation. From here on the forward and the adjoint mode are written in the notation of algorithmic differentiation: the forward derivative (tangent) of a program variable x is ẋ, its reverse derivative (adjoint) x̄. The forward product has the seed u̇=v and the result Ṙ=Av; the adjoint product has the seed R̄=w and the result ū=ATw. The two satisfy the duality R̄TṘ=ūTu̇, which is ⟨w,Av⟩=⟨ATw,v⟩ above. The code follows Tapenade's names: x_d is ẋ and x_b is x̄ (residual_d, flux_d, h_d; residual_b, h_b, uL_b). The variation symbol δ appears only in the continuous adjoint of the appendices, as the variation of a continuous function; the dot and the bar in the main text are forward- and reverse-mode derivatives of the discrete code.
前向容易,伴随难。Av 可以用一次扰动近似(η 是一个小步长):Av≈[R(u+ηv)−R(u)]/η。ATw 没有一次扰动的公式。它的第 j 个分量是 (ATw)j=wT(Aej)≈wT[R(u+ηej)−R(u)]/η:要对每个未知量各扰动一次,一共 N 次残差计算,才得到整个 ATw。实际网格的 N 太大,这条路走不通,所以 ATw 要靠把残差计算反过来求导:第 9 节的边循环只走一遍。
Forward is easy, adjoint is not. Av can be approximated with a single perturbation (η a small step): Av≈[R(u+ηv)−R(u)]/η. ATw has no single-perturbation formula. Its j-th component is (ATw)j=wT(Aej)≈wT[R(u+ηej)−R(u)]/η: one perturbation per unknown, N residual evaluations in all, give the whole of ATw. On a real mesh N is far too large for that, so ATw is obtained by differentiating the residual computation in reverse: the edge loop of Section 9, run once.
h=H(uL,uR) 只有两个输入。两侧状态的切线(微小扰动)为 u̇L、u̇R 时,由链式法则
h=H(uL,uR) has only two inputs. When the two states carry tangents (small perturbations) u̇L and u̇R, the chain rule gives
cL、cR 是局部导数,在冻结状态 u* 处取值。下表四种面的差别只体现在这两个系数上:
cL and cR are the local derivatives, evaluated at the frozen state u*. The four kinds of face below differ only in these two coefficients:
| 面的类型 | Face type | 右状态 uR | Right state uR | 切线 u̇R | Tangent u̇R | cL | cR | |
|---|---|---|---|---|---|---|---|---|
| 内部对偶面 | Interior dual face | u[j] | v[j] | 12vnuL+ε|n| | 12vnuR−ε|n| | |||
| 无粘壁面半面 | Inviscid wall half-face | —(h≡0) | — (h≡0) | — | 0 | 0 | ||
| 远场半面(入流) | Far-field half-face (inflow) | u∞(常数) | u∞ (constant) | 0 | 12vnuL+ε|n| | 不用 | not used | |
| 远场半面(出流) | Far-field half-face (outflow) | uL(外推) | uL (extrapolated) | u̇L | vnuL(两项相加) | vnuL (the two terms combine) | 不用 | not used |
把 residual 里的“状态”换成“扰动”、flux 换成 flux_d,其余一行不改:收集两端扰动,计算 ḣ,按同样的符号分发(图 8.2)。
Replace "state" by "perturbation" in residual, and flux by flux_d; not one other line changes. Gather the perturbations at the two ends, compute ḣ, scatter it with the same signs (Figure 8.2).
residual 逐行对应:每个对偶面上把两端扰动按局部导数合成一个 ḣ,再以 +、− 分发到两端节点;每个控制体的 Ṙ 就是它所有对偶面的 ḣ 按符号(作为 i 取 +、作为 j 取 −)求和。Figure 8.2 · The forward edge loop corresponds line for line to residual: on every dual face the perturbations at the two ends are combined through the local derivatives into a single ḣ, which is then scattered to the two end nodes with + and −; the Ṙ of a control volume is the signed sum (+ where it is i, − where it is j) of the ḣ of all its dual faces.图 8.3 的默认种子是 v=ei(只扰动一个内部节点)。播放时注意:只有该节点的那些对偶面得到非零的 ḣ,最后只有它和它的邻居非零——这正是 A 的相应一列。点击网格中的控制体可以逐个修改种子(0 → 1 → −1 → 0)。
The default seed in Figure 8.3 is v=ei (perturbing a single interior node). Watch as it plays: only the dual faces of that node pick up a non-zero ḣ, and at the end only the node itself and its neighbours are non-zero — which is precisely the corresponding column of A. Click a control volume in the mesh to cycle its seed entry (0 → 1 → −1 → 0).
每个面的计算是线性的:ḣ=cLu̇L+cRu̇R,再以 +,− 分发到 i,j。所以一个内部对偶面对 A 的贡献只落在第 i,j 行、第 i,j 列交叉处的 2×2 块上(边界半面只贡献 Aii,无粘壁面半面什么也不贡献):
The computation on each face is linear: ḣ=cLu̇L+cRu̇R, then scattered to i,j with +,−. So the contribution of one interior dual face to A falls only on the 2×2 block where rows i,j meet columns i,j (a boundary half-face contributes only Aii, and an inviscid wall half-face contributes nothing at all):
这一点在 图 8.3 的“矩阵视图”里可以直接看到:播放时,当前对偶面写入的块会被高亮(内部对偶面是 2×2 块,远场半面只有对角元,无粘壁面半面什么也不写),右边两列分别是显式矩阵乘积与边循环的累加结果——循环走完,两列完全相同。
This is visible directly in the "matrix view" of Figure 8.3: as it plays, the block written by the current dual face is highlighted (a 2×2 block for an interior dual face, just the diagonal entry for a far-field half-face, nothing at all for an inviscid wall half-face), and the two columns on the right hold the explicit matrix product and the running total of the edge loop — once the loop has finished, the two columns agree exactly.
上面的表格给出了四种对偶面的 、。它们不是另外推出来的:把第 5 节的 residual 循环逐行求导,就同时得到它们和 residual_d 的每一行。
The table above gives and for the four kinds of dual face. They are not derived separately: differentiating the residual loop of Section 5 line by line gives them, and every line of residual_d with them.
第一步,写下一个内部对偶面在 residual 里执行的三行(local() 与 residual()):
Step 1. The three lines an interior dual face executes in residual (local() and residual()):
第二步,给输入一个任意方向的种子 :把输入换成 ,对 在 处求导,每个程序变量 的导数记作 (第 8 节的记号)。收集是线性的,它的切线就是同样的收集:,。
Step 2. Seed the input with an arbitrary direction, : replace the input by and differentiate with respect to at , writing for the derivative of each program variable (the notation of Section 8). The gather is linear, so its tangent is the same gather: , .
第三步,通量这一行用链式法则,再逐项求偏导:中心项 的导数是 ,耗散项 对 是 、对 是 :
Step 3. The flux line takes the chain rule, then the partial derivatives term by term: the central term differentiates to , and the dissipation gives for and for :
第四步,分发同样是线性的:,。三行合起来就是 residual_d,代码里把 、 写作 h_d、r_d。
Step 4. The scatter is linear too: , . Together the three lines are residual_d, whose code writes and as h_d and r_d.
第五步,边界半面只改第二个槽位的收集:
Step 5. Boundary half-faces change only the gather of the second slot:
第六步,所有对偶面加起来, 对 是线性的,它就是 :每个内部对偶面贡献上面那个 块,边界半面只贡献对角元。
Step 6. Summed over all dual faces, is linear in , and it is : every interior dual face contributes the block above, and a boundary half-face only a diagonal entry.
核对:对一个测试方向 ,residual_d 与 (小步长 )的最大相对差 1.3×10−10,与显式矩阵乘积 的最大差 5.6×10−16;一个内部对偶面上 、 与通量公式的中心差分之差 1.9×10−11;每个出流半面上 与 之差最大 1.1×10−16(本页代码在构建时计算)。
Check: for a test direction , residual_d and (a small step ) differ by 1.3×10−10 relative, and residual_d and the explicit product by 5.6×10−16; on one interior dual face, and differ from central differences of the flux formula by 1.9×10−11; on every outflow half-face, and differ by at most 1.1×10−16 (computed by this page’s code at build time).
上面把碰到状态的三行逐行求了导。可是一个对偶面在算通量之前,还要先由节点坐标算出它的法向、长度和法向速度;把这些也写出来,residual 里一个对偶面执行的是下表的各句(壁面半面只执行几何——它自己的一句 ,见下面第一步——和最后那句目标函数:它没有长度、法向速度、状态、通量与分发)。按原来的顺序逐句求导,就得到前向程序: 时它就是本节的 residual_d,、 时它就是第 12 节的 gradient_d。两者本是同一个程序。
The derivation above differentiated the three lines that touch the state. Before the flux, however, a dual face also computes its normal, its length and its normal speed from the node coordinates; with these written out, one dual face of residual executes the statements of the table below (a wall half-face runs only the geometry, its own statement (Step 1 below), and the objective's statement at the end: no length, normal speed, state, flux or scatter). Differentiating them one by one, in the same order, gives the forward program: with it is residual_d of this section, with and it is gradient_d of Section 12. The two are one program.
| # | # | 原始语句 | primal statement | 切线语句(代码名:x_d 即 ẋ) | tangent statement (code: x_d is ẋ) |
|---|---|---|---|---|---|
| 1 | x[q] ← X[q] (q in t, t′) | x_d[q] ← X_d[q] | |||
| 2 | m ← (x[i] + x[j]) / 2 | m_d ← (x_d[i] + x_d[j]) / 2 | |||
| 3 | xc[t] ← mean of x[q], q in t | xc_d[t] ← mean of x_d[q], q in t | |||
| 4 | d[t] ← xc[t] − m | d_d[t] ← xc_d[t] − m_d | |||
| 5 | n ← sum over t of s_t·(d[t].y, −d[t].x) | n_d ← sum over t of s_t·(d_d[t].y, −d_d[t].x) | |||
| 6 | len ← sqrt(n·n) | len_d ← (n·n_d) / len | |||
| 7 | vn ← β·n | vn_d ← β·n_d | |||
| 8 | uL ← u[i]; uR ← u[j] (入流 uinf,出流 uL) | uL ← u[i]; uR ← u[j] (inflow uinf, outflow uL) | uL_d ← u_d[i]; uR_d ← u_d[j] (入流 uinf_d,出流 uL_d) | uL_d ← u_d[i]; uR_d ← u_d[j] (inflow uinf_d, outflow uL_d) | |
| 9 | h ← vn·(uL² + uR²)/4 − ε·len·(uR − uL) | h_d ← cL·uL_d + cR·uR_d + hv·vn_d + hl·len_d | |||
| 10 | r[i] += h; r[j] −= h | r_d[i] += h_d; r_d[j] −= h_d | |||
| 11(壁面) | 11 (wall) | L += uL²·(n·e)/2 | L_d += uL·(n·e)·uL_d + uL²·(e·n_d)/2 |
字母分工。第 8–12 节里, 只数设计分量(、、),一个单元的顶点用 数;不带下标的 是目标的方向 ,带下标的 是第 个单位向量;下标 、 指一个面的左侧与右侧,不带下标的 是目标。、 是一个对偶面两侧的两个单元,这里的 从不表示时间。本页播放器里的 e13 之类是对偶面的名字(就是 f13),与目标的方向 无关。
Letters. In Sections 8–12, only counts the design components (, , ), and the vertices of a cell are counted by ; without an index is the direction of the objective, is the -th unit vector; the subscripts and name the left and the right side of a face, and without a subscript is the objective. and are the two cells on either side of a dual face; never means time here. The e13 of this page's players is the name of a dual face (it is f13), unrelated to the objective's direction .
第一步,逐句的局部导数。记 (代码里的 len)。长度与法向速度对法向求导:
Step 1, the local derivatives of each statement. Write (len in the code). The length and the normal speed differentiate as
通量 有四个输入,每个输入一项偏导:
The flux has four inputs, and each gives one partial derivative:
代码里这四个偏导依次叫 cL、cR、hv、hl,就是上表 h_d 一句里的四个系数:hv 是 ,hl 是 。前两项就是上面的 ;后两项是几何项,代入 、 后合成第 12 节的 :
In the code these four partial derivatives are called cL, cR, hv and hl, the four coefficients of the h_d statement in the table above: hv is and hl is . The first two terms are the above; the last two are the geometric terms, and with and substituted they combine into the of Section 12:
法向由线性的几何语句算出,所以它的切线就是同样的线性组合:,,;边界半面 。
The normal comes from linear geometric statements, so its tangent is the same linear combination: , , ; a boundary half-face has .
第二步,边界上的三种行:
Step 2, the three kinds of boundary row:
第三步,本页的一个例子(图 8.4 逐句播放)。取对偶面 f13(节点 6、14;本页的播放器把对偶面写作 e 加编号,这里是 e13),种子 , 是壁面节点 7 的 坐标;状态的种子取这个 的切线解 (第 10 节解它;任何 都能逐句代入,取这一个是为了让整个循环的 为零)。逐句得到 ,,,,,,以及
Step 3, an example on this page (Figure 8.4 plays it statement by statement). Take dual face f13 (nodes 6 and 14; the players of this page write a dual face e and its number, here e13) and the seed , the coordinate of wall node 7; as the state seed take the tangent solution of this , (Section 10 solves it; any can be put through the statements, and this one makes the loop's vanish). Statement by statement: , , , , , , and
第四步,整个循环。80 个面(48 个内部对偶面与 32 个边界半面)都这样走一遍, 在每个节点都是零(最大 8.3×10−17,右端项本身最大 0.191);目标的切线
Step 4, the whole loop. Running all 80 faces (48 interior dual faces and 32 boundary half-faces) this way, is zero in every node (at most 8.3×10−17, against a right-hand side of size 0.191); the tangent of the objective,
就是梯度的第 个分量 :第 12 节的伴随梯度给出同一个数(相对差 2.4×10−16),第 14 节的复数步长也是(相对差 1.2×10−16)。本节末尾的推导(“本页一个面的算例:前向扫描”,图 8.5)把同样的前向扫描写成一般的四条语句,取的是另一个对偶面(面循环里第一个两侧都不在壁面上的内部面)和种子 ,不是这里的坐标种子。
is the -th component of the gradient: the adjoint gradient of Section 12 gives the same number (relative difference 2.4×10−16), and so does the complex step of Section 14 (1.2×10−16). The derivation at the end of this section (“A worked example on this page: the forward sweep of one face”, Figure 8.5) works the same sweep in the general four-statement form, on another dual face (the first interior one whose two sides are off the wall) and for the seed instead of a coordinate.
核对:把逐句写出的前向程序(几何、状态和 三种种子同时给随机值)与本页的 residual_d、gradient_d 与 dRdUinf 之和相比,相对差 1.4×10−16,目标部分 3.5×10−16;逐句算出的每个对偶面几何与本页存的 、、 最大差 0(本页代码在构建时计算)。
Check: the forward program written statement by statement (random seeds on the geometry, the state and at once) against the sum of this page’s residual_d, gradient_d and dRdUinf: relative difference 1.4×10−16, objective part 3.5×10−16; the geometry computed statement by statement against the stored , and of every dual face: 0 (computed by this page’s code at build time).
固定网格和当前可微分支,把一个面的所有输入记为 ;它包括面两侧的状态(新增模型中是重构状态)、面的几何与边界数据。令 、 分别是 对存储状态和设计变量的导数, 是局部通量 Jacobian。于是每个残差行的导数按同一个有向面关联矩阵 装配,这里的求和包括相应的边界通量映射。本页没有被替换的行,边界条件全部在边界面的通量里。前向模式按计算顺序传播切向量,状态种子是 ,设计种子是 :
At fixed mesh and differentiable branch, let collect all face inputs: the states of both sides (reconstructed states in the added model), the face geometry and the boundary data. Let and be the derivatives of with respect to the stored states and the design variables, and the local flux Jacobian. Assemble each residual derivative with the same oriented face incidence , including the boundary flux maps. This page replaces no rows: every boundary condition sits in a boundary-face flux. Forward mode carries the tangents in the order of the computation, with the state seed and the design seed :
无体源项时 。面长度和法向量随节点坐标变化,它们的设计依赖属于 ,不能只对状态求导。本页的 就是这些几何语句(、 与 ,都是 的函数)的导数,再加上远场入流面的来流槽位 。主线格式是一阶, 只连接面两侧的值;新增模型的二阶重构使 连接更多邻居;matrix-free 仍只需依次执行这些局部乘积,不必存储全局矩阵。
Set when there is no volume source. Face lengths and normals move with the node coordinates; that design dependence belongs in , so differentiating the states alone is insufficient. On this page is exactly the derivative of these geometric statements (, and , all functions of ) together with the inflow slot of the far-field inflow faces. The main-line scheme is first order, so connects only the two values of a face; the second-order reconstruction of the added model connects it to more neighbours. Matrix-free evaluation performs these local products without storing the global matrix.
上面的 、 与 来自同一段短程序:残差代码对每个面依次执行四条语句。设 是面 读取的存储位置(两侧的 、,以及重构需要时它们的邻居),把这些位置上的状态排成 :
The , and above come from one short program: the residual code runs four statements on every face. Let be the stored locations that face reads (its two sides and , and their neighbours when the reconstruction needs them), and stack the states stored there as :
(1)取值: 的每一行只有一个 1,从全局数组里挑出 上的状态;(2)重构:由这些状态和设计变量算出面的全部输入 ,在本页是取出的两个状态 ,加上面积向量 与面长 (一阶时对状态 ;面积向量与面长由节点坐标 算出);(3)通量:(本页把面通量 记作 );(4)分发: 是有向关联矩阵 的第 列,在第 行为 、第 行为 ,即 、(边界面只写一行)。这四条语句是残差代码的骨架;本页代码里的每一条语句(含边界面与源项)都在本节里逐句写出,并附各自的切向量语句。
(1) Gather: each row of holds a single 1 and picks one state of out of the global array. (2) Reconstruct: the states and the design give all inputs of the face, on this page the two gathered states , plus the area vector and the face length (at first order on the states; the area vector and the length are functions of the node coordinates ). (3) Flux: (this page writes the face flux as ). (4) Scatter: is column of the oriented incidence , in row and in row , that is and (a boundary face writes one row). These four statements are the skeleton of the residual code; every statement of this page's code, boundary faces and sources included, is written out with its tangent statement in this section.
前向模式对每条语句用一次链式法则:每条语句输出的切向量是它输入的切向量的线性组合,每个中间变量都有自己的切向量:
Forward mode applies the chain rule once per statement: the tangent of each output is a linear combination of the tangents of the statement's inputs, and every intermediate variable gets a tangent of its own:
取值与分发都是线性的,它们的切向量就是同样的取值与分发作用在切向量上;只有重构与通量两条语句带来依赖当前状态的局部导数 、 与 。把(1)代入(2),(2)代入(3),(3)代入(4),对所有面求和,再减去体源项的切向量 ,就逐条语句得到 :
Gather and scatter are linear: their tangents are the same gather and scatter applied to the tangents. Only the reconstruct and flux statements bring local derivatives that depend on the current state: , and . Substituting (1) into (2), (2) into (3) and (3) into (4), summing over all faces and subtracting the tangent of the volume source assembles statement by statement:
上面的 就是 ;一阶重构不改变取出的状态, 在状态上是单位阵, 就是取值。所以一个面对 的贡献是四个初等 Jacobian 的乘积
The above is ; first-order reconstruction leaves the gathered states unchanged, so is the identity on the states and is the gather. One face therefore contributes to the product of four elementary Jacobians,
它从右往左作用在 上,与程序执行的顺序相同:先取值,再重构、通量,最后分发。 只有几个位置,所以 只有一小块非零,matrix-free 乘积就是按面执行这几次局部运算,不存储任何全局矩阵。图 8.5 按这四条语句逐条播放本页一个面的数值。
It acts on from right to left, in the order in which the program runs: gather first, then reconstruct and flux, scatter last. holds only a few locations, so has a small nonzero block, and the matrix-free product runs these local operations face by face without storing any global matrix. Figure 8.5 plays the four statements with the numbers of one face of this page.
取面循环里第一个两侧都不在壁面上的内部面 (节点 9 在左、10 在右);一阶格式只读两侧的状态,。设计种子取 , 是来流值 (本页前向求解的方向);状态种子取本页前向求解对同一方向给出的解 ,于是下面每个切向量都是对 的导数。表中的数由本页代码在构建时算出:基准状态、前向求解与局部导数都用本页自己的程序。
Take the first interior face of the face loop whose two sides are off the wall, (node 9 on the left, 10 on the right); the first-order scheme reads only its two sides, . Seed the design with , where is the free-stream value (the direction of this page's tangent solve), and the states with this page's tangent solution for the same direction, ; every tangent below is then a derivative with respect to . The numbers are computed by this page's code at build time: its baseline state, its tangent solve and its local derivatives.
| 语句Statement | 量Quantity | 原始值Primal value | 切向量Tangent |
|---|---|---|---|
| (1)取值(1) gather | 0.917 | 0.9071 | |
| 0.9456 | 0.9737 | ||
| 设计种子design seed | 1 | 1 | |
| (2)重构(2) reconstruct | −0.4099 0.2802 | 0 0 | |
| 0.4965 | 0 | ||
| (3)通量(3) flux | −0.1284 | −0.2628 | |
| (4)分发(4) scatter | −0.1284 | −0.2628 | |
| 0.1284 | 0.2628 |
不直接进入这个面:, 全部来自状态的切向量。 只作为远场入流面的右状态进入通量,所以 只在 5 个节点上非零;几何量也是 的一部分,但这个种子不移动任何节点,所以它们的切向量都是零。
does not enter this face directly: , and comes entirely from the state tangents. enters only as the right state of the far-field inflow faces, so is nonzero in 5 nodes only; the geometry is part of too, but this seed moves no node, so its tangents are zero.
所有面与源项的这些贡献加起来是 ,在收敛解上为零:这正是下面前向求解的方程。
Summed over all faces and the source terms, these contributions give , which vanishes at the converged solution: the tangent equation solved further on.
按程序顺序执行四条语句,箭头上是各自的初等 Jacobian;下方是它们的乘积与形状(行 × 列,本页这个面的尺寸)。
The four statements in program order, each arrow labelled with its elementary Jacobian; below, their product with the shapes (rows × columns, this face of this page).
种子:( 是来流值 ),。在 上: 0.9071, 0.9737。
Seeds: ( is the free-stream value ) and . On : 0.9071, 0.9737.
(1)取值:,只挑出 上的 2 个状态的切向量(上一步列出的数)。
(1) Gather: picks the tangents of the 2 states of (the numbers of the previous step).
(2)重构:。一阶格式,面输入中的状态就是取出的状态。 (0, 0), 0(这个种子不移动任何节点)。: 不进入这个面。
(2) Reconstruct: . First order: the states among the face inputs are the gathered states. (0, 0), 0 (this seed moves no node). : does not enter this face.
(3)通量: −0.2628。
(3) Flux: −0.2628.
(4)分发: −0.2628,,即加上 0.2628。
(4) Scatter: −0.2628 and , that is 0.2628 added.
给状态种子 与几何/入流参数的种子 ,逐个标量操作应用链式法则;点表示前向导数(主线第 8 节的记号)。把最小二乘梯度记作 (代码里的 grad[i]),它的切线是 。固定网格时,最小二乘梯度对 u 是线性的;几何变化时,M 本身也会改变。漏掉 项会使状态点积检验仍然通过,却使形状梯度错误。
Given the state seed and the geometry/inflow seed , apply the chain rule to every scalar operation; the dot is the forward derivative (the notation of Section 8 of the main line). Call the least-squares gradient (grad[i] in the code); its tangent is . The least-squares gradient is linear in u for fixed geometry, but M changes under geometry perturbations. Omitting may still pass a state transpose test while corrupting shape gradients.
differentiate coordinates, centres, normals, lengths differentiate M and b; grad_d = solve(M, b_d - M_d*grad) differentiate reconstructed traces and inflow u_in(xq) H_d = chain_rule(flux) R_d[L] += H_d; R_d[R] -= H_d
先解出梯度 ∇_hu_i=M_i⁻¹b_i,再对 ()求导:。这样不用显式推导逆矩阵的每个元素,也清楚地看到几何项来自哪里。固定网格令 ;几何方向则不能省略这些项。面板用固定示例种子 展示逐面累加,下一节求解面板再计算真正的参数敏感度。
First solve ∇_hu_i=M_i⁻¹b_i, then differentiate () to obtain . This exposes the geometry terms without separately differentiating every inverse entry. Fixed-grid directions set ; shape directions cannot. The face player uses the sample seed ; the tangent-solve panel later computes actual parameter sensitivities.
前向的一维通量是“两个输入、一个输出”:(u̇L,u̇R)↦ḣ=cLu̇L+cRu̇R。它的转置是“一个输入、两个输出”:给面上的权重 h̄,返回
The forward one-dimensional flux is "two inputs, one output": (u̇L,u̇R)↦ḣ=cLu̇L+cRu̇R. Its transpose is "one input, two outputs": given a weight h̄ on the face, it returns
整个边循环也按同样的规则倒过来:原来写 r[i] 的地方现在读 w[i],原来读 u[i] 的地方现在写 y[i]。注意符号:原来是 r[i]+=h、r[j]−=h,转置后就是 h̄=w[i]−w[j];两个分发则都是加。按第 8 节的记号,种子是 R̄=w,结果是 ū=y=ATw。
The whole edge loop reverses by the same rule: where it used to write r[i] it now reads w[i], and where it used to read u[i] it now writes y[i]. Mind the signs: what was r[i]+=h and r[j]−=h becomes h̄=w[i]−w[j] after transposing, while both scatters are additions. In the notation of Section 8, the seed is R̄=w and the result is ū=y=ATw.
在图 9.1 里对比第 8 节:底部数组的箭头方向反过来了——输入权重 w 在下面(残差一侧),结果 y 在上面(状态一侧)。这就是“转置”在数据流上的样子。无粘壁面半面上 cL=0,所以它在伴随里同样什么都不贡献。
Compare Figure 9.1 with Section 8: the arrows on the bottom arrays have reversed — the input weights w are below (the residual side) and the result y above (the state side). That is what "transpose" looks like as data flow. On an inviscid wall half-face cL=0, so it contributes nothing in the adjoint either.
| 步骤 | Step | residual(原始) | residual (primal) | residual_d(前向) | residual_d (forward) | residual_b(伴随) | residual_b (adjoint) |
|---|---|---|---|---|---|---|---|
| 收集 | Gather | uL ← u[i],uR ← u[j] | uL ← u[i], uR ← u[j] | vL ← v[i],vR ← v[j] | vL ← v[i], vR ← v[j] | h_b ← w[i] − w[j] | |
| 面上计算 | Computation on the face | h ← flux(uL, uR) | h_d ← cL·vL + cR·vR | (uL_b, uR_b) ← (cL·h_b, cR·h_b) | |||
| 分发 | Scatter | r[i] += h,r[j] −= h | r[i] += h, r[j] −= h | r_d[i] += h_d,r_d[j] −= h_d | r_d[i] += h_d, r_d[j] −= h_d | y[i] += uL_b,y[j] += uR_b | y[i] += uL_b, y[j] += uR_b |
| 边界半面 | Boundary half-face | uR 由边界条件给出 | uR given by the boundary condition | vR:入流 0,出流 vL;壁面半面 h_d = 0 | vR: inflow 0, outflow vL; wall half-face h_d = 0 | 不减 w[j];出流把 uR_b 折回 y[i],入流丢弃 | w[j] not subtracted; outflow folds uR_b into y[i], inflow drops it |
| 数据方向 | Data direction | 状态 → 残差 | state → residual | 状态 → 残差 | state → residual | 残差 → 状态 | residual → state |
前向与伴随的镜像。图 9.2 把第 8 节的前向边循环和本节的伴随边循环放在一起走:每处理一个面,左边在 A 里写下它的 2×2 块(边界面只写一个对角元,壁面什么也不写),右边在 AT 里写下同样的四个数,位置是转置后的。走完所有的面,两个矩阵互为转置,而且左边正是第 6 节显式装配的 Jacobian,面板的最后一步核对这两点。
The forward and the adjoint loop, mirrored. Figure 9.2 runs the forward edge loop of Section 8 and the adjoint edge loop of this section together: at each face the left grid receives the face's 2×2 block of A (a boundary face just one diagonal entry, a wall face nothing) and the right grid the same four numbers at the transposed positions of AT. Once every face is done, the two matrices are transposes of each other and the left one is the Jacobian assembled explicitly in Section 6; the last step of the panel checks both.
怎样确认 residual_b 写对了?利用对偶关系 R̄TṘ=ūTu̇,也就是 ⟨w,Av⟩=⟨ATw,v⟩:任取两个向量,分别调用前向和伴随函数,比较两个内积。只要有一个系数或符号写错,随机向量几乎必然让两者不相等(图 9.3)。
How do you confirm that residual_b is written correctly? Use the duality R̄TṘ=ūTu̇, that is ⟨w,Av⟩=⟨ATw,v⟩: take any two vectors, call the forward and the adjoint function, and compare the two inner products. If even one coefficient or sign is wrong, random vectors will almost certainly make the two disagree (Figure 9.3).
第 8 节的前向循环里,每个对偶面做三件线性的事。把它们写成三个小矩阵相乘,转置就成了机械的一步:顺序倒过来,每个矩阵各自转置。图 9.4 把三种循环并排画出。
In the tangent loop of Section 8 every dual face does three linear things. Written as a product of three small matrices, transposing becomes mechanical: reverse the order and transpose each factor. Figure 9.4 draws the three loops side by side.
第一步,对偶面 的三行是三个线性映射(记号与上面“局部导数怎样组成全局 Jacobian”一段相同;这里只对状态求导,网格不动,面的几何 、 随设计变化,归 ,第 12 节讲它):收集 从 取出两个状态槽位, 里随状态变的两项是 ,内部对偶面的 两行是 、;通量 对这两个槽位的两项是 ,它们把两个槽位变成一个数 ;分发 把 加到 、从 减去。所以
Step 1. The three lines of dual face are three linear maps (the notation of the block “How local derivatives form the global Jacobian” above; only the state is differentiated here, the mesh does not move, and the face geometry , changes with the design, so it belongs to , which Section 12 is about): the gather picks the two state slots out of , the two entries of that move with the state are , and for an interior dual face its two rows are and ; the two entries of for these slots are , and they turn the two slots into one number ; the scatter adds to and subtracts it from . Hence
第二步,乘积的转置要反序,,和则逐项转置:
Step 2. The transpose of a product reverses the order, , and a sum transposes term by term:
第三步,从右往左读,每个因子就是伴随循环的一行:
Step 3. Read from right to left, each factor is one line of the adjoint loop:
这就是上面表格 residual_b 一列的三个步骤行(收集、面上计算、分发;种子 ,结果 ,记号见第 8 节)。分发为什么转置成收集、取值为什么转置成带累加的分发,见本节末尾“从切向量到伴随:转置把语句顺序反过来”一段。这些数落在 与 的哪些位置,上面的镜像面板(图 9.2)逐面演示。
These are the three step rows (gather, face computation, scatter) of the residual_b column of the table above (seed , result , the notation of Section 8). Why the scatter transposes into a gather, and the gather into a scatter with accumulation, is explained in the block “From tangent to adjoint: the transpose runs the statements backwards” at the end of this section. Where these numbers land in and in , face by face, is what the mirror panel above (Figure 9.2) plays.
第四步,边界半面只改 或 ,而且都没有右节点,:
Step 4. Boundary half-faces change only or , and have no right node, :
转置之后:入流 ,第二个槽位的 被丢掉;出流 , 折回左节点;壁面什么也不做;边界上 ,没有 可减。这正是上面表格“边界半面”一行的规则。格点页的边界半面同样是 =节点、,规则一字不改。
Transposed: inflow gives and the second slot’s is dropped; outflow gives and folds back into the left node; the wall does nothing; on the boundary , with no to subtract. These are exactly the rules of the “Boundary half-face” row of the table above. The boundary half-faces of this page also have = the node and , and the rules apply word for word.
第五步,各对偶面的项相加时次序可以任意,所以伴随循环不必倒着走对偶面:需要倒序的只是每个对偶面内部的三行。本页的 residual_b 就按与 residual 相同的对偶面次序循环。
Step 5. The dual face terms can be added in any order, so the adjoint loop need not visit the dual faces backwards: only the three lines inside each dual face are reversed. This page’s residual_b loops over the dual faces in the same order as residual.
第六步,一个立刻能做的例子:。每个内部对偶面上 ,什么也不分发;只有远场半面留下 (壁面 )。所以 只在远场节点上非零。这正是第 5 节“内部通量两两抵消”的转置读法: 是 对 的导数,而 只含远场通量。
Step 6. An example to try at once: . On every interior dual face and nothing is scattered; only the far-field half-faces leave (the wall has ). So is non-zero only in far-field nodes. This is the cancellation of Section 5 read in transpose: is the derivative of with respect to , and contains only far-field fluxes.
核对:两个测试向量的点积测试 ;residual_b 与显式 的第 行逐元比较,24 行的最大差为 0; 在远场以外的节点上最大为 0(本页代码在构建时计算)。
Check: the dot-product test on two test vectors gives ; residual_b against row of the explicit , entry by entry, differs by at most 0 over all 24 rows; is at most 0 away from the far field (computed by this page’s code at build time).
h_d,种子 u̇=v)。伴随一行从右往左画:输入 w 在残差一侧,结果 y=ATw 在状态一侧;原来的分发 r[L]+=h、r[R]−=h 转置成收集 h̄=w[L]−w[R](代码写作 h_b;种子 R̄=w,结果 ū=y),原来的收集转置成两次相加的分发。最后两行是边界面的三种变体,以及本页一个真实内部面的两个局部导数。Figure 9.4 · One face loop written three ways. Boxes are stored arrays, arrows are data flow, and the colour tells primal (blue), tangent (green) and adjoint (red) apart. The first two rows run left to right: gather the two sides, compute on the face, scatter with + and −; the tangent only replaces the flux by its linearisation ḣ=cLu̇L+cRu̇R (h_d in the code, with the seed u̇=v). The adjoint row is drawn right to left: its input w sits on the residual side and its result y = ATw on the state side; the old scatter r[L] += h, r[R] −= h transposes into the gather h̄=w[L]−w[R] (h_b in the code; the seed is R̄=w and the result ū=y), and the old gather into a scatter that adds twice. The last two lines give the three boundary variants and the two local derivatives of one real interior face of this page.上面把面循环的三行转置了。第 8 节的表把一个面写成了完整的程序(含几何与目标),它的伴随也可以逐句写出。规则只有一条:前向的一句 ,在反向程序里变成对它的每个输入各一句 ;各句按相反的顺序执行;所有横线从 0 开始,只累加。
Above, the three lines of the face loop were transposed. The table of Section 8 writes one face as a complete program (geometry and objective included), and its adjoint can be written statement by statement too. There is a single rule: a forward statement becomes, in the reverse program, one statement for each of its inputs; the statements run in the opposite order; every bar starts at 0 and only accumulates.
最后一式说:每一句的伴随就是它的转置。为什么反向程序要把各句倒过来执行、为什么每个横线量都从 0 开始并用 += 累加,以及这些关系怎样接成整个对偶面、整个循环的对偶关系(本节的点积测试检查的正是它),本节末尾的推导在“从切向量到伴随:转置把语句顺序反过来”与“对偶恒等式的逐步证明,以及它的数值形式:点积测试”两段里,对一个对偶面的四条语句讲清楚了。本页被几句同时读到的变量有:n 被 与 读到(壁面半面上没有这两句,n 只被目标那一句读到),出流半面上 被两个槽位读到,每个节点被以它为端点的所有面读到,也被含有它的单元的所有其他对偶面读到(经单元中心)。
The last relation says that the adjoint of a statement is its transpose. Why the reverse program runs the statements backwards, why every bar starts at 0 and accumulates with +=, and how these relations chain into the duality of the whole dual face and the whole loop (the identity that the dot-product test of this section checks) is explained for the four statements of a dual face in the derivation at the end of this section, under “From tangent to adjoint: the transpose runs the statements backwards” and “The duality identity step by step, and its numerical form: the dot-product test”. On this page the variables read by several statements are: n, by and by (a wall half-face has neither statement, and there n is read only by the objective); , by both slots on an outflow half-face; each node, by every face that ends at it and by every other dual face of each cell that contains it (through the cell centre).
| # | # | 切线语句 | tangent statement | 伴随语句(倒序;x_b 即 x̄) | adjoint statement (reverse order; x_b is x̄) |
|---|---|---|---|---|---|
| 11(壁面) | 11 (wall) | L_d += uL·(n·e)·uL_d + uL²·(e·n_d)/2 | u_b[i] += uL·(n·e)·L_b; n_b += uL²·e·L_b/2 | ||
| 10 | r_d[i] += h_d; r_d[j] −= h_d | h_b ← r_b[i] − r_b[j] | |||
| 9 | h_d ← cL·uL_d + cR·uR_d + hv·vn_d + hl·len_d | uL_b += cL·h_b; uR_b += cR·h_b; vn_b += hv·h_b; len_b += hl·h_b | |||
| 8 | uL_d ← u_d[i]; uR_d ← u_d[j] (入流 uinf_d,出流 uL_d) | uL_d ← u_d[i]; uR_d ← u_d[j] (inflow uinf_d, outflow uL_d) | u_b[i] += uL_b; u_b[j] += uR_b (入流 uinf_b += uR_b,出流 u_b[i] += uR_b) | u_b[i] += uL_b; u_b[j] += uR_b (inflow uinf_b += uR_b, outflow u_b[i] += uR_b) | |
| 7 | vn_d ← β·n_d | n_b += β·vn_b | |||
| 6 | len_d ← (n·n_d) / len | n_b += (n / len)·len_b | |||
| 5 | n_d ← sum over t of s_t·(d_d[t].y, −d_d[t].x) | d_b[t] += s_t·(−nb.y, nb.x) | |||
| 4 | d_d[t] ← xc_d[t] − m_d | xc_b[t] += d_b[t]; m_b −= d_b[t] | |||
| 3 | xc_d[t] ← mean of x_d[q], q in t | x_b[q] += xc_b[t] / |t| (q in t) | |||
| 2 | m_d ← (x_d[i] + x_d[j]) / 2 | x_b[i] += m_b / 2; x_b[j] += m_b / 2 | |||
| 1 | x_d[q] ← X_d[q] | X_b[q] += x_b[q] |
读这张表:第 10–8 行是本节的 residual_b(第 9 行里给 、 的两句除外,它们和第 7–1 行一起是第 12 节的 gradient);壁面那一行的伴随给出第 7 节的 与显式的 ;入流行里 residual_b 丢掉的 (因为 不是状态),在设计的伴随里并没有丢:它就是 (第 11 节)。
How to read the table: rows 10–8 are residual_b of this section (except the two statements of row 9 that give and , which belong, with rows 7–1, to the gradient of Section 12); the adjoint of the wall row gives the of Section 7 and the explicit ; and the that residual_b drops in the inflow row (because is not a state) is not lost in the design adjoint: it is (Section 11).
同一个例子倒过来(图 9.5)。种子 (第 11 节的伴随解)、:,,,,,两条路在 汇合,再转回节点。这个对偶面上的对偶关系:
The same example reversed (Figure 9.5). With the seeds (Section 11's adjoint solution) and : , , , , ; the two paths meet in , which is then turned back to the nodes. The duality on this dual face:
右边逐项相加得 0.0509976876594891,两边相差 0。本节末尾的“同一个面的反向扫描”(图 9.7)在另一个对偶面(面循环里第一个两侧都不在壁面上的内部面)上逐级核对同样的对偶关系,反向种子同样是 ,前向种子则是 ,不是这里的坐标种子。
The terms on the right add up to 0.0509976876594891; the two sides differ by 0. “The reverse sweep of the same face” at the end of this section (Figure 9.7) checks the same duality stage by stage on another dual face (the first interior one whose two sides are off the wall), with the same reverse seed but the forward seed instead of a coordinate.
点积测试检查的是转置本身,所以它能抓住伴随里的错,而抓不住前向与伴随共有的错。图 9.6 用随机向量对本页的代码和几个故意写错的伴随各试 200 次。
The dot-product test checks the transpose itself: it catches a mistake in the adjoint, but not one that the forward and the adjoint code share. Figure 9.6 tries this page’s code and a few deliberately wrong adjoints on 200 random pairs each.
核对:把逐句写出的伴随程序( 随机、)与本页的 residual_b 加 相比,相对差 1.4×10−16;坐标的横线与 gradient 相比 2.3×10−16; 与 相比 2.6×10−16;整个程序的对偶关系 两边的相对差 0(本页代码在构建时计算)。
Check: the adjoint program written statement by statement (random , ) against this page’s residual_b plus : relative difference 1.4×10−16; the coordinate bars against gradient: 2.3×10−16; against : 2.6×10−16; the duality of the whole program, , relative difference of its two sides 0 (computed by this page’s code at build time).
把所有残差和状态分量展平后,下面只是交换两个有限求和的次序。残差种子 是任意给定的,尚未求解伴随方程;只有选择满足第 11 节线性方程的 ,才得到该目标对应的离散伴随解。取 、,前向得到 ;以 为种子反向扫描,得到 。下式就是 AD 的对偶(点积)恒等式:
After flattening all residual and state components, the following simply exchanges two finite sums. The residual seed is arbitrary and has not solved an adjoint equation; only satisfying Section 11 gives the discrete adjoint solution for the chosen objective. With and the forward sweep gives ; the reverse sweep seeded with gives . The identity below is the duality (dot-product) identity of AD:
内部面通量向左加、向右减,所以先把这两条路径合并成通量的伴随 ;之后按通量、重构、原始状态的相反依赖顺序传播。反向语句中的 += 很重要:同一状态会被多个面和多个求积点读取,这些贡献必须累加。这里没有分部积分,也没有另行猜测连续伴随边界条件。
An interior flux is added on the left and subtracted on the right, so combine those two paths into its adjoint before reversing fluxes, reconstruction and stored states. The += is essential: multiple faces and quadrature points read the same state, and their contributions must accumulate. There is no integration by parts and no separately guessed continuous adjoint boundary condition.
第 8 节局部链里的每个切向量都有对应的伴随。反向语句按与前向相反的顺序执行,每个输入的横线量都用 += 累加:
Every tangent of the local chain of Section 8 has its adjoint. The reverse statements run in the opposite order of the forward ones, and the bar of every input accumulates with +=:
本页没有被替换的残差行:边界条件都在边界面的通量里,所以上面的面循环转置就是完整的 。
This page replaces no residual rows: the boundary conditions all sit in boundary-face fluxes, so the transposed face loop above is the complete .
一个面的切向量是四个初等 Jacobian 的乘积。乘积的转置等于各个因子的转置按相反的顺序相乘:
The tangent of one face is the product of four elementary Jacobians. The transpose of a product is the product of the transposes in the reverse order:
仍从右往左读,这就是反向扫描的语句顺序:程序最后执行的分发最先处理,最先执行的取值最后处理。先把所有伴随置零,再以 为种子逐条执行:
Read again from right to left, this is the order of the reverse sweep: the scatter, which the program ran last, comes first, and the gather, which it ran first, comes last. Zero every adjoint, seed with , and run:
面循环结束后,源项的一对也按同样的规则反过来:,,。本页代码里每一条语句的伴随语句(顺序与前向相反)也都在本节里逐句写出。
After the face loop the source is reversed by the same rule: , , . The adjoint statement of every statement of this page's code, in the reverse order, is written out in this section as well.
边界面执行同样的语句。远场入流面上 的右槽位不是存储状态,而是 ,所以 的这一行为零:(2′)中 不把它的伴随 传给 , 则把它加进 的 分量。
A boundary face runs the same statements. On a far-field inflow face the right slot of is not a stored state but , so its row of is zero: in (2′), passes none of its adjoint to , and adds it to the component of .
分发与取值互为转置。分发 把一个面通量写进两行;它的转置 从这两行读回一个数,是一次取值。取值 从全局数组读出几个状态;它的转置 把它们的伴随加回原来的位置,是一次带累加的分发。
Scatter and gather are transposes of each other. The scatter writes one face flux into two rows; its transpose reads those two rows back into one number: a gather. The gather reads a few states out of the global array; its transpose adds their adjoints back into their places: a scatter with accumulation.
为什么要先置零、再用 += 累加:程序里的一次扇出(一个值被多处读取)在反向扫描里变成扇入。一个存储状态属于它每个相邻面的 ,设计变量也进入许多面与源项;链式法则对变量的每一次使用各给出一项, 是所有这些项的和:
Why zero first and then +=: a fan-out of the program (one value read in several places) becomes a fan-in of the reverse sweep. A stored state belongs to the of every neighbouring face, and the design enters many faces and the sources; the chain rule gives one term per use of a variable, and is the sum of all these terms:
让这个和从零开始,每个 += 加上一次使用的贡献。反过来,程序里的扇入( 收集它各个面的通量)在反向扫描里变成扇出: 只被读取,不需要累加。
starts this sum at zero, and each += adds the term of one use. Conversely, a fan-in of the program ( collecting the fluxes of all its faces) becomes a fan-out of the reverse sweep: is only read and needs no accumulation.
把同一个面的前向语句与反向语句配对。每一步只用一次转置的定义 ,把一个初等 Jacobian 从内积的右边移到左边,所以内积在每一级都相同:
Pair the forward and reverse statements of the same face. Each step uses the definition of the transpose once, , to move one elementary Jacobian from the right of the inner product to the left, so the inner product is the same at every stage:
对所有面求和,再加上源项的一对 (),右边的两项正好累加成反向扫描的输出 与 :
Summed over all faces, with the pair of the source added ( with ), the two terms on the right accumulate exactly into the outputs and of the reverse sweep:
这就是 AD 的点积测试:任取种子 与 ,前向扫描算出 ,反向扫描算出 ,两边之差应在舍入误差量级;只要有一条反向语句的转置写错,差就是 的。本页在构建时用它自己的 matrix-free 前向与反向乘积( 个分量的确定性种子,设计方向取下面算例的 )做了这个测试:,,相对差 。
This is the dot-product test of AD: take any seeds and , run the forward sweep for and the reverse sweep for ; the two sides must agree to round-off, and a single wrong transpose in the reverse statements gives an difference. This page ran the test on its own matrix-free forward and reverse products at build time (deterministic seeds with components, the design direction the of the example below): and , a relative difference of .
反向种子取本页伴随求解的解 ,于是 与 是这个面对 与 的贡献。下表按反向语句的顺序列出伴随值;最后一列是每一级的内积:先是 ,然后是 ,最后是 ,切向量取第 8 节“本页一个面的算例:前向扫描”里同一个面的前向算例的值。
Seed the reverse sweep with this page's adjoint solution, , so that and are this face's contributions to and . The table lists the adjoints in the order of the reverse statements; the last column is the inner product at each stage: first , then , and last , with the tangents of the forward example of the same face, the one under “A worked example on this page: the forward sweep of one face” in Section 8.
| 语句Statement | 量Quantity | 伴随Adjoint | 内积Inner product |
|---|---|---|---|
| (0)置零(0) zero | 0 | ||
| (4′)种子(4′) seed | 0.0674 | −2.531408882×10⁻⁴ | |
| 0.06643 | |||
| (4′)取值(4′) gather | 9.632×10⁻⁴ | −2.531408882×10⁻⁴ | |
| (3′)至(1′)加回(3′)–(1′) add back | 2.635×10⁻⁴ | −2.531408882×10⁻⁴ | |
| −5.054×10⁻⁴ | |||
| 0 |
3 级内积相同,最大相对差 :对偶恒等式在这些数上成立;图 9.7 按反向语句逐条播放这些数。这个面不直接依赖 ,所以 ; 的伴随来自它真正进入的那些行。
The 3 inner products agree, with a largest relative difference of : the duality identity holds in these numbers. Figure 9.7 plays them statement by statement in reverse. This face does not depend on directly, so ; the adjoint of comes from the rows it does enter.
箭头全部反向,每个初等 Jacobian 转置,顺序相反;存储状态与设计变量处用 += 累加。下方:转置的乘积与形状。
Every arrow reversed, every elementary Jacobian transposed, in the reverse order; the stored states and the design accumulate with +=. Below: the transposed product and its shapes.
(0)置零并给种子:,; 0.0674, 0.06643。
(0) Zero and seed: , ; 0.0674, 0.06643.
(4′)分发的转置是一次取值: 9.632×10⁻⁴。内积 −2.531408882×10⁻⁴。
(4′) The transposed scatter is a gather: 9.632×10⁻⁴. Inner product −2.531408882×10⁻⁴.
(3′)通量的转置:;一阶格式的面输入就是两侧状态, 直接给出它们的伴随,面积向量那一部分交给下一步的 。
(3′) The transposed flux: ; at first order the face inputs are the two states, and is their adjoint, the area-vector part going on to in the next step.
(2′)重构的转置:,,这个面给出 0()。
(2′) The transposed reconstruction: and ; this face gives 0 ().
(1′)取值的转置是带累加的分发:, 2.635×10⁻⁴, −5.054×10⁻⁴。内积 −2.531408882×10⁻⁴。
(1′) The transposed gather is a scatter with accumulation: , 2.635×10⁻⁴, −5.054×10⁻⁴. Inner product −2.531408882×10⁻⁴.
对偶:3 级内积相同(最大相对差 9×10⁻¹⁵), 在这个面上逐级成立。
Duality: the 3 inner products agree (largest relative difference 9×10⁻¹⁵); holds stage by stage on this face.
内部面通量的反向种子仍是 (残差种子 ,横线表示伴随,主线第 8 节的记号)。新增的是面状态向梯度和中心值反传,再把梯度的伴随 传给所有最小二乘邻点。共享节点、中心和几何量上的贡献都要累加。面板逐面分解完整反向贡献,包括该面的重构链;它不是仅把旧通量的两个局部导数调换位置。
An internal face still receives the reverse seed (the residual seed is ; the bar is the adjoint, the notation of Section 8 of the main line). Trace adjoints now pass to gradients and centre values, then the gradient adjoint to all least-squares neighbours. Accumulate every path to shared nodes, centres and geometry. The panel decomposes the complete reverse contribution of each face, including reconstruction; it does not merely swap two old flux derivatives.
for face in reverse order:
H_b = w[L] - w[R]
reverse flux, boundary closure and reconstructed traces
reverse the least-squares dependency for this contribution
accumulate into all state and coordinate inputs
# Full operator uses the reverse of the same scalar graph.反向经过线性重构时,一份面迹种子分别累加到中心状态、梯度和重构偏移。反向经过 时,先解 得到 (右端是梯度的伴随),再得到 。之后沿 M 和 b 的求和定义反传到每个邻点、权重和坐标。同一个邻点可能被多个面访问,所以必须使用 +=。
Reverse a reconstructed trace by accumulating its seed into the centre state, gradient and offset. Reversing first solves for , with the gradient’s adjoint on the right, and then gives . Reverse the sums defining M and b into every neighbour, weight and coordinate. Multiple faces share neighbours, so contributions must use +=.
上式最后两项只展示固定几何时 b 的状态路径;完整形状反向还包括 M、权重、偏移和边界函数。计算磁带(tape),用最少的话说。程序算一次残差时,把每一步标量运算(加减乘除、开方、正弦)连同它的输入依次记下来,这份记录就叫磁带。前向模式沿记录的顺序传播切线(点),反向模式按相反的顺序传播伴随(横线),每一步只用这一个运算自己的解析局部导数。它就是主线第 8、9 节手写的 residual_d、residual_b 的自动版本:不保存全局 A 的元素,也不用有限差分构造 ATw。
The last two terms show only the state path through b at fixed geometry. Full shape reversal includes M, weights, offsets and boundary data. The computational tape, in a few words. While the program evaluates the residual once, it records every scalar operation (addition, subtraction, multiplication, division, square root, sine) together with its inputs, in order: that record is the tape. Forward mode propagates tangents (dots) along the record, reverse mode propagates adjoints (bars) back through it in the opposite order, each step using only that one operation’s analytic local derivative. It is the automatic version of residual_d and residual_b, written by hand in Sections 8 and 9 of the main line: it stores no entry of the global A and builds no ATw by finite differences.
第 8 节只算了乘积 Av。前向模式真正要解的是切线方程:选一个输入 α,对 R(u(α),α)=0 求导,u 随 α 的变化率 v=du/dα 满足
Section 8 only computed the product Av. What forward mode actually has to solve is the tangent equation: pick one input α and differentiate R(u(α),α)=0 with respect to it; the rate v=du/dα at which u follows α satisfies
这里取远场来流值 α=u∞。只有远场入流半面见到它(作为通量的右状态),所以 f=∂R/∂u∞ 就是这些面上通量对右槽位的导数,写进各自的左节点,共 5 个非零分量。
Here take the far-field free-stream value, α=u∞. Only the far-field inflow half-faces see it, as the right state of their flux, so f=∂R/∂u∞ is the derivative of those fluxes with respect to their right slot, written into their left nodes: 5 non-zero entries.
按第 8 节的记号,v 就是种子 α̇=1 下的切线 u̇,f 是这个种子在 u̇=0 时产生的 Ṙ。切线方程说的是收敛残差的切线为零:Ṙ=Au̇+f=0。
In the notation of Section 8, v is the tangent u̇ for the seed α̇=1, and f is the Ṙ that this seed produces with u̇=0. The tangent equation says that the tangent of the converged residual vanishes: Ṙ=Au̇+f=0.
切线方程是线性的,用第 6 节那个 Jacobi 迭代、同一个 diag(A) 求解:
The tangent equation is linear, and it is solved by the Jacobi iteration of Section 6, with the same diag(A):
每一轮只调用一次与矩阵有关的函数:第 8 节的 residual_d,算出 Av。diag(A) 是第 6 节的 diagonal 在 u* 处的值。除此之外,矩阵既不形成,也不分解:这就是 matrix-free 的前向求解。
Each sweep calls just one matrix-related function: residual_d of Section 8, which computes Av. diag(A) is what diagonal of Section 6 returns at u*. Beyond that the matrix is neither formed nor factorised: this is the forward solve, matrix-free.
在解附近,primal 迭代的误差每一轮乘以 I−diag(A)−1A,切线迭代的误差乘的正是同一个矩阵。所以 primal 一过它非线性的起始阶段,两条收敛曲线就以同一个速率下降,这个速率就是谱半径 0.797:实测 k=60 到 100 之间,两者每轮都缩小 0.7971 倍。起点不同:primal 从 u≡1 出发,切线从 v=0 出发,初始残差就是 f。图 10.1 逐轮播放切线迭代:上面一行按面播放本轮内部的 residual_d 边循环(80 个面:48 个对偶面 + 32 个边界半面,最后一步是 Jacobi 更新),下面一行按轮播放,看切线解 v 如何从远场入流边界向下游铺开。切线迭代在 k=133 时降到判据 10−13 以下,从 k=165 起落在舍入误差平台上。
Near the solution the primal iteration multiplies its error every sweep by I−diag(A)−1A, and the tangent iteration multiplies its error by exactly the same matrix. So once the primal is past its nonlinear start, the two convergence curves fall at the same rate, the spectral radius 0.797: measured between k=60 and 100, both shrink by 0.7971 per sweep. They start from different places: the primal from u≡1, the tangent from v=0, its first residual being f. Figure 10.1 plays the tangent iteration: the upper row runs face by face through the residual_d edge loop inside one sweep (80 faces: 48 dual faces plus 32 boundary half-faces, the last step being the Jacobi update), while the lower row runs sweep by sweep, showing how the tangent solution v spreads downstream from the far-field inflow boundary. The tangent iteration drops below the 10−13 criterion at k=133 and is on its round-off floor from k=165 on.
L 不直接依赖 u∞,所以
L does not depend on u∞ directly, so
本算例是 1.95162064261909;在 u∞±10−6 各重解一次 primal 的中心差分与它的相对差是 1.8×10−10。一次切线求解只回答一个输入:要得到对第 12 节 48 个坐标的导数,前向那一路要解 48 次。下一节从另一头走,一次伴随求解就得到全部。
Here it comes to 1.95162064261909; a central difference that re-solves the primal at u∞±10−6 differs from it by 1.8×10−10 (relative). One tangent solve answers one input: for the derivatives with respect to the 48 coordinates of Section 12, the forward route has to solve 48 times. The next section goes from the other end, where one adjoint solve gives them all.
第一步, 在代码里只出现在一处:入流半面的右状态 (local() 的入流分支)。所以只有入流半面的通量对它有导数,就是通量对右槽位的偏导,在 处取值:
Step 1. appears in one place in the code: the right state of the inflow half-faces (the inflow branch of local()). Only their fluxes depend on it, through the partial derivative with respect to the right slot, taken at :
第二步,按 的符号分发;入流半面只有左节点:
Step 2. Scatter with the signs of ; an inflow half-face has only a left node:
用第 5 节记号块的话说, 就是 中属于 的那一列。第三步,入流半面上 ,两项都是负的,所以 。
In the notation block of Section 5, is the column of that belongs to . Step 3: on an inflow half-face , both terms are negative, and so .
第四步, 的符号由 决定。第 6 节的 Jacobi 迭代从 出发:
Step 4. The sign of is decided by . The Jacobi iteration of Section 6 starts from :
每个迭代值都是 Neumann 级数 的部分和;谱半径 保证它收敛。只要 且 的每个元都 ,级数的每一项都是非负矩阵,于是 ,而 ,所以每个迭代值和极限都 。 的非对角元是 :它们非负,就是 的非对角元非正,也就是每个内部面上 、,即第 3 节的单调性条件。
Every iterate is a partial sum of the Neumann series , which converges because the spectral radius is . If and every entry of is , every term of the series is a non-negative matrix, so ; with , every iterate and the limit are . The off-diagonal entries of are : they are non-negative exactly when the off-diagonal entries of are non-positive, that is when and on every interior face, the monotonicity of Section 3.
第五步,本页的情况: 的最小元是 0.648,,但 有 1 个正的非对角元( 的最小元是 −0.0079),所以这个充分条件在格点页不成立。用本页的 Jacobi 逐列解出的 仍然没有负元, 也处处为正(图 10.2):条件只是充分的。
Step 5, this page: the smallest entry of is 0.648 and , but has 1 positive off-diagonal entry (the smallest entry of is −0.0079), so the sufficient condition fails on the node page. , solved column by column with this page’s Jacobi iteration, still has no negative entry, and is positive everywhere (Figure 10.2): the condition is only sufficient.
核对: 有 5 个非零元,都是负的,从 −1.876 到 −0.610,与改变 的中心差分(步长 )之差 1.8×10−11;逐列用 Jacobi 解出的 最小元 2.2×10−4; 最小 0.728(本页代码在构建时计算)。
Check: has 5 non-zeros, all negative, from −1.876 to −0.610, and differs from central differences in (step ) by 1.8×10−11; the smallest entry of , solved column by column with Jacobi, is 2.2×10−4; the smallest is 0.728 (computed by this page’s code at build time).
上面的种子是 。同一个 Jacobi 迭代对设计的任何种子都适用。取第 8 节例子里的种子 : 是壁面节点 7 的 坐标,(图 10.3)。
The seed above is . The same Jacobi iteration serves any seed of the design. Take the seed of Section 8’s example, with the coordinate of wall node 7 and (Figure 10.3).
第一步,源项:让第 8 节的前向程序在 时走一遍,得到 (即 gradient_d),切线方程的右端项是它的相反数。它只在节点 6、7、8、14、15、16 上非零:只有法向随这个节点变、又真正跑通量的对偶面才写进它;这个节点上的壁面半面没有通量,只把目标改变 。
Step 1, the source term: run the forward program of Section 8 with and obtain (gradient_d); the right-hand side of the tangent equation is its negative. It is non-zero only in nodes 6, 7, 8, 14, 15, 16: only dual faces whose normal moves with this node and that run the flux write into it; the wall half-faces at this node run no flux and change only the objective, by .
第二步,用第 6 节的 Jacobi 迭代与同一个 解切线方程:
Step 2, solve the tangent equation by the Jacobi iteration of Section 6, with the same :
第 117 轮满足 的判据。第三步,目标的切线:
which meets the criterion at sweep 117. Step 3, the tangent of the objective:
这是梯度的一个分量。 有 49 个分量,前向路线就要这样解 49 次;第 12 节的图 12.7 把它和一次伴随求解的代价放在一起。
This is one component of the gradient. has 49 components, so the forward route solves 49 times like this; Figure 12.7 in Section 12 sets that against the cost of one adjoint solve.
核对:跑满 300 轮后,把 代回整个前向程序, 最大 8.3×10−17; 与第 12 节伴随梯度的第 个分量相对差 2.4×10−16,与复数步长相对差 1.2×10−16(本页代码在构建时计算)。
Check: after all 300 sweeps, put back through the whole forward program leaves at most 8.3×10−17; differs from the -th component of Section 12’s adjoint gradient by 2.4×10−16 and from the complex step by 1.2×10−16 (relative) (computed by this page’s code at build time).
设原始残差已经收敛,且当前光滑分支上的 非奇异。取设计种子(方向),令 ;前向求解得到的状态切向量是 。对每一个残差分量逐项求导,零残差的切向量仍为零:
Assume the primal residual is converged and is nonsingular on the current smooth branch. Choose a design seed (direction) and set ; the state tangent returned by the tangent solve is . Differentiate each residual component: the tangent of a zero residual is zero.
实现分三步:状态种子 、设计种子 ,前向求值残差得到 ;用精确 的乘积求解 ;再以 和 为种子对目标前向求导,得到 。一次 只是算子调用(一次前向扫描),不是求解 。稳态导数的对象是收敛残差方程,不是 Jacobi 的迭代次数。在不可微点(如 limiter 的切换或分支判别处)以上普通导数需要另外讨论,不能无条件套用。
Implementation has three steps: seed the state with and the design with and evaluate the residual tangent ; solve for with exact products; then differentiate the objective forward with the seeds and to obtain . One is an operator application (one forward sweep), not the solution . The steady derivative concerns the converged residual equation, not the Jacobi iteration count. At nondifferentiable points (a limiter switch or a branch test) these ordinary derivatives need separate treatment.
用面循环的语句来看:右端 是 时的一次前向扫描,只有 与源项两条路径起作用;迭代中的每个 是 时的一次前向扫描。一次前向求解的全部工作就是这段面循环以不同的种子反复执行。对 个设计分量逐个求导,要做 次这样的求解。
In terms of the face loop's statements: the right-hand side is one forward sweep with , in which only the paths and the source are active, and every of the iteration is one forward sweep with . All the work of a tangent solve is this face loop run again and again with different seeds, and the derivatives with respect to all design components take such solves.
对收敛方程 R(u(D),D)=0 求导,得到 :设计种子 的切线 使收敛残差的切线为零。这里不是给一个任意 v 做一次算子乘积,而是求出整个敏感度场。求解后用 与伴随梯度的同一分量比较。
Differentiating R(u(D),D)=0 gives : for the design seed the tangent keeps the converged residual's tangent at zero. This solves a sensitivity field rather than applying the tangent once to an arbitrary v. Compare with the corresponding adjoint gradient component.
伴随方程 ATψ=−g 的右端项 g=(∂L/∂u)T 只在壁面节点(1–8 号)上非零。它同样用第 6 节的 Jacobi 迭代、同一个 diag(A) 求解(AT 与 A 的对角元相同):
The right-hand side of the adjoint equation ATψ=−g, with g=(∂L/∂u)T, is non-zero only on the wall nodes (1–8). It too is solved by the Jacobi iteration of Section 6, with the same diag(A) (AT has the diagonal of A):
与矩阵有关的唯一操作是一次第 9 节的 residual_b 调用:种子取 R̄=ψ,它返回 ū=ATψ,伴随方程要求的就是 ū=−g。对设计变量再反传一步,设计伴随 D̄=RDTψ+∇DL 就是梯度 dL/dD(第 12 节对 D=X 写出)。
The only matrix-related operation is one call to residual_b of Section 9: with the seed R̄=ψ it returns ū=ATψ, and the adjoint equation asks for ū=−g. One more reverse step into the design variables gives the design adjoint D̄=RDTψ+∇DL, which is the gradient dL/dD (Section 12 writes it out for D=X).
伴随解同样给出第 10 节的那个导数,不用再解任何方程:
The adjoint solution gives the derivative of Section 10 as well, with no further solve:
本算例两者的相对差是 3.4×10−16。一次伴随求解回答一个输出对所有输入的导数:同一个 ψ 也给出第 12 节对 48 个坐标的导数,那里前向那一路要解 48 次。
Here the two agree to 3.4×10−16 (relative). One adjoint solve answers the derivatives of one output with respect to every input: the same ψ also gives the derivatives with respect to the 48 coordinates of Section 12, where the forward route solves 48 times.
伴随迭代的误差每一轮乘以 I−diag(A)−1AT,它是 I−Adiag(A)−1 的转置,而
The adjoint iteration multiplies its error every sweep by I−diag(A)−1AT, the transpose of I−Adiag(A)−1, and
与 primal、切线的 diag(A)−1A 相似。转置不改变特征值,所以三个迭代矩阵的谱完全相同:进入渐近阶段以后,primal、切线、伴随三条收敛曲线以同一个速率下降,这个速率就是谱半径 0.797。实测 k=60 到 100 之间,三条曲线每轮都缩小 0.7971 倍,四位有效数字相同。图 11.1 把三条曲线画在一起,并逐轮播放伴随迭代:上面一行按面播放本轮内部的 residual_b 边循环(80 个面:48 个对偶面 + 32 个边界半面,最后一步是 Jacobi 更新),下面一行按轮播放,看伴随解 ψ 如何从物面向上游铺开,与第 10 节里切线解的走向正好相反。
is similar to the primal's and the tangent's diag(A)−1A. Transposition leaves eigenvalues unchanged, so the three iteration matrices have exactly the same spectrum: once in the asymptotic phase the primal, tangent and adjoint convergence curves fall at the same rate, the spectral radius 0.797. Measured between k=60 and 100, all three shrink by 0.7971 per sweep, the same to four digits. Figure 11.1 draws the three curves together and plays the adjoint iteration: the upper row runs face by face through the residual_b edge loop inside one sweep (80 faces: 48 dual faces plus 32 boundary half-faces, the last step being the Jacobi update), while the lower row runs sweep by sweep, showing how the adjoint solution ψ spreads upstream from the body surface, the opposite way to the tangent solution of Section 10.
diag(A) 不再是常数。若网格均匀、边界右状态都取常数,闭合性会使 Aii=εΣ|n| 对所有内部控制体相同。这里不是这样:壁面半面完全不贡献,远场出流面的导数是 vnuL,再加上混合网格本身的不均匀性,所以 Aii 逐节点不同——面板副标题里给出了最小值与最大值。
diag(A) is no longer constant. On a uniform mesh with a constant right state on every boundary, closure would make Aii=εΣ|n| the same for every interior control volume. That is not the case here: wall half-faces contribute nothing at all, the derivative on a far-field outflow face is vnuL, and the hybrid mesh is itself non-uniform, so Aii differs from node to node — the panel subtitle gives the smallest and the largest value.
收敛到机器精度是什么样子。三个迭代都没有停在判据处,而是跑满 300 轮。primal、切线、伴随分别在 k=122、133、108 时降到判据 10−13 以下,但这时仍以每轮 0.797 倍的比例下降:判据只是约定的停止点,并不说明迭代已经走到头。它们按同一比例一直降到 10−16 附近,然后分别从 k=153、165、148 起落在舍入误差平台上:此后再也不超过这次运行最后三分之一里的最大值,三个平台都在 1.3×10−16 与 2.1×10−16 之间。曲线变平,才说明迭代已经收敛到机器精度。停在判据处的 ψ 与平台上的 ψ 相差 4.6×10−13。
What convergence to machine precision looks like. None of the three iterations stops at the criterion; each runs the full 300 sweeps. The primal, the tangent and the adjoint drop below the 10−13 criterion at k=122, 133 and 108, but there they are still falling by a factor of 0.797 per sweep: a criterion is an agreed place to stop, not a sign that the iteration has run its course. They keep falling at that rate down to about 10−16 and then settle onto their round-off floors from k=153, 165 and 148 on: from there they never again rise above the largest value of the run's last third, and the three floors all lie between 1.3×10−16 and 2.1×10−16. Only this flat tail shows that an iteration has converged to machine precision. The ψ at the criterion is 4.6×10−13 away from the ψ on the floor.
−ψi 表示“在节点 i 的控制体中加一个单位源项时 L 的变化率”。g 只在物面非零,ψ 却遍布上游节点:它们通过一串面通量影响物面上的载荷。
−ψi is "the rate of change of L when a unit source is added in the control volume of node i". g is non-zero only on the body surface, yet ψ spreads over the upstream nodes: they influence the load on the body through a chain of face fluxes.
伴随解本身画在图 11.2 里:右端项 只在壁面节点上, 却铺满了物体上游。
The adjoint solution itself is drawn in Figure 11.2: the right-hand side lives at the wall nodes only, yet covers the region upstream of the body.
上面说 。这是三行代数:
Above, . It is three lines of algebra:
第一个等号用伴随方程,第二个是转置的定义 ,第三个用切线方程。写成对节点的求和,两边加的是完全不同位置上的数(图 11.3): 只有 8 个壁面节点的项, 只有 5 个入流节点的项。
The first equality uses the adjoint equation, the second is the definition of the transpose, , the third the tangent equation. Written as sums over nodes, the two sides add numbers from entirely different places (Figure 11.3): has terms only in the 8 wall nodes, only in the 5 inflow nodes.
核对:,,相对差 3.4×10−16(本页代码在构建时计算)。
Check: and , a relative difference of 3.4×10−16 (computed by this page’s code at build time).
第一步,切线迭代 ,而精确解满足 。两式相减,误差每轮乘以同一个矩阵:
Step 1. The tangent iteration is , and the exact solution satisfies . Subtracting, the error is multiplied by one matrix every sweep:
第二步,伴随迭代同样:
Step 2. The adjoint iteration likewise:
第三步,改写伴随的迭代矩阵。 是对角阵,,:
Step 3. Rewrite the adjoint iteration matrix. is diagonal, and :
第四步,相似变换 不改变特征值;一个矩阵与它的转置有同一个特征多项式:
Step 4. The similarity leaves eigenvalues unchanged, and a matrix and its transpose share their characteristic polynomial:
所以两个迭代矩阵的特征值完全相同,谱半径 也相同。第五步,把误差按特征向量展开,每一轮每个分量乘以自己的特征值;轮数一多,模最大的分量占上风,误差与 成比例地下降(特征向量不完备时结论不变:)。
So the two iteration matrices have exactly the same eigenvalues and the same spectral radius . Step 5. Expanding the error in eigenvectors, every sweep multiplies each component by its eigenvalue; after enough sweeps the component of largest modulus dominates and the error falls in proportion to (without a complete set of eigenvectors the conclusion stands: ).
第六步,原始迭代 是非线性的。在 附近展开:
Step 6. The primal iteration is nonlinear. Expand it about :
因为 , 的变化只乘在已经是一阶小量的 上,是二阶项:
Because , the change of multiplies an that is already first order, which makes it second order:
进入渐近段后,原始迭代的误差也乘以同一个矩阵:三条曲线同速(图 11.4)。
Once in the asymptotic phase the primal error is multiplied by the same matrix: the three curves fall at the same rate (Figure 11.4).
核对:本页代码分别求出 与 的全部 24 个特征值,谱半径都是 0.7971(两者之差 2.9×10−15),逐个配对后最大差 3.7×10−15;第二大的模是 0.6955; 到 100 之间实测每轮缩小 0.7971、0.7971、0.7971,与 之差不超过 7.3×10−6(本页代码在构建时计算)。
Check: this page’s code computes all 24 eigenvalues of and of separately: both spectral radii are 0.7971 (they differ by 2.9×10−15) and the eigenvalues, paired off, differ by at most 3.7×10−15; the second largest modulus is 0.6955; between and 100 the measured factors per sweep are 0.7971, 0.7971 and 0.7971, within 7.3×10−6 of (computed by this page’s code at build time).
把第 9 节的反向程序对所有对偶面走一遍,种子取 、。写横线的语句有三类:
Run the reverse program of Section 9 over all dual faces with the seeds and . Three kinds of statement write bars:
第一式说,伴随方程恰好就是“状态的横线为零”。本页的例子:入流半面 f72 的左节点是 20,(就是 ),,这一句贡献 ;8 个入流半面加起来 ,与第 10 节的 相同。于是 就是 。
The first relation says that the adjoint equation is exactly “the bar of the state is zero”. On this page: the inflow half-face f72 has left node 20, (which is ) and , so this statement contributes ; over the 8 inflow half-faces, , the of Section 10. So is .
为什么,用第 8 节的语言说:整个程序的对偶关系对任何种子都成立。前向种子取某个设计方向 与它的切线解 ,则 (切线方程);反向种子取 、,则 (伴随方程)。剩下的就是
Why, in the language of Section 8: the duality of the whole program holds for any seeds. For the forward seeds take a design direction and its tangent solution ; then (the tangent equation). For the reverse seeds take and ; then (the adjoint equation). What is left is
所以 就是梯度。这与本节后面“从有限维 Lagrangian 逐步消去状态敏感度”一段是同一个论证,只是写成了点和横线。产生 的 Jacobi 迭代,图 11.1 在网格上逐轮播放。
so is the gradient. It is the argument of the block “Eliminate state sensitivity from a finite-dimensional Lagrangian” later in this section, written in dots and bars. The Jacobi iteration that produces is played on the mesh, sweep by sweep, in Figure 11.1.
核对:在 、 处,逐句写出的反向程序给出的 最大 4.2×10−16; 与 之差 4.4×10−16; 与本页 gradient 之差 2.2×10−16(本页代码在构建时计算)。
Check: at , , the reverse program written statement by statement gives of at most 4.2×10−16; differs from by 4.4×10−16; differs from this page’s gradient by 2.2×10−16 (computed by this page’s code at build time).
在满足 的解上 。先把 固定,沿切向量 、 对 前向求导,再按 和 收集系数。即使 随设计改变,额外的 项也在收敛解上为零。
At a feasible state , . Hold fixed, differentiate forward along the tangents and , then collect the coefficients of and . Even if varies with the design, the extra term vanishes at the converged state.
选择 使未知的状态敏感度前面的系数为零。于是一个目标只需求解一次伴随线性系统,再进行设计反传;总设计梯度是列向量 。注意符号来自这里的加号约定,并不是因为“伴随一定带负号”。
Choose to cancel the coefficient of the unknown state sensitivity. One objective then requires one adjoint solve followed by design reverse accumulation; the total design gradient is a column. Its sign follows from the plus convention in this Lagrangian, not from a universal negative sign for adjoints.
用反向模式的横线记号:以 为种子运行残差的反向扫描,得到 ,这就是伴随方程。再把目标自己的反向语句(种子 )累加进同一组横线量:它们给 加上 ,给 加上 ,于是 。伴随方程说的正是状态的横线量总和为零:状态不再把敏感度往下传,同一次扫描的设计伴随 就是梯度 。也可以直接检查与 tangent 的等价性:利用 ,,所以两条路线给出同一个 。更换为 会得到 ,但这只是两个离散乘子的符号约定,不能据此认定连续场 与离散向量逐点相等。
In the bar notation of reverse mode: the residual's reverse sweep seeded with returns , which is the adjoint equation. Accumulate the objective's own reverse statements (seeded with ) into the same bars: they add to and to , so . The adjoint equation says exactly that the state's total bar vanishes: the state passes no sensitivity on, and the design adjoint of the same sweep, , is the gradient . Check equivalence with the tangent: gives , so both routes give the same . Changing to yields , but that relates two discrete sign conventions; it does not prove pointwise equality with a continuous field .
精确算子决定求解哪个方程;Jacobi 迭代决定如何求解。较便宜的近似 Jacobian(例如一阶或冻结部分系数的 Jacobian)可以用于迭代修正或预处理,但不能替代精确转置。应监控真残差 。若其不为零,当前伴随与精确 tangent 的方向导数差为 ,因此只看迭代次数或预处理残差不够。
The exact operator determines the equation; the Jacobi iteration determines how it is solved. A cheaper approximate Jacobian (first order, or with some coefficients frozen) may supply corrections or preconditioning, but cannot replace the exact transpose. Monitor the true residual . If it is nonzero, the current adjoint directional derivative minus the exact tangent derivative is ; an iteration count or a preconditioned residual alone is insufficient.
同样的结论也可以不经过 Lagrangian,直接由前向求解的两个关系得到:
The same result follows without a Lagrangian, directly from the two relations of the tangent solve:
最后一项是三个因子 、、 的乘积,括号可以从两边加。前向路线先算 :它依赖方向 ,每个设计分量都要解一次以 为系数的方程,全部分量要解 次。伴随路线先算行向量 :它与 无关,只要解一次。把这个行向量记作 :
The last term is the product of three factors, , and , which can be bracketed from either side. The tangent route computes first: it depends on the direction , so every design component needs one solve with , solves for all of them. The adjoint route computes the row first: it does not depend on , so it is solved once. Call this row :
这对每个方向 都成立,所以梯度就是上面方框里的 :一次伴随求解之后,再以 为种子做一次反向扫描,就得到全部分量。两条路线的计算量(本页的 与 由本页代码数出,与页首路线图相同):
This holds for every direction , so the gradient is the boxed above: after one adjoint solve, one reverse sweep seeded with gives every component. The cost of the two routes ( and of this page, counted by its code, as in the page roadmap at the top):
| 前向(tangent)路线Tangent route | 伴随(adjoint)路线Adjoint route | |
|---|---|---|
| 每个目标的线性求解Linear solves per objective | 次,系数 , with | 1 次,系数 1, with |
| 右端Right-hand sides | ||
| 求解之后After the solves | 次前向扫描,各得一个 forward sweeps, one each | 1 次反向扫描,得到整个 1 reverse sweep, the whole |
| 一次求解给出One solve gives | 一个分量 one component | 全部 个分量all components |
| 个目标时With objectives | 仍是 次( 与目标无关)still ( does not depend on the objective) | 次 |
本页 、:前向路线要解 49 个 24 阶的线性方程组,伴随路线只解 1 个。设计变量多而目标少时(形状优化通常如此)伴随路线便宜得多;设计变量少而目标多时,前向路线更合适。
This page has and : the tangent route solves 49 linear systems of order 24, the adjoint route one. With many design variables and few objectives (the usual case in shape optimisation) the adjoint route is far cheaper; with few design variables and many objectives the tangent route is the better choice.
与主线第 8 节一样,把乘积链先从左边乘起:解一次 ATψ=−L_uT,就得到 (dL/dD)T=L_DT+R_DTψ,也就是以 为种子反传到设计变量得到的 。右端只取决于目标函数;一次求解加一次反向参数累加,就给出全部 48 个节点坐标及入流水平 u∞ 的导数。一阶用原来的 Jacobi 转置系统迭代;二阶用精确 AT 与转置 GS 预处理的 GMRES。
As in Section 8 of the main line, multiply the product chain from the left: one solve of ATψ=−L_uT gives (dL/dD)T=L_DT+R_DTψ, the design adjoint of the reverse sweep seeded with . The right-hand side depends only on the objective; one solve and one reverse parameter accumulation give the derivatives with respect to all 48 node coordinates and the inflow level u∞. First order retains Jacobi iteration of the transposed system; second order uses exact AT and transposed-GS-preconditioned GMRES.
预处理的转置,用最少的话说。正向方程用 B 作预处理,伴随方程 ATψ=… 就要用 BT:GMRES 处理的是 ATBT=(BA)T。B 是“从 z=0 出发、按单元顺序做三次 GS 扫描”这一整个线性映射,它的转置就是把同样三次扫描按相反的单元顺序、用转置后的系数执行一遍。只转置一个局部系数不够,必须转置整个映射;两者是否互为转置,可以用点积测试 wT(Bv)=(BTw)Tv 检查(check.js)。停止条件检查原系统的真残差。没有把近似 Jacobian 的转置当作精确伴随算子。
The transposed preconditioner, in a few words. If the forward equation is preconditioned with B, the adjoint equation ATψ=… is preconditioned with BT: GMRES then works with ATBT=(BA)T. B is the whole linear map “three GS sweeps in cell order, starting from z=0”; its transpose runs the same three sweeps in reverse cell order with the transposed coefficients. Transposing one local coefficient is not enough; the whole map must be transposed, and the dot-product test wT(Bv)=(BTw)Tv checks that the two are transposes (check.js). Stopping tests use the true system residual. The approximate Jacobian transpose is not substituted for the exact adjoint operator.
一阶 Jacobi 解转置系统时,对角仍是 diag(A),但非对角乘积必须调用 ATψ。二阶用上面说的 BT 预处理 AT。面板显示每次迭代的 ψ 与真残差,收敛后的梯度由一次反向参数累加得到。
For first-order transposed Jacobi, diag(A) stays the same but the product must be ATψ. Second order preconditions AT with the BT described above. The player displays each ψ iterate and true residual; a single reverse parameter accumulation then gives all gradients.
到这里为止,状态以外的输入只出现过远场来流 u∞(第 10、11 节);第 8 节只把网格坐标当作动机提了一句。真正的外形优化要的是对网格坐标 X 的导数。本节把这条链路从头到尾推一遍,前向与伴随各走一次,并说明几何项究竟藏在哪里。
So far the only input besides the state has been the free-stream value u∞ (Sections 10 and 11); Section 8 named the mesh coordinates only as motivation. Real shape optimisation needs the derivative with respect to the mesh coordinates X. This section works that chain through from end to end, once forward and once adjoint, and shows where the geometric terms actually hide.
设计变量取全部节点坐标 X(算例 24 个节点、48 个分量)。状态由 R(u,X)=0 隐式决定,目标写成 L(u,X)——两者都显式依赖坐标:残差通过面法向,目标通过物面法向。全导数是
The design variables are all the node coordinates X (24 nodes in the test case, 48 components). The state is fixed implicitly by R(u,X)=0 and the objective is written L(u,X) — and both depend explicitly on the coordinates as well: the residual through the face normals, the objective through the wall normals. The total derivative is
其中 du/dX 不能随便取:把 R(u(X),X)=0 对 X 求导,得到约束
where du/dX is not free to choose: differentiating R(u(X),X)=0 with respect to X gives the constraint
消去 du/dX 有两条路,区别只在先解哪一个:
There are two routes to eliminating du/dX, differing only in which one is solved first:
算例里前向要解 48 次线性系统,伴随只解 1 次,两条路的结果相差 4.4e−16:前向那一路的 ∂R/∂X 由 gradient_d 解析地逐列算出(见下文“转置关系照样成立”),两条路都是精确的,差别只剩舍入误差。各自与“逐分量重新求解 primal”的中心差分对照是 3.7e−10 与 3.7e−10(量级 1.032),这个量级由差分步长决定。这就是伴随方法的全部理由:代价与设计变量个数无关。
In the test case the forward route solves 48 linear systems and the adjoint only 1, and the two results differ by 4.4e−16: the ∂R/∂X of the forward route is computed analytically, column by column, by gradient_d (see “The transpose relation holds here too” below), so both routes are exact and only round-off separates them. Each agrees with central differences that re-solve the primal for every component to 3.7e−10 and 3.7e−10 respectively (on a magnitude of 1.032), a level set by the difference step. This is the whole reason for the adjoint method: its cost does not depend on the number of design variables.
这条链路和它的几何依赖范围,可以直接对照 图 12.1 来看。
This chain, and how far its geometric dependence reaches, can be read off directly from Figure 12.1.
残差对 X 的依赖全部藏在对偶面法向里。第 2 节给过它的构造:一条内部边 (i,j) 的法向由相邻单元各贡献一段,
The dependence of the residual on X hides entirely in the dual-face normals. Section 2 gave the construction: the normal of an interior edge (i,j) is assembled from one segment contributed by each adjacent cell,
其中 xc 是单元中心(顶点平均)。于是移动一个节点会同时改变它所有相邻单元的中心,进而改变这些单元里几乎每一条对偶面的法向(个别面上两个相邻单元的贡献恰好抵消,见上面图 12.1 的 (b))——格点格式的几何依赖比格心格式宽一圈,这是两种格式在几何导数上唯一的实质差别。
where xc is the cell centre (the average of its vertices). So moving one node changes the centres of all its adjacent cells at once, and with them the normal of almost every dual face in those cells (on a few faces the two adjacent cells' contributions cancel exactly — see (b) of Figure 12.1 above) — the geometric dependence of a node-centred scheme reaches one ring wider than a cell-centred one, and that is the only substantive difference between the two schemes in the geometric derivatives.
通量对法向的导数与格心版逐字相同:
The derivative of the flux with respect to the normal is word for word the same as in the cell-centred version:
差别只在怎么把 n̄ 送回节点。先把旋转转置掉,d̄=(−sn̄y,sn̄x)(s=±1 是该段的取向),再按 d=xc−m 分给两处:
The difference lies only in how n̄ is sent back to the nodes. First transpose the rotation away, d̄=(−sn̄y,sn̄x) (where s=±1 is the orientation of that segment), then distribute it to two places through d=xc−m:
前一式对该单元 t 的每个顶点 k 各做一次,它来自 xc 是顶点平均(|t| 是该单元的顶点数,三角形 3、四边形 4),后一式来自 m 是边中点。边界半面简单得多,它就是格心版的一半:nbnd=12(yb−ya,−(xb−xa)),转置回去也只差一个 12。
The first update is made once for every vertex k of that cell t; it comes from xc being the vertex average (|t| is the number of vertices of that cell, 3 for a triangle and 4 for a quadrilateral), the second from m being the edge midpoint. A boundary half-face is far simpler, being exactly half of the cell-centred version: nbnd=12(yb−ya,−(xb−xa)), and transposing it back differs only by that 12.
前向方向同样是解析的。把上面的回传倒过来走,就是 gradient_d(u, X_d):单元中心的位移是它各顶点位移的平均 ẋc,边中点的位移是 ṁ=12(ẋi+ẋj),每一段贡献 ±(ḋy,−ḋx),ḋ=ẋc−ṁ,加起来就是 ṅij;边界半面取所在边界边的一半。再算 ḣ=(∂h/∂n)·ṅ,按 h 的符号分发;壁面半面上 h≡0,只给目标函数加上 12uL2e·ṅ。它与 gradient 的关系,正是第 8、9 节 residual_d 与 residual_b 的关系。
The forward direction is analytic too. Run the transposed steps above the other way and you have gradient_d(u, X_d): a cell centre moves by the average ẋc of its vertices' displacements and an edge midpoint by ṁ=12(ẋi+ẋj), so each segment contributes ±(ḋy,−ḋx) with ḋ=ẋc−ṁ, and their sum is ṅij; a boundary half-face takes half of its boundary edge's. Then ḣ=(∂h/∂n)·ṅ is scattered with the signs of h; on a wall half-face h≡0 and only the objective gains 12uL2e·ṅ. It is to gradient what residual_d of Section 8 is to residual_b of Section 9.
L=Σ壁面12uL2(n·e) 同样显式依赖坐标,而且只在物面上:
L=Σwall12uL2(n·e) depends explicitly on the coordinates too, and only on the body surface:
投影之后它是一个常向量,比通量那一项还简单。无粘壁面上 h≡0,所以壁面对 ψT∂R/∂X 没有贡献,却对 ∂L/∂X 有贡献——两项在同一个边循环里累加,这也是 gradient(u, psi) 一次循环就能给出完整梯度的原因。
After projection it is a constant vector, simpler even than the flux term. On an inviscid wall h≡0, so the wall contributes nothing to ψT∂R/∂X while it does contribute to ∂L/∂X — the two terms accumulate in the same edge loop, which is why gradient(u, psi) delivers the complete gradient in a single pass.
两项可以分开验证。把种子取成 ψ=0:非壁面分支的 h̄ 全为零,只剩目标函数那一项,于是 gradient(u, 0) 就是 ∂L/∂X;再用 gradient(u, psi) 减掉它,剩下的正是 ψT∂R/∂X。两半分别与中心差分(u 冻结)对照:8.4e−11 与 1.0e−10。
The two terms can be verified separately. Take the seed ψ=0: every h̄ outside the wall branch vanishes and only the objective term is left, so gradient(u, 0) is ∂L/∂X; subtract it from gradient(u, psi) and what remains is exactly ψT∂R/∂X. The two halves agree with central differences (with u frozen) to 8.4e−11 and 1.0e−10.
几何算子也有前向与伴随两个方向,同样可以用点积测试卡住(对偶关系 R̄TṘ=X̄TẊ,种子 R̄=w):
The geometric operator has a forward and an adjoint direction as well, and a dot-product test pins it down just the same (the duality R̄TṘ=X̄TẊ with the seed R̄=w):
算例实测偏差 1.9e−16:前向方向是解析的 gradient_d,反向是 gradient,两边都精确,所以这是一个真正的转置检验,偏差只是舍入误差,与第 9 节状态空间的点积测试一样。gradient_d 另外单独与中心差分(u 冻结,h=10−6)对照过:相差 1.5e−10,那是差分步长的量级。注意 w 住在残差空间(24 维)、Ẋ 住在坐标空间(48 维)——两个空间维数不同,转置关系依然成立:这正是“伴随”与“转置”是同一件事的又一次体现。
The measured deviation is 1.9e−16: the forward direction is the analytic gradient_d and the adjoint direction gradient, both exact, so this is a true transpose test and the deviation is round-off, as in the state-space test of Section 9. gradient_d itself is checked separately against central differences (u frozen, h=10−6): they differ by 1.5e−10, the level of the difference step. Note that w lives in the residual space (24 dimensions) and Ẋ in the coordinate space (48 dimensions) — the two spaces have different dimensions and the transpose relation holds regardless: one more sign that "adjoint" and "transpose" are the same thing.
把整个网格刚体平移,所有法向都不变,因此 L 不变。于是梯度必须满足
Translate the whole mesh rigidly and no normal changes, so L does not change either. The gradient must therefore satisfy
(两个坐标分量各自求和为零)。这条与差分无关、与算例无关,纯粹由问题的平移不变性决定,是几何梯度最便宜的自检——不过它只查平移不变性:通量导数的符号即使写错,各分量之和照样为零。算例实测两个分量之和的最大绝对值为 7.4×10−16。
(each of the two coordinate components summing to zero on its own). This owes nothing to finite differences and nothing to this particular test case; it follows purely from the translational invariance of the problem, and it is the cheapest self-check a geometric gradient has, though it only tests translational invariance: even a sign error in the flux derivative still sums to zero. The measured largest absolute value of the two component sums is 7.4×10−16.
| 检查 | Check | 格心 | Cell-centred | 格点 | Node-centred |
|---|---|---|---|---|---|
| 显式 ∂L/∂X 对中心差分(u 冻结) | Explicit ∂L/∂X against central differences (u frozen) | 5.8e−11 | 8.4e−11 | ||
| ψT∂R/∂X 对中心差分(u 冻结) | ψT∂R/∂X against central differences (u frozen) | 3.5e−11 | 1.0e−10 | ||
解析的 (∂R/∂X)Ẋ(gradient_d)对中心差分(u 冻结) | Analytic (∂R/∂X)Ẋ (gradient_d) against central differences (u frozen) | 1.7e−10 | 1.5e−10 | ||
| 几何算子点积测试(转置;两侧都是解析的) | Dot-product test of the geometric operator (transpose; both sides analytic) | 5.6e−17 | 1.9e−16 | ||
| 前向链路对全链路中心差分 | Forward route against full-chain central differences | 2.2e−10 | 3.7e−10 | ||
| 伴随链路对全链路中心差分 | Adjoint route against full-chain central differences | 2.2e−10 | 3.7e−10 | ||
| 前向链路对伴随链路(两侧都是解析的) | Forward route against adjoint route (both analytic) | 6.9e−17 | 4.4e−16 | ||
| 平移不变性:各分量之和的最大绝对值 | Translational invariance: largest absolute component sum | 6.2e−17 | 7.4e−16 | ||
| 线性系统求解次数(前向 / 伴随) | Linear solves (forward / adjoint) | 48 / 1 | 48 / 1 |
真实求解器里还多一层。本页把所有节点坐标都当设计变量。真实外形优化的设计变量是物面参数(FFD 控制点、样条系数……),体网格由变形算法从物面推出:Xvol=G(Xsurf)。链式法则再接一段 ∂G/∂Xsurf:伴随方向就是把本节得到的 dL/dXvol 通过网格变形算法的转置送回物面。结构一点没变,只是多接一段——而且那一段同样要用点积测试单独验证。
A real solver has one more layer. This page treats every node coordinate as a design variable. In real shape optimisation the design variables are surface parameters (FFD control points, spline coefficients, …), and the volume mesh is produced from the surface by a deformation algorithm: Xvol=G(Xsurf). The chain rule then gains one more link, ∂G/∂Xsurf: in the adjoint direction the dL/dXvol obtained here is sent back to the surface through the transpose of the mesh-deformation algorithm. The structure does not change at all, there is simply one more link — and that link has to be verified separately, with a dot-product test of its own.
第一步,对偶面的法向由边中点与相邻单元的中心构成(第 2 节),它们都是节点坐标的线性函数;控制体面积 不进入 (第 2 节),所以几何只经由单元中心和边中点进入:
Step 1. A dual-face normal is built from the edge midpoint and the centres of the neighbouring cells (Section 2), all linear in the node coordinates; the control-volume areas do not enter (Section 2), so the geometry enters only through the cell centres and edge midpoints:
所以对节点 的 Jacobian 是常数矩阵: 是单元 的顶点时有一项 , 是边的端点 或 时再有一项 :
So the Jacobian with respect to node is a constant matrix: a term when is a vertex of cell , and a further when is an endpoint or of the edge:
方括号在条件成立时为 1,否则为 0。边界半面 与格心页的面一样,只多一个 。
where a bracket is 1 when its condition holds and 0 otherwise. A boundary half-face, , is the cell-centred face with an extra .
第二步,通量对法向有两条路:()与 ();、 是另外的输入,保持不变:
Step 2. The flux reaches the normal by two routes: () and (); and are separate inputs and stay fixed:
第三步,前向(gradient_d):,,再按 的符号分发。第四步,反向(gradient):(壁面 ,只加目标的 ),再乘各个 Jacobian 的转置:
Step 3, forward (gradient_d): , , scattered with the signs of . Step 4, reverse (gradient): (on the wall and only the objective’s is added), then multiply by the transpose of each Jacobian:
这正是 core_node.js 里 gradient() 的 db = [−s·nb_y, s·nb_x] 与之后的两次分发。
These are db = [−s·nb_y, s·nb_x] and the two scatters after it in gradient() of core_node.js.
第五步,每个面单独满足平移不变性:对所有节点求和,,所以一个面送回各节点的量加起来为零(图 12.2)。上面“一条必然成立的结构性检查”那一小节,就是这些面逐个求和的结果。
Step 5. Every face is translation invariant on its own: summed over all nodes, , so what one face sends back to the nodes adds up to zero (Figure 12.2). The subsection “A structural check that cannot fail” above is the sum of these, face by face.
核对:对偶面 f13 上 与通量公式对 的中心差分之差 4.4×10−11;按上面的公式逐对偶面累加,与本页 gradient 的最大差 1.1×10−16;各对偶面送出的量之和的最大模 7.8×10−18(本页代码在构建时计算)。
Check: on dual face f13, differs from central differences of the flux formula in by 4.4×10−11; accumulated dual face by dual face with the formulas above, the result differs from this page’s gradient by at most 1.1×10−16; the largest modulus of what one dual face sends out, summed, is 7.8×10−18 (computed by this page’s code at build time).
把全部对偶面加起来,得到的梯度正是两项之和(图 12.3):显式项只在壁面节点上, 铺满全场。
Adding up all dual faces gives the gradient as the sum of its two terms (Figure 12.3): the explicit term sits on the wall nodes only, covers the whole field.
第 8 节表里的第 1–5 句是对偶面法向的构造,本页的残差经由它们依赖坐标:边中点、单元中心(顶点的平均)、线段与法向都是坐标的线性函数,它们的切线与伴随在第 8、9 节的两张表里;接着是长度 与法向速度。所以在格点格式里单元中心确实进入 。单元(以及对偶控制体)的面积随坐标的变化是:
Statements 1–5 of the table in Section 8 build the dual-face normal, and through them this page’s residual depends on the coordinates: the edge midpoint, the cell centres (vertex averages), the segments and the normal are linear in the coordinates, with their tangents and adjoints in the two tables of Sections 8 and 9; then come the length and the normal speed. So on the node-centred page the cell centres do enter . The area of a cell (or of a dual control volume) moves with the coordinates as
求和遍及多边形的各条边 , 是逆时针边的外法向。后一式说,一个多边形的面积以它的边向外扫过的速率增长,每条边以两端速度的平均移动(离散几何守恒律)。
The sum runs over the edges of the polygon, and is the outward normal of a counter-clockwise edge. The last relation says that a polygon grows at the rate its edges sweep outwards, each edge moving with the mean velocity of its ends (the discrete geometric conservation law).
稳态残差里没有面积:对偶控制体的面积 只乘 对时间的导数,只在第 15 节的源项与误差范数里出现。所以 只经过法向的构造、 与 。
The steady residual has no area in it: the dual-volume area multiplies only the time derivative of and appears only in the source and the error norms of Section 15. So runs only through the construction of the normal, and .
第 8 节的例子正好走了中心那条路:种子只动节点 7,它是单元 的顶点,不是对偶面 f13 的端点,所以 ,,。反过来,两段方向相反(),所以 、:两个端点 6、14 从中点与两个中心收到的量正好抵消(两个单元的顶点数相同,都是 4 个,两份 一样大),只剩两个单元的另外 4 个顶点。
Section 8’s example takes exactly the centre path: the seed moves only node 7, a vertex of cell but not an end of dual face f13, so , and . In reverse, the two segments point opposite ways (), so and : what the two ends 6 and 14 receive through the midpoint and the two centres cancels exactly (the two cells have the same number of vertices, 4 each, so the two shares are equal), and only the other 4 vertices of the two cells are left.
核对:单元 1(顶点 1、9、10、2)的面积 0.3436,上式的 与中心差分之差 9.4×10−11;随机顶点速度下 的两种写法之差 1.1×10−16(本页代码在构建时计算)。
Check: cell 1 (vertices 1, 9, 10, 2) has area 0.3436; the above differs from central differences by 9.4×10−11, and the two forms of differ by 1.1×10−16 for random vertex velocities (computed by this page’s code at build time).
图 12.4 播放 gradient_d:种子只动一个节点的 ,状态冻结(),第 8 节的语句逐面执行。只有法向随这个节点变的那几个面有事可做;3 个可选节点分别在壁面、中间一圈与远场,远场的入流、出流半面按第 8 节第二步处理。最后一帧 的相反数就是第 10 节切线方程的右端项。
Figure 12.4 plays gradient_d: the seed moves the of one node, the state is frozen (), and the statements of Section 8 run face by face. Only the few faces whose normal moves with this node have anything to do; the 3 nodes to choose from sit on the wall, on the middle ring and on the far field, where the inflow and outflow half-faces follow Step 2 of Section 8. The negative of the last frame, , is the right-hand side of the tangent equation of Section 10.
图 12.5 反过来播放 gradient:种子 、,每个面执行第 9 节的伴随语句,把 转回它的节点,节点上的 用 += 累加。走完全部 80 个面(48 个内部对偶面与 32 个边界半面), 就是 (最后一帧与图 12.3 (c) 相同),入流槽位累加出 :一次反向扫描得到全部 49 个分量。
Figure 12.5 plays gradient backwards: with the seeds and each face runs the adjoint statements of Section 9, turns its back to its nodes, and the nodes accumulate with +=. After all 80 faces (48 interior dual faces and 32 boundary half-faces), is (the last frame is Figure 12.3 (c)) and the inflow slots have accumulated : one reverse sweep gives all 49 components.
形状梯度的用处是预言:沿方向 移动网格 , 应当变化 ,误差是 。取只动 8 个壁面节点、使 下降最快的方向(壁面节点上的 ,归一化),在移动后的网格上重解 primal(图 12.6):
What a shape gradient is for is prediction: moving the mesh by along a direction should change by , up to . Take the direction in which falls fastest when only the 8 wall nodes move ( on the wall nodes, normalised) and re-solve the primal on the moved mesh (Figure 12.6):
余项的阶是梯度对错的独立判据:梯度对, 按 下降(本页最小两步之间实测 2.000);梯度错, 只按 下降。比如只用显式项 (它沿 只给出 −0.5830),丢掉 ,实测阶是 1.000。这个检验不需要在截断与舍入之间挑步长(第 13 节),也不需要每个分量各解一次。
The order of the remainder is an independent test of the gradient: with the right gradient falls like (measured between the two smallest steps on this page: 2.000); with a wrong one, only like . Keeping, for instance, only the explicit term (which gives just −0.5830 along ) and dropping , the measured order is 1.000. The test needs no step chosen between truncation and round-off (Section 13), and no solve per component.
图 12.7 把两条路线放在同一条时间轴上,以面循环次数计。前向路线对 的 49 个分量各解一次切线方程,共 5336 次面循环;伴随路线解一次 (108 次),再加一次反向扫描,就得到同样的 49 个数。伴随路线的代价为什么不随设计变量的个数增长、什么时候前向路线反而更合适,见第 11 节“为什么一次伴随求解能代替 次前向求解”一段;有成千上万个形状参数时,增长的只有前向路线。
Figure 12.7 puts the two routes on one time axis, counted in face loops. The tangent route solves one tangent equation for each of the 49 components of , 5336 face loops in all; the adjoint route solves once (108) and adds one reverse sweep, and has the same 49 numbers. Why the cost of the adjoint route does not grow with the number of design variables, and when the tangent route is nevertheless the better one, is explained in the block “Why one adjoint solve replaces tangent solves” in Section 11; with thousands of shape parameters only the tangent route grows.
核对:图 12.4 每个种子的逐面累加与 gradient_d 相同(最大差不超过 量级);图 12.5 累加出的 与 gradient 之差 2.2×10−16;Taylor 检验的实测阶 1.000、2.000、1.000;两条路线的 49 个分量逐个相差至多 1.9×10−12(都停在判据 处)(本页代码在构建时计算)。
Check: in Figure 12.4 every seed’s face-by-face accumulation equals gradient_d (to the order of at most); the accumulated in Figure 12.5 differs from gradient by 2.2×10−16; the measured orders of the Taylor test are 1.000, 2.000 and 1.000; the 49 components of the two routes differ by at most 1.9×10−12 (both stopped at the criterion ) (computed by this page’s code at build time).
节点位置变化同时改变边法向、面长、积分点、单元中心及最小二乘权重。二阶还有重构偏移与目标边界值的路径。入流数据是固定的空间函数 u_in(x,y),因此移动入流面时不能冻结 u_in 的数值。所有这些依赖均在同一个计算图内解析求导,没有用差分近似形状梯度。
Moving nodes changes normals, lengths, quadrature locations, centres and least-squares weights. Second order adds reconstruction-offset and objective-trace paths. The inflow is a fixed spatial function u_in(x,y), so its sampled value changes when an inflow face moves. All paths are differentiated analytically in the same graph, without finite-difference shape derivatives.
导数只在当前连接关系、有效网格和入/出流符号不改变的邻域内解释。跨过 β·n=0 时边界分支会改变,不能把当前线性化外推到另一分支。
Derivatives apply locally while connectivity, mesh validity and inflow/outflow signs remain unchanged. Crossing β·n=0 changes a branch and requires a fresh linearisation.
上一节把 dL/dX 推了出来:伴随只解一次线性系统,就拿到全部分量。这一节只做一件事——验证它是对的。
The previous section derived dL/dX: the adjoint solves one linear system and gets every component. This section does one thing only — check that it is right.
判据是最朴素的那个,逐个坐标分量做中心差分:
The criterion is the plainest one there is, a central difference taken coordinate by coordinate:
关键在于 L(Xk±h) 是完整的目标函数:每次都要把网格改形、重新解一次非线性 primal、再算目标。所以这里验证的是全导数——状态对几何的响应也算在内——而不是冻结状态的偏导数。代价也正因如此:中心差分每个分量两次扰动,一共 2M 次非线性求解,而伴随只用了一次线性求解。
What matters is that L(Xk±h) is the complete objective: every evaluation deforms the mesh, solves the nonlinear primal again, and only then computes the objective. So what is verified here is the total derivative — the response of the state to the geometry included — and not a partial derivative at a frozen state. That is also what it costs: two perturbations per component, 2M nonlinear solves in all, against the single linear solve of the adjoint.
还有省一半的做法——前向差分,只往一侧扰动:
There is also a way to halve that — a forward difference, perturbing to one side only:
基准值 L(X) 全局只算一次,于是总共约 M 次非线性求解而不是 2M 次。代价是精度低一个档次:截断误差是 O(h) 而不是 O(h2)。两者的误差随步长怎么走,图 13.1 底部可以逐个变量扫描出来。
The baseline L(X) is computed once for all, so the total is about M nonlinear solves rather than 2M. The price is one order of accuracy: the truncation error is O(h) instead of O(h2). The sweep at the foot of Figure 13.1 plots how the two errors run with the step size, one variable at a time.
图 13.1 里可以自己改差分步长 h,再按“用差分验证”重算。
In Figure 13.1 the step h can be set by hand and recomputed with "Verify by differences".
步长为什么要能调。中心差分的误差有两段:截断误差约 O(h2),随 h 变小而减小;舍入误差约 O(ϵ/h)(ϵ 为机器精度),随 h 变小反而增大。两者相抵,最佳步长大致在 h∼ϵ1/3,也就是 10−5 到 10−6 量级。把 h 调到 10−3 会看到截断误差主导,调到 10−10 会看到舍入噪声主导——两头都会让“验证”看起来像是失败了。这正是差分校验最容易骗人的地方,也是点积测试(第 9 节)比它更可靠的原因:点积测试是精确恒等式,根本不含步长。
Why the step has to be adjustable. The error of a central difference has two parts: truncation, about O(h2), which shrinks as h shrinks; and round-off, about O(ϵ/h) (with ϵ the machine precision), which grows as h shrinks. Between them the best step lies roughly at h∼ϵ1/3, of order 10−5 to 10−6. Set h to 10−3 and truncation dominates; set it to 10−10 and round-off noise does — at either end the "verification" looks like a failure. This is exactly where a difference check is most deceptive, and why the dot-product test (Section 9) is the more reliable of the two: it is an exact identity, with no step size in it at all.
那么哪一侧更糟?要看离最优点多远,因为两侧的斜率根本不同:h 每放大一个数量级,截断误差涨约 100 倍(二次);h 每缩小一个数量级,舍入误差只涨约 10 倍(线性)。实测扫描里截断一侧的斜率很干净,舍入一侧则起伏不定(舍入误差本身近乎随机);中心差分的最小值多数落在 h 约 10−4 到 10−6 之间,与 ϵ1/3 的量级一致。于是离最优点越远,偏大的那一侧通常越吃亏,但对个别分量两侧可能相差不到一个数量级。实用结论是——宁可把 h 取偏小,也不要取偏大。
So which side is worse? It depends how far from the optimum you are, because the two slopes are quite different: enlarging h by one order multiplies the truncation error by about 100 (quadratic), while shrinking h by one order multiplies the round-off error by only about 10 (linear). In the measured sweep the truncation slope is clean while the round-off side is ragged (round-off is close to random); the central minimum mostly lies between h≈10−4 and 10−6, consistent with the ϵ1/3 estimate. So the further from the optimum, the more the too-large side usually costs, though for a few components the two sides differ by less than an order of magnitude. The practical conclusion — better to take h a little too small than a little too large.
差分的步长两难,在第 14 节的复数步长那里不存在:那里没有相减,步长取 10−30,48 个分量与伴随梯度都相符到舍入误差。
The step dilemma of differences does not arise for the complex step of Section 14: nothing is subtracted there, the step is 10−30, and all 48 components agree with the adjoint gradient to round-off.
上面说中心差分的最优步长大致是 。它来自两个误差的平衡。第一步,截断:沿坐标 作 Taylor 展开(撇号表示对 的导数):
Above, the best central step is roughly . It comes from balancing two errors. Step 1, truncation: expand along the coordinate (primes are derivatives with respect to ):
第二步,舍入:每次算出的 都带有约 的舍入误差( 是机器精度)。两个几乎相等的数相减,这个误差不会跟着变小,再除以 (或 ),就放大成约 (前向差分约 )。第三步,两部分相加,对 求导令其为零:
Step 2, round-off: every evaluation of carries a rounding error of about ( the machine precision). Subtracting two nearly equal numbers does not shrink it, and dividing by (or ) amplifies it to about (about for the forward difference). Step 3: add the two parts, differentiate with respect to and set the result to zero:
所以中心差分的最优步长随 缩放,前向差分随 。最优点上两部分同量级;步长偏大时截断误差按 (或 )长,偏小时舍入误差按 长。
So the best central step scales like and the best forward step like . At the optimum the two parts are of the same size; too large a step grows the truncation like (or ), too small a step grows the round-off like .
第四步,代入本页的数。取第 14 节面板的默认坐标(节点 7 的 y),用 的宽差分估出 、,而 :公式给出 ,。第 14 节的步长扫描(图 14.2)按十进位取步长,中心差分实测误差最小的步长是 ,前向差分是 。
Step 4, this page’s numbers. For the default coordinate of the Section 14 panel (the y of node 7), wide differences with estimate and , and : the formulas give and . In the step sweep of Section 14 (Figure 14.2), which steps by decades, the smallest measured errors are at for the central and for the forward difference.
核对:预测与实测最优步长相差 0.60 与 0.17 个数量级;实测最小相对误差:中心差分 2.1×10−11,前向差分 5.1×10−8(本页代码在构建时计算)。
Check: predicted and measured best steps differ by 0.60 and 0.17 decades; the smallest measured relative errors are 2.1×10−11 (central) and 5.1×10−8 (forward) (computed by this page’s code at build time).
第二步的舍入项 与分量 本身的大小无关。于是在同一个步长下,所有分量的绝对误差差不多大,小分量的相对误差就大(图 13.2):本页 48 个分量的大小从 6.5×10−4 到 1.0×100, 的中心差分的绝对误差都在 2.9×10−12 到 3.7×10−10 之间,相对误差最大 3.3×10−7。这就是上面的步长扫描按各分量自己的大小取相对误差的原因。
The round-off term of Step 2 does not depend on the size of the component itself. At one step, then, all components carry absolute errors of about the same size, and small components carry large relative errors (Figure 13.2): this page’s 48 components range from 6.5×10−4 to 1.0×100 in size, the absolute errors of central differences with all lie between 2.9×10−12 and 3.7×10−10, and the largest relative error is 3.3×10−7. This is why the step sweep above measures each component’s error relative to its own size.
选择任意坐标或入流水平 u∞;每个扰动都重新计算几何、重构和边界值,再把原始方程收敛。支持中心、前向、后向差分以及 10⁻¹ 到 10⁻¹² 的步长。大步长可能改变边界分支或失去有效网格,出现这种情况应标记为不适合当前局部导数验证。
Select any coordinate or the inflow level u∞. Each perturbation rebuilds geometry, reconstruction and boundary values and reconverges the primal. Central, forward and backward differences span steps from 10⁻¹ to 10⁻¹². Large perturbations can change boundary branches or invalidate the mesh and must be flagged as unsuitable for the current local derivative check.
第 13 节的差分受步长两头夹击:截断误差要 h 小,舍入误差要 h 大。复数步长把这个两难拿掉:把一个节点坐标挪一个虚数步长 ih,整个 primal 求解用复数算,L 的虚部除以 h 就是导数:
The differences of Section 13 are squeezed from both sides: truncation wants a small h, round-off a large one. The complex step removes the dilemma: move one node coordinate by an imaginary step ih, run the whole primal solve in complex arithmetic, and the imaginary part of L, divided by h, is the derivative:
原因是 Taylor 展开:
The reason is the Taylor expansion:
实部与虚部各自分开:虚部的第一项就是 hdL/dXm,下一项已是 h3。这里没有两个相近的数相减,也就没有相消,h 可以取 10−30:O(h2) 的截断误差远在舍入误差之下。
Real and imaginary parts separate: the imaginary part starts with hdL/dXm, and its next term is already h3. No two nearly equal numbers are subtracted, so nothing cancels, and h can be 10−30: the O(h2) truncation error lies far below round-off.
把残差里的每一个实数换成复数:每个未知量有实部与虚部两个数。被挪动的是一个节点坐标 Xm,复数从面法向进入。第 12 节的对偶面法向 nij=Σt∋(i,j)±(dy,−dx) 与边界半面的法向 12(yb−ya,−(xb−xa)) 都对坐标是线性的,所以复数法向正好是
Replace every real number in the residual by a complex one: every unknown carries a real and an imaginary part. What moves is one node coordinate Xm, and the complex numbers come in through the face normals. The dual-face normals of Section 12, nij=Σt∋(i,j)±(dy,−dx), and the boundary half-face normals 12(yb−ya,−(xb−xa)) are all linear in the coordinates, so the complex normal is exactly
其中 n(em) 是同一个法向公式作用在第 m 个分量为 1、其余分量为 0 的坐标场上:它只落在被挪动节点所在单元里的对偶面上,以及以它为端点的边界边的两个半面上。vn=β·n 随之是复数。|n| 要当心:它得写成复数开方 (nx2+ny2)1/2,不能用复数的模,那是一个实数,会把虚部整个丢掉。h 这么小,这个开方就是
where n(em) is the same normal formula applied to the coordinate field that is 1 in component m and 0 in every other: it lives only on the dual faces inside the cells around the moved node and on the two half-faces of each boundary edge that ends at it. vn=β·n becomes complex with it. |n| needs care: it has to be the complex root (nx2+ny2)1/2, not the modulus of a complex number, which is real and would drop the imaginary part altogether. With h this small the root is
其余照旧。通量 14vn(uL2+uR2)−ε|n|(uR−uL) 里只有加、减、乘;Jacobi 更新里有一次复数除法,用 Smith 算法:先用分母里绝对值较大的那个分量去除,免得平方溢出。远场按 Revn 的符号分入流与出流,与实数求解完全一样:分支只看实部,导数沿选中的分支照常传下去。目标 L=Σ壁面12uL2(n·e) 也用复数的 u 与 n 来算。边循环与 Jacobi 迭代都不变,只是每个数多带一个虚部;判据要连虚部一起看:ImR/h 的均方根也要低于 10−13。从 u* 出发时实部一开始就满足判据,只看它,迭代在第 0 轮就会停下,漏掉状态的响应。伪代码在图 14.1 的最上面。
Everything else stays. The flux 14vn(uL2+uR2)−ε|n|(uR−uL) needs only addition, subtraction and multiplication; the Jacobi update has one complex division, done by Smith's algorithm, which divides by the larger of the denominator's two parts first so that no square can overflow. The far field splits into inflow and outflow by the sign of Revn, exactly as in the real solve: a branch looks at the real part only, and the derivative passes through the branch taken as usual. The objective L=Σwall12uL2(n·e) is computed from the complex u and n too. The edge loop and the Jacobi iteration stay as they are; every number just carries an imaginary part. The criterion must watch the imaginary part as well: the rms of ImR/h must fall below 10−13 too. Started at u*, the real part meets the criterion from the start, and a test on it alone would stop the iteration at sweep 0 and miss the state's response. The pseudocode is at the top of Figure 14.1.
复数迭代从哪里出发并非无关紧要。从收敛解 u* 出发(虚部为零),实部停在不动点上(到舍入误差为止)。把残差在 u* 处展开,R(u*)=0:
Where the complex iteration starts matters. Started at the converged solution u* (imaginary part zero), the real part stays at the fixed point, to round-off. Expand the residual about u*, where R(u*)=0:
于是 Jacobi 更新 u←u−diag(A)−1R(u) 的虚部除以 h 是
so the imaginary part of the Jacobi update u←u−diag(A)−1R(u), divided by h, is
这里 diag(A) 也是复数(它由同一个复数边循环收集),所以更新里那个商的虚部完整写出来是
Here diag(A) is complex as well (the same complex edge loop collects it), so the imaginary part of the quotient in the update reads in full
从 u* 出发,ReR 停在舍入误差上,第二项也只有舍入误差的量级,剩下的正是第 10 节的前向 Jacobi 迭代,只是右端从第 10 节的 f=∂R/∂u∞ 换成了这里的 f=∂R/∂Xm:v0=0,同一个 diag(A)。所以每一轮的 Imuk/h 就是前向迭代的 vk,不只是收敛后一致:48 个坐标、每一轮(共 300 轮),最大差(除以 max|v|)是 1.1e−14;实部与实数的 Jacobi 迭代逐比特相同(复数除法用 Smith 算法:虚部小到 h 的量级时,商的实部就是实数的那一次除法)。从初始流场 u≡1 出发就不同了:ReR≠0,第二项留下来,迭代把 diag(A) 也求了导,虚部是 primal 迭代值对坐标的导数:另一个序列,只在收敛时才到同一个极限。前几轮的差可达 max|v| 的 1.18 倍,而且每个分量都要把 primal 重解一遍。节点 7 的 y 从初始流场出发要 122 轮才满足判据,从 u* 出发与前向迭代一样是 120 轮。轮数相差不多,因为 primal 与前向迭代以同一个速率收敛(第 10 节);差别在每一轮算出来的是什么。
Started at u*, ReR sits at round-off, so the second term is down at round-off too, and what is left is the forward Jacobi iteration of Section 10 with this f=∂R/∂Xm in place of Section 10’s f=∂R/∂u∞: v0=0, the same diag(A). So Imuk/h is the forward iterate vk at every sweep, not only after convergence: over the 48 coordinates and every sweep (300 of them) the largest difference (over max|v|) is 1.1e−14, and the real part is the real Jacobi iterate to the last bit (the complex division is Smith's: with an imaginary part of the order of h, the real part of the quotient is the real division itself). Started at the initial field u≡1 it is different: ReR≠0, the second term stays, the iteration differentiates diag(A) as well, and the imaginary part is the derivative of the primal iterate with respect to the coordinate: another sequence, which reaches the same limit only at convergence. In the first sweeps the two differ by up to 1.18 times max|v|, and every component re-solves the primal. The y of node 7 needs 122 sweeps to meet the criterion from the initial field, and 120 from u*, the forward iteration's count. The counts are close because the primal and the forward iteration converge at the same rate (Section 10); what differs is what each sweep computes.
每个坐标一次复数求解,得到梯度的一个分量。48 个分量与伴随梯度之差的最大值(除以梯度的最大分量)是 2.2e−16,第 13 节的中心差分(h=10−6)是 3.6e−10:那里的差全是步长造成的。复数步长是精确的前向模式,所以它与伴随相符到舍入误差,是一个与步长无关的梯度检验。它的代价也是前向模式的代价:48 个分量要 48 次复数求解,伴随只要一次线性求解。所以它用来检验伴随,不用来代替伴随。在图 14.1 里选一个节点坐标和起点,看两条迭代逐轮重合或分开;图下的表把复数步长、伴随与中心差分的值并排列出。
One complex solve per coordinate gives one component of the gradient. Over the 48 components its largest difference from the adjoint gradient (over the gradient's largest component) is 2.2e−16, against 3.6e−10 for the central differences of Section 13 (h=10−6): that difference was all step size. The complex step is an exact forward mode, so it agrees with the adjoint to round-off, a gradient check whose result does not depend on the step size. Its cost is the forward mode's too: 48 components take 48 complex solves, where the adjoint takes one linear solve. So it checks the adjoint; it does not replace it. In Figure 14.1 pick a node coordinate and a start, and watch the two iterations coincide sweep by sweep, or part; the table under the plots lists the complex step, the adjoint and the central difference side by side.
第一步,残差和目标函数由加、减、乘、除和平方根组成(远场开关在分支内固定),都是实解析的; 非奇异,由隐函数定理,收敛解 也解析地依赖坐标。所以这个离散的 对 是实解析的,可以沿虚方向展开;,:
Step 1. The residual and the objective are built from additions, subtractions, multiplications, divisions and square roots (the far-field switch fixed within its branch), so both are real analytic; is non-singular, so by the implicit function theorem the converged state depends analytically on the coordinates too. This discrete is therefore real analytic in and can be expanded in an imaginary direction; and :
第二步,实部与虚部分开:
Step 2. Separate the real and imaginary parts:
第三步,对精确的复数延拓,截断误差是 ,与中心差分的截断误差一样大(第五步线性化的 另带一个 项):步长扫描里两条曲线在大步长处几乎重合(图 14.2)。第四步,区别在舍入:虚部是直接算出来的,不是两个相近的数之差,它的舍入误差约为 ,除以 后是 ,与 无关。所以 可以一直取小:本页取 ,截断项 ,远在舍入水平 之下; 又远大于双精度能表示的最小正数(约 ),不会下溢。
Step 3. For the exact complex extension the truncation error is , as large as that of the central difference (the linearised of Step 5 adds a further term): the two curves of the step sweep nearly coincide at large steps (Figure 14.2). Step 4. The difference is round-off. The imaginary part is computed directly, not as the difference of two nearly equal numbers, so its rounding error is about , and after dividing by it is , independent of . The step can therefore be made as small as we like: this page takes , where the truncation term lies far below the round-off level ; and is still far above the smallest positive double (about ), so nothing underflows.
第五步,法向长度要用复数平方根,不能用复数的模。设 (,),用 :
Step 5. The length of the normal must be the complex square root, not the modulus of a complex number. With (, ) and :
这就是上面的公式 。丢掉的项在实部是 、在虚部是 ,除以 后只影响导数到 ,与第三步同阶。复数的模 却是实数,虚部为零, 的导数就整个丢了。
This is the formula above. The dropped terms are in the real part and in the imaginary part, so after dividing by they touch the derivative only at , the order of Step 3. The modulus , in contrast, is real: its imaginary part is zero and the derivative of is lost altogether.
核对:在面 f12 上把精确的复数平方根与上面的线性化公式相比: 时实部差 2.4×10−6、虚部差除以 为 1.8×10−6, 时是 2.4×10−10 与 1.8×10−10(每缩小 100 倍降 倍,二阶), 时 0 与 0; 的复数步长与伴随梯度的相对差 2.2×10−16(本页代码在构建时计算)。
Check: on face f12, the exact complex square root against the linearised formula: at the real parts differ by 2.4×10−6 and the imaginary parts, divided by , by 1.8×10−6; at by 2.4×10−10 and 1.8×10−10 (a factor for every factor 100 in : second order); at by 0 and 0; the complex step with differs from the adjoint gradient by 2.2×10−16 relative (computed by this page’s code at build time).
独立复数运算执行同一残差、边界函数与目标,h=10⁻²⁵。基准实部决定边界分支;线性校正使 Im(R)/h 收敛,然后从 Im(L)/h 取总导数。输出不是直接返回前向算子结果。最小二乘的坐标差、矩阵求逆、长度开方和正弦边界都保留复数虚部。
Independent complex arithmetic executes the same residual, boundary function and objective with h=10⁻²⁵. Baseline real values select boundary branches. Correct the imaginary residual until Im(R)/h converges, then read Im(L)/h. This is not a returned tangent derivative. Complex parts propagate through coordinate differences, least-squares inversion, lengths and the sine boundary.
伴随给出的是离散格式的精确导数,与格式本身是几阶精度无关;格式的精度阶决定的是离散解,以及由它算出的 L 与 dL/dX,离连续问题有多远。这一节用构造解把它测出来:先选一个光滑函数当精确解,由它反推出源项与边界数据,再在一族逐次一致加密的网格上求解,看误差随网格尺寸按几次方减小。
The adjoint gives the exact derivative of the discrete scheme, whatever that scheme's order of accuracy; the order decides how far the discrete solution, and the L and dL/dX computed from it, are from the continuous problem. This section measures it with a manufactured solution: pick a smooth function as the exact solution, derive the source term and the boundary data from it, solve on a family of uniformly refined meshes, and see with what power of the mesh size the error falls.
与二维对流扩散页面用的是同一个函数。它处处介于 0.25 与 0.75 之间,与本页算例的解同号(u>0),所以对流速度 uβ·n 的符号与原问题相同,远场上入流与出流的划分也不变。它的一阶导数:
It is the same function as on the advection–diffusion pages. It lies between 0.25 and 0.75 everywhere, of the same sign as this page's solution (u>0), so the convective speed uβ·n has the same signs as in the original problem, and the far field splits into the same inflow and outflow parts. Its first derivatives:
在方程右端加一个源项 s,让 ue 恰好是它的解:
Put a source term s on the right-hand side and make ue its exact solution:
这里用到 β=(1,0.5) 是常数:∇·(12u2β)=uβ·∇u。离散时每个控制体只取源项的一个值(单点积分),从残差里减去:
This uses β=(1,0.5) being constant: ∇·(12u2β)=uβ·∇u. The discrete source takes one value per control volume (a one-point quadrature) and is subtracted from the residual:
其中 |Ωi| 是节点 i 的中位对偶控制体的面积,源项就在节点 xi 上求值。
where |Ωi| is the area of the median-dual control volume of node i, and the source is evaluated at the node xi itself.
远场照原问题按特征方向划分:入流半面的外侧状态取 ue,在半面的中点 xf 求值;出流半面照旧外推:
The far field splits by the characteristic direction as in the original problem: the outer state of an inflow half-face is ue at its midpoint xf, and an outflow half-face extrapolates as before:
物面在这一节里也按同一条规则处理:流体从物面流进计算域的地方(vn<0,特征进入计算域)给 ue;流体撞向物面的迎风一侧(vn>0,特征离开计算域)外推。于是整条边界上只在特征进入的地方给数据,构造解问题是适定的。本页算例的壁面在每个壁面半面上规定通量;为什么不能把它照搬过来,见本节后面“为什么不能照搬本页的壁面”一小节。
In this section the body follows the same rule: where the flow leaves the body into the domain (vn<0, characteristics enter the domain) the outer state is ue, and on the windward side, where the flow runs into the body (vn>0, characteristics leave the domain), the state is extrapolated. Data are then given only where characteristics enter, along the whole boundary, and the manufactured problem is well posed. The wall of this page's test case prescribes the flux on every one of its wall half-faces; why that cannot simply be carried over is the subject of the subsection “Why this page's wall cannot be carried over” below.
从 24 个单元的基础网格出发,每加密一层:每个三角形用三条边的中点分成 4 个三角形,每个四边形用四条边的中点与顶点平均点分成 4 个四边形,边界边在中点处一分为二。物面与远场都是直边八边形,所以每一层的计算域完全相同,网格尺寸逐层减半,hℓ=h0/2ℓ。7 层网格有 24、96、384、1536、6144、24576、98304 个单元,24、80、288、1088、4224、16640、66048 个节点。这就是二维对流扩散页面用的那一族网格,只是多加密了一层(到 98304 个单元、66048 个节点),原因见下文。
Starting from the 24-cell base mesh, each refinement splits every triangle into 4 triangles through its edge midpoints and every quadrilateral into 4 quadrilaterals through its edge midpoints and its vertex average, and every boundary edge in two at its midpoint. The body and the far field are both straight-edged octagons, so the domain is exactly the same on every level and the mesh size halves from one level to the next, hℓ=h0/2ℓ. The 7 meshes have 24, 96, 384, 1536, 6144, 24576, 98304 cells and 24, 80, 288, 1088, 4224, 16640, 66048 nodes. It is the family of the advection–diffusion pages, refined one level further (to 98304 cells and 66048 nodes), for a reason that comes below.
误差在节点上量,与 ue 在节点的值比较,用对偶控制体面积加权的 L2 范数与最大范数:
The error is measured at the nodes, against ue there, in the L2 norm weighted by the dual control-volume areas and in the max norm:
相邻两层的网格尺寸之比正好是 2,所以观测阶是误差之比以 2 为底的对数:
The mesh sizes of two neighbouring levels differ by exactly a factor of 2, so the observed order is the base-2 logarithm of the error ratio:
每一层都用第 6 节的 Jacobi 迭代求解:先迭代到判据满足(残差的均方根小于 10−13),比如在第 k 轮,再多跑 k 轮,这时已经落在舍入误差平台上。网格越密,Jacobi 越慢:最细一层要 3633 轮才满足判据。离散解与求解器无关,误差表不受影响。本页格式的结果:
Every level is solved by the Jacobi iteration of Section 6: iterated until the criterion is met (the rms residual below 10−13), at some sweep k, and then k sweeps more, by which time it is on its round-off floor. The finer the mesh, the slower Jacobi: the finest level takes 3633 sweeps to meet the criterion. The discrete solution does not depend on the solver, and the error table is not affected. This page's scheme:
| 层 ℓ | level ℓ | 节点数 | nodes | ‖e‖2 | p | ‖e‖∞ | p | 满足判据的轮数 | sweeps to criterion |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 24 | 6.18e−2 | — | 1.05e−1 | — | 141 | |||
| 1 | 80 | 4.46e−2 | 0.47 | 9.09e−2 | 0.21 | 273 | |||
| 2 | 288 | 3.06e−2 | 0.54 | 6.87e−2 | 0.40 | 496 | |||
| 3 | 1088 | 2.05e−2 | 0.58 | 5.09e−2 | 0.43 | 830 | |||
| 4 | 4224 | 1.31e−2 | 0.65 | 3.61e−2 | 0.49 | 1350 | |||
| 5 | 16640 | 7.88e−3 | 0.73 | 2.41e−2 | 0.58 | 2220 | |||
| 6 | 66048 | 4.48e−3 | 0.82 | 1.49e−2 | 0.70 | 3633 |
图 15.1 把两种格式的 L2 与最大范数误差画在同一张双对数图上。
Figure 15.1 puts the L2 and the max-norm errors of both schemes on one logarithmic plot.
对流通量里的耗散项可以写成扩散两点通量的样子。设 d 是从左侧节点指向右侧节点的边向量,且与 n 平行:
The dissipation in the convective flux can be written as a two-point diffusive flux. Let d be the edge vector from the left node to the right one, and let it be parallel to n:
离散格式实际上是在解多了一个扩散项、扩散系数为 O(h) 的方程,误差应当是 O(h):格式的设计精度是一阶。实测的 L2 阶是从下面一步步往 1 爬的:本页 0.47、0.54、0.58、0.65、0.73、0.82,格心页面 0.42、0.48、0.60、0.72、0.81、0.87。本页最大范数的阶 0.21、0.40、0.43、0.49、0.58、0.70 也在爬;格心页面的最大范数阶不那么整齐,先降后升。
The discrete scheme is in effect solving the equation with an added diffusion of coefficient O(h), so its error should be O(h): the design order is one. The measured L2 orders climb towards 1 from below, one level at a time: 0.47, 0.54, 0.58, 0.65, 0.73, 0.82 on this page, 0.42, 0.48, 0.60, 0.72, 0.81, 0.87 on the cell-centred page. This page's max-norm orders, 0.21, 0.40, 0.43, 0.49, 0.58, 0.70, climb too; the cell-centred page's are less orderly and fall before they rise.
为什么这么慢?把 ue 代进离散方程,每单位面积剩下的残差就是截断误差:
Why so slowly? Put ue into the discrete equations; what remains of the residual per unit area is the truncation error:
在规则网格上,一阶格式的截断误差是 O(h)。在这张不规则网格上并非如此:在本页,它的 L2 范数逐层是 1.6e−1、1.3e−1、8.9e−2、6.2e−2、4.3e−2、3.0e−2、2.1e−2,每加密一次的阶只有 0.38、0.50、0.52、0.52、0.51、0.51,大约是 h1/2;在格心页面上,它几乎不变。不规则的控制体各个面上的 Taylor 展开项在控制体内并不逐点抵消。可是解的误差照样在减小:相邻控制体的局部误差在解里互相抵消,误差仍然是一阶的(这叫超收敛)。这种抵消是逐层慢慢显现出来的,耗散越强显现得越慢:把 ε 从 0.8 换成 0.3,第 4 层(4224 个节点)上的 L2 阶就从 0.65 升到 0.82(格心页面第 4 层从 0.72 升到 0.86)。图 15.2 把截断误差与解的误差画在一起。
On a regular mesh the truncation error of a first-order scheme is O(h). On this irregular mesh it is not: here its L2 norm is 1.6e−1, 1.3e−1, 8.9e−2, 6.2e−2, 4.3e−2, 3.0e−2, 2.1e−2 level by level, the orders only 0.38, 0.50, 0.52, 0.52, 0.51, 0.51, about h1/2; on the cell-centred page it hardly changes. The Taylor terms of the faces of an irregular control volume do not cancel pointwise inside it. The solution error falls all the same: the local errors of neighbouring control volumes cancel in the solution, and the error is still first order (supraconvergence). That cancellation shows itself only gradually, level by level, and the more slowly the stronger the dissipation: with ε=0.3 instead of 0.8 the L2 order on level 4 (4224 nodes) rises from 0.65 to 0.82 (the cell-centred page, level 4: from 0.72 to 0.86). Figure 15.2 draws the truncation error beside the solution error.
本页算例的壁面在每个壁面半面上规定通量(h=0)。照搬到构造解问题上,就是在每个壁面半面上规定精确解的法向通量 12ue(xf)2vn。在流体撞向物面的迎风一侧(vn>0),特征离开计算域;这里通量 12u2vn 一旦给定,u 本身也就给定了,问题多了一个条件。这与第 4 节解释本页为什么没有 Dirichlet 壁面的,是同一个理由。
The wall of this page's test case prescribes the flux on every one of its wall half-faces (h=0). Carried over to the manufactured problem, that means prescribing the exact solution's normal flux 12ue(xf)2vn on every one of them. On the windward side, where the flow runs into the body (vn>0), characteristics leave the domain; there a given flux 12u2vn gives u itself, and the problem has one condition too many. It is the same reason Section 4 gives for this page having no Dirichlet wall.
实测:在每个壁面半面上都规定通量时,迎风侧壁面控制体上的最大误差逐层是 2.3e−2、2.7e−2、3.1e−2、5.3e−2、6.8e−2、8.1e−2,并不减小;其余地方的最大误差是 1.1e−1、9.1e−2、6.9e−2、5.1e−2、4.1e−2、4.3e−2。L2 误差仍在减小(阶 0.52、0.55、0.57、0.63、0.71),因为出问题的只是贴着迎风侧壁面的一层,这一层随网格加密越来越薄。图 15.3 把两者画在一起。
Measured: with the flux prescribed on every one of the wall half-faces, the max error on the windward wall control volumes is 2.3e−2, 2.7e−2, 3.1e−2, 5.3e−2, 6.8e−2, 8.1e−2 level by level, and does not fall; everywhere else it is 1.1e−1, 9.1e−2, 6.9e−2, 5.1e−2, 4.1e−2, 4.3e−2. The L2 error still falls (orders 0.52, 0.55, 0.57, 0.63, 0.71), because the trouble is confined to one layer against the windward wall, and that layer gets thinner with every refinement. Figure 15.3 draws the two side by side.
上面的构造解都换掉了本页的壁面条件,所以回答不了一个问题:本页自己的离散问题(每个壁面半面上 h=0,远场 u∞=1,没有源项)在网格加密时会怎样?把它放到同一族网格上,每一层都用第 6 节的 Jacobi 迭代从 u0≡1 出发求解(满足判据后再跑同样多轮),结果如下:
The manufactured problems above all replace this page's wall condition, so they cannot answer one question: what does this page's own discrete problem (h=0 on every wall half-face, u∞=1 at the far field, no source) do under refinement? On the same mesh family, every level solved by the Jacobi iteration of Section 6 from u0≡1 (and as many sweeps again after the criterion), it gives:
| 层 ℓ | level ℓ | 节点数 | nodes | Lh | 与上一层之比 | ratio to previous | 壁面最大值 | largest wall value | 满足判据的轮数 | sweeps to criterion | 每轮缩小 | factor per sweep | 符号不对的局部导数 | derivatives of the wrong sign |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 24 | 0.693 | — | 1.54 | 122 | 0.797 | 1 | |||||||
| 1 | 80 | 1.524 | 2.20 | 2.05 | 165 | 0.844 | 8 | |||||||
| 2 | 288 | 从 u0≡1 出发的 Jacobi 不收敛Jacobi from u0≡1 does not converge | ||||||||||||
Lh 从第 0 层到第 1 层增大到 2.20 倍,迎风一侧的壁面值也一层比一层大(1.54、2.05)。原因是守恒:在 h=0 的壁面上,上游沿特征线流进迎风壁面控制体的通量没有出口,只能沿着壁面、从侧面的面上被运走;网格越密,贴壁这一层越薄,侧面也越短,那里的 u 就得越大。壁面值越过第 3 节的单调性界 1.43 以后,符号不对的局部导数一层比一层多(1、8),Jacobi 每一轮的缩小倍数也向 1 靠拢(0.797、0.844)。到第 2 层(288 个节点),从 u0≡1 出发的 Jacobi 不再收敛:残差均方根在 k=5 降到最小值 7.7×10−3,随后增大,k=19 时迭代值溢出。diag(A) 直到 k=11 才出现非正元素,所以先失效的是每一轮迭代的收缩,而不是除以 diag(A)。格心页面的 Lh 是 0.412、0.804、1.557,第 3 层起 Jacobi 同样不再收敛。
Lh more than doubles from level 0 to level 1 (ratio 2.20), and the windward wall values grow level by level (1.54, 2.05). The cause is conservation: on a wall with h=0, the flux that the characteristics carry into the windward wall control volumes has no exit through the wall and can only be carried away sideways, along the wall; the finer the mesh, the thinner that layer and the shorter its side faces, so the larger u has to be there. Once the wall values are past the monotonicity bound 1.43 of Section 3, local derivatives of the wrong sign multiply level by level (1, 8), and the Jacobi factor per sweep creeps towards 1 (0.797, 0.844). On level 2 (288 nodes) the Jacobi iteration from u0≡1 no longer converges: the rms residual reaches its smallest value, 7.7×10−3, at k=5, then grows, and the iterate overflows at k=19. diag(A) has no non-positive entry until k=11, so what fails first is the contraction of each sweep, not the division by diag(A). On the cell-centred page Lh is 0.412, 0.804, 1.557, and Jacobi stops converging from level 3 on.
上一小节“在每个壁面半面上规定通量”的构造解看不到这种积累,因为它规定的是精确解的非零通量 12ue2vn:流进迎风壁面控制体的通量正好从壁面流出去,没有东西积累,那里的误差不随加密减小,但也只停在上面实测的大小。本页的 h=0 却把这份通量整个挡在了壁面上。
The manufactured problem of the previous subsection, with the flux prescribed on every wall half-face, cannot show this build-up, because the flux it prescribes is the exact solution's non-zero 12ue2vn: what flows into the windward wall control volumes leaves through the wall, nothing accumulates, and the error there does not fall with refinement, but stays at the size measured above. This page's h=0 holds that flux back at the wall entirely.
第 11–14 节验证的是:在每一张给定的网格上,伴随与几何梯度对这个离散的 Lh 精确到舍入误差,这与格式几阶无关,也与离散问题有没有连续极限无关。上面的加密表说明,本页的主线问题没有这样的极限:Lh 随加密增长而不收敛,离散解也不逼近任何连续解。原因在壁面条件(第 4 节),不在格式的阶:提高格式的阶也好,改动伴随也好,都改变不了这一点。所以本页的伴随回答的是“这张网格上的这个离散问题”的灵敏度。要让加密有意义,得换一个有连续极限的内边界:各节末尾的“新增”扩展(选读)把内边界也按特征方向处理,它的加密研究在新增的第 15、16 节和附录 B。
What Sections 11 to 14 verified is that on each given mesh the adjoint and the geometric gradient are exact, to round-off, for this discrete Lh, whatever the scheme's order and whether or not the discrete problem has a continuum limit. The refinement table above shows that the main-line problem has no such limit: Lh grows without converging, and the discrete solutions approach no continuous solution. The cause is the wall condition (Section 4), not the order of the scheme: neither raising the order nor changing the adjoint can alter that. So the page's adjoint gives the sensitivity of this discrete problem on a given mesh. For refinement to mean something, the inner boundary has to be one with a continuum limit: the optional “Added” extension at the end of each section treats the inner boundary by characteristics too, and its refinement study is in the added Sections 15 and 16 and Appendix B.
一阶、二阶使用完全相同的解析解与特征边界,没有切换成不同物理问题。每次把三角形或四边形细分为四块,保留原多边形内外边界。
Both orders use exactly the same analytic solution and characteristic boundaries; no different physical problem is substituted. Each refinement splits triangles or quadrilaterals into four while preserving polygonal boundaries.
二阶一直加密到第 4 层(4224 个节点),最后两层的实测阶是 1.97、2.37,达到设计阶 2。一阶的耗散很强,渐近区来得很晚:一直加密到第 7 层(263168 个节点),最后三层的实测阶是 0.63、0.77、0.86,逐层往 1 爬,还没有到 1;不能把尚未达到 1 的实测阶写成 1。一阶的细网格用 fast1.js 求解:它逐位复现本页一阶模型的残差、对角元与 Jacobi 迭代(check.js 核查),每一层的解都再代回本页模型的残差检验。
Second order is refined to level 4 (4224 nodes); its last two measured orders are 1.97, 2.37, reaching the design order 2. First order is strongly dissipative and reaches its asymptotic range late: refined to level 7 (263168 nodes), its last three measured orders are 0.63, 0.77, 0.86, climbing level by level but not yet 1; a measured order below one must not be relabelled one. The fine first-order levels are solved by fast1.js, which reproduces this page’s first-order residual, diagonal and Jacobi iteration bit for bit (checked in check.js); every level’s solution is put back into this page’s own residual as a check.
格点比较解析节点值;格心比较解析场的控制体平均值,采用独立体积分求积。图表显示实际数值,不用理论斜率替换测量曲线。
Compare analytic nodal values on the node grid and independently integrated control-volume averages on the cell grid. Plots show measured errors, not replacement curves with prescribed slopes.
原算例的零数值通量内壁不提供所需的光滑、正值连续参考。附录另设特征边界对照;它的加密结果不能用来证明原零通量算例的连续一致性。原算例仍需通过自己的离散转置与梯度检验。
The original zero numerical wall flux does not supply the required smooth positive continuous reference. The appendix uses a separate characteristic-boundary contrast; its refinement cannot establish continuous consistency of the original zero-flux case. The original case retains its own discrete transpose and gradient checks.
点积测试检查离散转置,差分和复数步长检查同一离散目标的梯度。独立连续参考回答另一问题:加密后是否趋向相同的连续灵敏度。出现差异时,先核对边界、目标、乘子含义、正则性和参考误差,再讨论适定性。
Dot tests check the discrete transpose; finite differences and complex steps check the gradient of the same discrete objective. An independent continuous reference asks a different question: whether refinement approaches the same continuous sensitivity. A discrepancy calls for checking boundaries, objectives, multiplier interpretation, regularity and reference error before diagnosing well-posedness.
附录 A集中给出完整推导、参考实现、图与交互;可先继续阅读离散伴随主线。
Appendix A contains the full derivation, reference implementation, figures and interaction. You can continue the discrete-adjoint lesson first.
正文使用普通数组点积定义 。若另选带权内积,算子的伴随就会改变:状态权矩阵为 、残差权矩阵为 时,带权伴随是 。代码的普通转置不能在没有换元说明的情况下被写成该带权算子。
The main derivation defines using ordinary array dot products. A weighted inner product changes the adjoint: with state and residual weights and it is . The coded ordinary transpose cannot be relabelled as that weighted operator without a change of variables.
残差的归一化也会改变乘子的数值。若改为 , 为可逆的行尺度矩阵(例如各控制体的体积),则在收敛解上 ,;目标总梯度保持不变。即使 随设计改变,由 产生的项也乘以 ,在精确收敛时为零。把控制体积分残差改成体积平均残差后,不能仍直接比较原来的乘子曲线。
Residual normalization also changes multiplier values. For with an invertible row-scaling matrix (for example the control-volume sizes), at convergence and , while the total objective gradient is unchanged. Even when depends on the design, the terms from multiply and vanish at exact convergence. Multipliers of integrated and volume-normalized residuals cannot be compared without rescaling.
连续附录从微分方程、积分目标和分部积分出发;正文从实际离散残差及其有限维链式法则出发。两者可以比较同一设计变量的目标总导数与加密极限,但还须匹配符号、变量、积分权重和边界约束。
The continuous appendix starts from differential equations, integral objectives and integration by parts; the main text starts from actual discrete residuals and finite-dimensional chain rules. Compare total derivatives of the same objective and design variable and their refinement limits, after matching signs, variables, weights and boundary constraints.
新的原始边界有明确的光滑正值参考,主线零通量问题的“不存在光滑参考”结论在这里不再适用。但光滑原始解不保证伴随处处光滑:本目标只作用于内边界,特征线终止在内边界或外边界时得到不同的伴随末端值,两类特征线的分界上伴随有跳变。附录 B 给出推导及射线追踪参考。
The new primal boundary admits a smooth positive reference; the main-line zero-flux problem’s lack of such a reference does not apply here. A smooth primal does not ensure a globally smooth adjoint: this objective acts only on the inner boundary, giving different terminal data to characteristics ending on inner versus outer boundaries, and the adjoint jumps across the interface between the two families. Appendix B derives and traces the reference.
离散点积与梯度验证检查的是当前程序,连续比较检查的是网格极限;附录 B 的表逐层给出后者。ψ_h 与射线参考之差的 L2 范数随加密减小,但减得慢:一阶第 0–4 层是 0.250、0.224、0.194、0.163、0.138,二阶是 0.236、0.163、0.116、0.099、0.078。主要的差来自两类特征线的分界:参考解在那里跳变,离散伴随把跳变抹开。这两个证据不能互相替代;尤其不能用原始解的二阶误差自动宣布伴随跨特征分界的最大范数也二阶。
Discrete dot products and gradient tests validate the program; continuous comparisons study its mesh limit, which the table in Appendix B measures level by level. The L2 norm of ψ_h minus the ray reference falls under refinement, but slowly: 0.250, 0.224, 0.194, 0.163, 0.138 at first order and 0.236, 0.163, 0.116, 0.099, 0.078 at second order, levels 0–4. Most of the difference sits at the interface between the two families of characteristics, where the reference jumps and the discrete adjoint smears the jump. Neither kind of evidence replaces the other, and second-order primal errors do not imply second-order adjoint maximum-norm convergence across characteristic interfaces.
Open ray tracing, pointwise comparison and the refinement table in Appendix B
| 原始 | Primal | 切线(前向) | Tangent (forward) | 伴随(反向) | Adjoint (reverse) | ||
|---|---|---|---|---|---|---|---|
| 函数 | Function | residual(u) | residual_d(u, v) | residual_b(u, w) | |||
| 种子 | Seed | — | u̇=v | R̄=w | |||
| 结果 | Result | R(u) | Ṙ=Av | ū=ATw | |||
| 面上计算 | Computation on the face | h=H(uL,uR) | ḣ=cLu̇L+cRu̇R | (ūL,ūR)=(cL,cR)h̄ | |||
| 边循环 | Edge loop | 收集 → 计算 → 分发 | gather → compute → scatter | 同左,数据换成扰动 | as on the left, with perturbations in place of states | 顺序倒转,收集与分发互换 | order reversed, gather and scatter exchanged |
| Jacobi 求解 | Jacobi solve | u←u−diag(A)−1R(u) | v←v−diag(A)−1(Av+f) | ψ←ψ−diag(A)−1(ATψ+g) | |||
| 种子 ei 的结果 | Result for the seed ei | — | A 的第 i 列 | column i of A | A 的第 i 行 | row i of A | |
| 能否用差分近似 | Can differences approximate it? | — | 能 | yes | 只能逐列:N 次扰动才得到 ATw;实用上反向求导 | only column by column: N perturbations give ATw; in practice, reverse differentiation |
| 单元中心 | Cell-centred | 节点中心(本页) | Node-centred (this page) | ||
|---|---|---|---|---|---|
| 未知量在哪 | Where the unknowns live | 单元中心 | cell centres | 网格节点 | mesh nodes |
| 控制体 | Control volume | 单元本身(四边形或三角形) | the cell itself (quadrilateral or triangle) | 中位对偶体(中点—中心连线围成) | the median dual, bounded by the midpoint-to-centre lines |
| 未知量个数 | Number of unknowns | 24(8 四边形 + 16 三角形) | 24 (8 quadrilaterals + 16 triangles) | 24(节点) | 24 (nodes) |
| 面 | Faces | 48 条边 | 48 edges | 48 个对偶面 + 32 个边界半面 = 80 | 48 dual faces + 32 boundary half-faces = 80 |
| 每个控制体的面数 | Faces per control volume | 3 或 4(看单元类型) | 3 or 4, depending on the cell type | 内部节点等于相邻节点数(本算例都是 5);边界节点是相邻节点数(壁面 3、远场 4)再加两个边界半面(算例中每个控制体 5 或 6 个面) | for an interior node, the number of neighbouring nodes (5 everywhere in this mesh); a boundary node has its neighbours (3 on the wall, 4 on the far field) plus its two boundary half-faces (5 or 6 faces per control volume here) |
| 对角元 diag(A) | Diagonal diag(A) | 都逐个不同(壁面不贡献、出流面导数不同) | different entry by entry in both (the wall contributes nothing, and an outflow face has a different derivative) | ||
| 边循环结构 | Structure of the loop | 完全相同:收集 → 一维通量 → 分发 | identical: gather → one-dimensional flux → scatter | ||
| 前向 / 伴随 | Forward / adjoint | 完全相同:只依赖 cL,cR 与分发符号 | identical: they depend only on cL,cR and the scatter signs | ||
flux 里面:无粘壁面取 h=0,远场按特征方向选入流/出流(壁面的 h=0 是离散闭合,第 4 节);边循环一行都不用改。flux alone: an inviscid wall takes h=0, and the far field picks inflow or outflow by the characteristic direction (the wall's h=0 is a discrete closure, Section 4); not one line of the edge loop changes.本页交互面板里的数字(收敛解、Jacobian、前向与伴随结果、梯度、差分与复数步长)都由页面中的脚本在加载时实时计算;正文、表格与静态图中引用的数值由同一份模型代码预先算出。
The numbers in the interactive panels — the converged solution, the Jacobian, the forward and adjoint results, the gradients, the differences and the complex step — are computed live by the page’s own scripts as it loads; the values quoted in the text, the tables and the static figures were computed in advance from the same model code.
保留原网格、内部通量、守恒分发与内边界积分目标的形式;必要的改动是内外边界统一特征处理、目标在入流面上用给定的 u_in,以及二阶梯度、分段积分和完整导数链。一阶保留 Jacobi;二阶采用一阶 GS 预处理的精确算子求解。全部状态和几何导数均对应当前选定的离散。
Keep the original mesh, interior flux, conservative scatter and inner-boundary functional form. The necessary changes are characteristic treatment on both boundaries, the prescribed u_in in the objective on inflow faces, second-order gradients, segment integration and complete derivative paths. First order retains Jacobi; second order solves with exact operators and first-order GS preconditioning. All state and geometry derivatives match the selected discretisation.
这里没有激波、限制器或湍流。连续目标为零,离散目标和梯度就是网格误差,加密时实测总体在减小(一阶减得慢;格点页从第 0 层到第 1 层还先增大);离散伴随也实测趋向连续参考,只是在两类特征线的分界附近收敛得慢。页面区分这种教学验证与实际工程优化目标。接下来走向更复杂方程时,应逐项增加新物理与离散算子,并保留可独立核查的导数验证。
There are no shocks, limiters or turbulence here. The continuous objective is zero, so the discrete objective and gradient are mesh error, and under refinement they are measured decreasing overall (slowly at first order; on the node page they first grow from level 0 to level 1); the discrete adjoint is measured approaching the continuous reference, slowly near the interface between the two families of characteristics. This teaching test is distinguished from an engineering optimisation objective. More complex equations should add physics and discrete operators together with independently checkable derivative tests.
(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);
/* The mesh of the advection-diffusion pages: the advection pages'
asymmetric hybrid O-mesh, node for node and cell for cell, and its
uniform subdivisions (the family the order study runs on). Shared by the
cell-centred and the node-centred core. */
(function () {
"use strict";
const key = (a, b) => Math.min(a, b) + "-" + Math.max(a, b);
/* ---- the base mesh: the advection pages' asymmetric hybrid O-mesh,
node for node and cell for cell ---- */
function baseMesh() {
const NR = 3, NT = 8;
const BODY = [0.10, -0.04], ROUT = 1.9, LAYER = 0.45;
const rBody = th => 0.55 * (1 + 0.28 * Math.cos(th) + 0.12 * Math.sin(2 * th));
const X = [], inner = [], outer = [];
for (let k = 0; k < NT; k++) {
const th = 2 * Math.PI * k / NT;
inner.push([BODY[0] + rBody(th) * Math.cos(th), BODY[1] + rBody(th) * Math.sin(th)]);
outer.push([ROUT * Math.cos(th), ROUT * Math.sin(th)]);
}
const mid = inner.map((p, k) => [(1 - LAYER) * p[0] + LAYER * outer[k][0],
(1 - LAYER) * p[1] + LAYER * outer[k][1]]);
inner.forEach(p => X.push(p));
mid.forEach(p => X.push(p));
outer.forEach(p => X.push(p));
const nid = (r, k) => r * NT + ((k % NT) + NT) % NT + 1;
const C2N = [];
for (let r = 0; r < NR - 1; r++) {
for (let k = 0; k < NT; k++) {
const a = nid(r, k), b = nid(r, k + 1), c = nid(r + 1, k + 1), d = nid(r + 1, k);
if (r === 0) C2N.push([a, d, c, b]); // quadrilateral, counter-clockwise
else { C2N.push([a, d, c]); C2N.push([a, c, b]); }
}
}
const bnd = {};
for (let k = 0; k < NT; k++) {
bnd[key(nid(0, k), nid(0, k + 1))] = "wall";
bnd[key(nid(NR - 1, k), nid(NR - 1, k + 1))] = "far";
}
return { X, C2N, bnd, BODY };
}
/* ---- uniform subdivision: every triangle into four by its edge
midpoints, every quadrilateral into four through its centre. The
boundary is polygonal, so the domain is the same at every level and the
meshes form one self-similar family. ---- */
function subdivide(mesh) {
const X = mesh.X.map(p => p.slice());
const midOf = {};
const mp = (a, b) => {
const k = key(a, b);
if (!midOf[k]) {
X.push([(X[a - 1][0] + X[b - 1][0]) / 2, (X[a - 1][1] + X[b - 1][1]) / 2]);
midOf[k] = X.length;
}
return midOf[k];
};
const C2N = [];
mesh.C2N.forEach(v => {
if (v.length === 3) {
const [a, b, c] = v, ab = mp(a, b), bc = mp(b, c), ca = mp(c, a);
C2N.push([a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]);
} else {
const [a, b, c, d] = v, ab = mp(a, b), bc = mp(b, c), cd = mp(c, d), da = mp(d, a);
X.push([0, 1].map(t => (X[a - 1][t] + X[b - 1][t] + X[c - 1][t] + X[d - 1][t]) / 4));
const m = X.length;
C2N.push([a, ab, m, da], [ab, b, bc, m], [m, bc, c, cd], [da, m, cd, d]);
}
});
const bnd = {};
Object.keys(mesh.bnd).forEach(k => {
const [a, b] = k.split("-").map(Number), m = midOf[k];
bnd[key(a, m)] = mesh.bnd[k];
bnd[key(m, b)] = mesh.bnd[k];
});
return { X, C2N, bnd };
}
window.MESHAD = { baseMesh, subdivide, key };
})();
/* Smooth scalar convection extension. Original first-order cores remain untouched.
The objective L_h = sum over inner-boundary quadrature points of (w_q/2) u_b^2 e.n uses the BOUNDARY state u_b = u+:
the prescribed inflow data u_in on inflow faces and the interior trace (u+ = u-) on outflow faces, as the continuous
derivation of Appendix B does. trace:'interior' keeps the interior trace u- on inflow faces too (the adjoint-inconsistent
variant, kept only to measure the contrast). */
(function(root){'use strict';const Q=typeof module!=='undefined'?require('../../q1d/reconstruction/math'):root.QMath,{real,complex,tape,norm,dot,gmres}=Q,zeros=n=>Array(n).fill(0);
function model(mesh,{node=false,order=2,trace='boundary'}={}){const cells=mesh.C2N.map(c=>c.map(i=>i-1)),nx=mesh.X.length,n=node?nx:cells.length,params=[...mesh.X.flat(),1],edges=[],emap=new Map();cells.forEach((c,ci)=>c.forEach((a,k)=>{const b=c[(k+1)%c.length],key=[Math.min(a,b)+1,Math.max(a,b)+1].join('-');let e=emap.get(key);if(!e){e={a,b,L:ci,R:-1,bc:mesh.bnd[key]||'interior',parts:[]};emap.set(key,e);edges.push(e);}else e.R=ci;e.parts.push({ci,sign:a===e.a?1:-1});}));const neighbors=Array.from({length:n},()=>new Set());edges.forEach(e=>{const L=node?e.a:e.L,R=node?e.b:e.R;if(R>=0){neighbors[L].add(R);neighbors[R].add(L);}});
function evaluate(o,u,p,space=order){const {c,add,sub,mul,div,pow}=o,sum=a=>a.reduce(add,c(0)),scale=(a,s)=>a.map(v=>mul(v,s)),plus=(a,b)=>a.map((v,i)=>add(v,b[i])),minus=(a,b)=>a.map((v,i)=>sub(v,b[i])),mid=(a,b)=>scale(plus(a,b),c(.5)),cross=(a,b)=>sub(mul(a[0],b[1]),mul(a[1],b[0])),dp=(a,b)=>sum(a.map((v,i)=>mul(v,b[i]))),X=Array.from({length:nx},(_,i)=>p.slice(2*i,2*i+2)),cent=cells.map(cell=>[0,1].map(k=>div(sum(cell.map(i=>X[i][k])),c(cell.length)))),points=node?X:cent;
const exact=x=>add(p[2*nx],mul(c(.2),o.sin(mul(c(Math.PI),sub(x[1],mul(c(.5),x[0]))))));
const gradients=[],lsq=[];for(let i=0;i<n;i++){let a=c(0),b=c(0),d=c(0),rx=c(0),ry=c(0);for(const j of neighbors[i]){const v=minus(points[j],points[i]),w=div(c(1),dp(v,v)),du=sub(u[j],u[i]);a=add(a,mul(w,mul(v[0],v[0])));b=add(b,mul(w,mul(v[0],v[1])));d=add(d,mul(w,mul(v[1],v[1])));rx=add(rx,mul(w,mul(v[0],du)));ry=add(ry,mul(w,mul(v[1],du)));}const det=sub(mul(a,d),mul(b,b));gradients.push(space===1?[c(0),c(0)]:[div(sub(mul(d,rx),mul(b,ry)),det),div(sub(mul(a,ry),mul(b,rx)),det)]);lsq.push([a,b,d,det]);}
const rec=(i,x)=>add(u[i],dp(gradients[i],minus(x,points[i]))),R=Array.from({length:n},()=>c(0)),vol=Array.from({length:n},()=>c(0)),integral=Array.from({length:n},()=>c(0)),sources=Array.from({length:n},()=>c(0)),faces=[];let J=c(0);
function triangle(i,a,b,d){const ar=mul(c(.5),cross(minus(b,a),minus(d,a)));vol[i]=add(vol[i],ar);const bary=[[1/3,1/3,1/3,-27/48],[.6,.2,.2,25/48],[.2,.6,.2,25/48],[.2,.2,.6,25/48]];for(const [r,s,t,w]of bary){const xx=plus(plus(scale(a,c(r)),scale(b,c(s))),scale(d,c(t))),ww=mul(c(w),ar);integral[i]=add(integral[i],mul(ww,exact(xx)));}}
cells.forEach((cell,ci)=>{if(!node){cell.forEach((a,k)=>triangle(ci,cent[ci],X[a],X[cell[(k+1)%cell.length]]));}else cell.forEach((a,k)=>{const b=cell[(k+1)%cell.length],prev=cell[(k+cell.length-1)%cell.length],m1=mid(X[a],X[b]),m0=mid(X[prev],X[a]);triangle(a,X[a],m1,cent[ci]);triangle(a,X[a],cent[ci],m0);});});
function face(L,RR,bc,a,b,sign=1,aggregate=null){const diff=minus(b,a),normal=aggregate||[mul(c(sign),diff[1]),mul(c(-sign),diff[0])],len=pow(dp(normal,normal),.5),vn=add(normal[0],mul(c(.5),normal[1])),xf=mid(a,b);const baseN=normal.map(v=>o===real?v:null);let isIn;if(o===real)isIn=(baseN[0]+.5*baseN[1])<0;else{const val=v=>o===complex?v[0]:activeTape.val[v];isIn=val(vn)<0;} // branch frozen by the real geometry at evaluation
let flux=c(0),obj=c(0),samples=[];const zs=space===1?[0]:[-1/Math.sqrt(3),1/Math.sqrt(3)];for(const z of zs){const xx=plus(xf,scale(diff,c(.5*z))),ul=rec(L,xx),ur=RR>=0?rec(RR,xx):isIn?exact(xx):ul;const h=sub(mul(c(.25),mul(vn,add(mul(ul,ul),mul(ur,ur)))),mul(c(.8),mul(len,sub(ur,ul))));flux=add(flux,mul(c(1/zs.length),h));if(bc==='wall'){const ub=trace==='interior'?ul:ur;obj=add(obj,mul(c(.5/zs.length/Math.sqrt(1.25)),mul(add(normal[0],mul(c(.5),normal[1])),mul(ub,ub))));}samples.push({x:xx,ul,ur,h});}R[L]=add(R[L],flux);if(RR>=0)R[RR]=sub(R[RR],flux);J=add(J,obj);faces.push({L,R:RR,bc,a,b,normal,mid:xf,len,vn,flux,samples});}
for(const e of edges){if(!node)face(e.L,e.R,e.bc,X[e.a],X[e.b]);else{const midp=mid(X[e.a],X[e.b]);if(space===1){let nv=[c(0),c(0)];for(const part of e.parts){const v=minus(cent[part.ci],midp);nv=plus(nv,[mul(c(part.sign),v[1]),mul(c(-part.sign),v[0])]);}face(e.a,e.b,'interior',midp,midp,1,nv);}else for(const part of e.parts)face(e.a,e.b,'interior',midp,cent[part.ci],part.sign);if(e.R<0){face(e.a,-1,e.bc,X[e.a],midp);face(e.b,-1,e.bc,midp,X[e.b]);}}}
for(let i=0;i<n;i++)R[i]=sub(R[i],sources[i]);return {R,J,faces,gradients,lsq,points,vol,sources,truth:node?points.map(exact):integral.map((v,i)=>div(v,vol[i]))};}
let activeTape;function linearize(u,p=params){const t=tape();activeTape=t;const uu=u.map(t.op.input),pp=p.map(t.op.input),out=evaluate(t.op,uu,pp),rv=a=>a.map(i=>t.val[i]);return {R:rv(out.R),J:t.val[out.J],jvp:(v,dp=zeros(p.length))=>{const d=t.forward([...v,...dp]);return {R:out.R.map(i=>d[i]),J:d[out.J],faces:out.faces.map(f=>d[f.flux]),gradients:out.gradients.map(g=>g.map(i=>d[i]))};},vjp:(w,j=0)=>{const b=t.reverse([...out.R,out.J],[...w,j]);return {u:b.slice(0,n),p:b.slice(n)};},faceVjp:(i,w)=>{const b=t.reverse([out.faces[i].flux],[w]);return {u:b.slice(0,n),p:b.slice(n)};}};}
function preconditioner(u,p=params,trans=false){const E=evaluate(real,u,p,1),rows=Array.from({length:n},()=>new Map());const put=(i,j,v)=>rows[i].set(j,(rows[i].get(j)||0)+v);for(const f of E.faces){const ul=u[f.L],ur=f.R>=0?u[f.R]:f.vn<0?f.samples[0].ur:ul,cl=.5*f.vn*ul+.8*f.len,cr=.5*f.vn*ur-.8*f.len,aa=f.R<0&&f.vn>=0?cl+cr:cl;put(f.L,f.L,aa);if(f.R>=0){put(f.L,f.R,cr);put(f.R,f.L,-aa);put(f.R,f.R,-cr);}}const P=trans?Array.from({length:n},()=>new Map()):rows;if(trans)rows.forEach((row,i)=>row.forEach((v,j)=>P[j].set(i,v)));return rhs=>{let x=zeros(n);for(let k=0;k<3;k++)for(let it=0;it<n;it++){const i=trans?n-1-it:it;let v=rhs[i];for(const [j,a]of P[i])if(j!==i)v-=a*x[j];x[i]=v/P[i].get(i);}return x;};}
function diagonal(u,p=params){const e=evaluate(real,u,p,1),d=zeros(n);for(const f of e.faces){const ul=u[f.L],ur=f.samples[0].ur,cl=.5*f.vn*ul+.8*f.len,cr=.5*f.vn*ur-.8*f.len;d[f.L]+=f.R<0&&f.vn>=0?cl+cr:cl;if(f.R>=0)d[f.R]-=cr;}return d;}
function linear(L,rhs,u,p=params,trans=false){const op=v=>trans?L.vjp(v).u:L.jvp(v).R,pre=preconditioner(u,p,trans);if(order===1){const D=diagonal(u,p),x=zeros(n),history=[],states=[];for(let it=0;it<8000;it++){const ax=op(x),r=rhs.map((v,i)=>v-ax[i]);history.push(norm(r));states.push(x.slice());if(history.at(-1)<2e-14)return {x,history,states,residual:history.at(-1)};x.forEach((v,i)=>x[i]=v+r[i]/D[i]);}throw Error('Linear Jacobi failed');}let x=zeros(n),history=[norm(rhs)],states=[x.slice()];for(let cycle=0;cycle<15;cycle++){const ax=op(x),r=rhs.map((v,i)=>v-ax[i]),g=gmres(op,r,pre,2e-14,Math.min(n,60));states.push(...g.states.slice(1).map(z=>z.map((v,i)=>v+x[i])));x=x.map((v,i)=>v+g.x[i]);history.push(...g.history.slice(1));const res=norm(op(x).map((v,i)=>v-rhs[i]));if(res<1e-13)return {x,history,states,residual:res};}throw Error('Linear solve did not converge '+history.at(-1));}
function solve(p=params,start){if(order===1){let u=start?start.slice():Array(n).fill(p[2*nx]),history=[],states=[];for(let it=0;it<10000;it++){const R=evaluate(real,u,p).R;history.push(norm(R));states.push(u.slice());if(history.at(-1)<8e-14)return {u,history,states,residual:history.at(-1)};const D=diagonal(u,p);u=u.map((v,i)=>v-R[i]/D[i]);if(!u.every(v=>v>0&&Number.isFinite(v)))throw Error('Non-positive Jacobi update');}throw Error('Primal Jacobi failed');}let u=start?start.slice():Array(n).fill(p[2*nx]),history=[],states=[];for(let it=0;it<45;it++){const L=linearize(u,p),r=norm(L.R);history.push(r);states.push(u.slice());if(r<2e-13)return {u,history,states,residual:r};const d=linear(L,L.R.map(v=>-v),u,p).x;let step=1,un;while(step>1e-8){un=u.map((v,i)=>v+step*d[i]);const ev=evaluate(real,un,p),valid=un.every(v=>v>0)&&ev.faces.every(f=>f.samples.every(s=>s.ul>0&&s.ur>0));if(valid&&norm(ev.R)<r*(1-1e-4*step))break;step*=.5;}if(step<=1e-8)throw Error('No positive smooth update');u=un;}throw Error('Nonlinear solve failed');}
function sensitivities(sol,p=params){const L=linearize(sol.u,p),g=L.vjp(zeros(n),1),s=linear(L,g.u.map(v=>-v),sol.u,p,true);return {...s,gradient:L.vjp(s.x,1).p,L};}
function cs(sol,k,p=params,h=1e-25){const L=linearize(sol.u,p),u=sol.u.map(v=>[v,0]),pp=p.map((v,i)=>[v,k===i?h:0]);for(let i=0;i<3;i++){const R=evaluate(complex,u,pp).R.map(v=>-v[1]/h);if(norm(R)<1e-13)break;const d=linear(L,R,sol.u,p).x;u.forEach((v,i)=>v[1]+=h*d[i]);}const out=evaluate(complex,u,pp);return {gradient:out.J[1]/h,residual:norm(out.R.map(v=>v[1]/h))};}
return {n,nx,node,order,mesh,params,neighbors,evaluate:(u,p=params)=>evaluate(real,u,p),linearize,solve,sensitivities,linear,cs,preconditioner};}
const api={model,...Q};if(typeof module!=='undefined')module.exports=api;else root.Transport2=api;
})(typeof window!=='undefined'?window:globalThis);
本项目以离散伴随为主线:从可逐句检查的小模型,逐步走向工业级非结构网格 RANS 的完整离散伴随实现。连续伴随在简单算例中提供独立验证与对比,不是学习后续章节的前提。
This project follows discrete adjoints, from small models whose operations can be inspected statement by statement toward a complete industrial unstructured-grid RANS discrete adjoint. Continuous adjoints provide independent checks and comparisons for simple cases; they are not a prerequisite for later chapters.
这里的“走向”是项目目标,并不表示当前已实现 RANS。可以为选定的连续 RANS 模型推导形式伴随;但不能据此假定它严格对应工业求解器中的湍流闭合、壁面处理、限幅、边界与网格算法。最终需要验证的是实际离散残差及目标的导数。
RANS is the project destination, not a capability already implemented here. A formal continuous adjoint can be derived for a specified continuous RANS model; this does not establish strict equivalence to an industrial solver’s turbulence closure, wall treatment, limiters, boundary and mesh algorithms. The derivatives to verify are those of the implemented discrete residual and objective.
连续 PDE → 线性化与分部积分 → 连续边界条件 → 独立参考场 λ
Continuous PDE → linearize and integrate by parts → dual boundary conditions → independent λ
离散残差 Rh → 精确 Jacobian → 转置求解 → 守恒行上的 −ψh
Discrete residual Rh → exact Jacobian → transpose solve → −ψh on conservation rows
将定常 PDE 写为 ,目标泛函记为 ,采用拉格朗日泛函
页面求解的是 。这里的连续乘子采用相反符号:在守恒方程上有 。这些残差已经是控制体积分后的守恒平衡,不需要再除以控制体的体积或面积。被代数边界约束替换的行对应约束乘子,其数值不是连续场的点值。
三类检查分别回答不同的问题:
有限组网格上误差下降是数值证据,不是收敛定理。即使积分范数下的误差下降,边界层或特征跳跃仍可能阻止最大范数下的收敛。
出现差异提供了诊断机会,但差异本身不能证明问题不适定。应先明确状态空间与边界迹空间、允许的数据、目标泛函以及范数。Hadamard 意义下的适定性要求解存在、唯一且连续依赖于数据。正确推导的连续伴随可以是适定的,而离散边界闭合却不一致;一个边界数据与目标泛函不匹配的适定方程,仍然是错误的伴随。约束乘子、有限正则性和参考解误差也会造成表观差异。
把原始边界条件的齐次线性化约束纳入算子 的定义域 。如果该算子是适当空间之间的有界同构,则其 Banach 伴随也是相应对偶空间之间的有界同构。这给出一种充分的局部分析框架,并不是写出形式微分表达式就能得到的结论。对于无界 PDE 算子,必须先明确其定义域及图范数,或者采用适当的弱形式。对于非线性原始问题,仅有存在唯一性还不能保证解映射可微;需要在所选解支上检查局部光滑性及线性化可逆性。
对于 ,原始算子的零模态给出必要的兼容性条件:
若 含有非零元素,则伴随一旦有解就不唯一。仅有与原始零空间正交的条件,并不能构成一般的无穷维存在性定理:还需要考虑值域闭性和稳定性。对齐次伴随边界数据,所需估计具有以下形式:
如果边界数据非齐次,还应加上相应的边界迹范数。每个网格尺度下逆算子有界也不够:在正确加权的离散范数下,稳定界必须与网格尺度无关。不考虑积分残差和控制体权重的原始欧氏条件数,本身不能作为连续问题的诊断结论。
取 ,固定原始入口值,目标变分为 。对原始线性化 使用 Green 恒等式,得到
在原二维物体算例中,法向指向流体计算域的外部,所以迎风物面是流体域的原始出流边界。一条已经携带 的特征线,不可能在这里同时满足零物理通量。首先失败的是所假设的光滑正值原始问题,而不只是某个已经建立的连续伴随的离散逼近。在假定的壁面状态 处,
对任意变分 都成立:通量约束丢失了一阶约束信息,特征速度也同时退化。把壁面条件照抄到形式伴随上,不能恢复有效的光滑解支。弱解/熵解表述需要另行分析边界迹与可微性。
对于特征边界对照,每条内部射线都会在有限路程内到达出流边界。边界数据几乎处处确定内部场;切线或角点引出的跳跃可以在允许非光滑解的空间中处理,本身不意味着不适定。特征边界附近的比值 需要兼容性分析:在这里的物面目标中,分子与分母具有可相消的法向因子,不能只看分母就断言伴随发散。
对流扩散。 正扩散系数提供椭圆主部,但这不是任意低阶项与 Robin 边界系数下的普遍可逆性证明。仍需检查齐次零空间,以及所用能量估计或最大值原理的假设。独立计算支持当前参数组,没有证明对所有参数都成立的定理。把目标所要求的壁面伴随值 1 改成 0,可能得到另一个适定问题,但会给出错误的目标灵敏度。在远场同时规定状态值和法向导数,一般会过约束。把被替换方程行的乘子当成壁面场值,则是另一类代数含义错误。
拟一维 Euler。 对所述正向亚声速分支,特征速度 解释了原始入口两个条件、出口一个条件,以及伴随的相反条件数。条件数目正确是必要条件,不是充分条件:特征传播与边界约束共同组成的边值算子还必须可逆。声速点处 奇异,当前使用 的表示失效,需要喉部兼容性/正则性分析。这不意味着所有跨声速伴随都不存在。激波还需要考虑激波位置变分及相应内部条件。这些现象超出当前数值证据的范围,可参考 Giles 与 Pierce 的连续喷管伴随分析。强、弱边界处理对数值伴随的不同影响,见 Duivesteijn 等人的研究。
因此,教学顺序应当是:先明确连续原始问题及所选解支;推导伴随,包括其边界定义域;分析存在性、唯一性和稳定性;随后才检查对该问题的一致、稳定逼近。反例应明确保留其失败的前提,不能悄悄替换模型后宣布验证成功。
实际通量是非线性的:
对状态变分 ,有
对于 ,驻值条件在原始边界迹可以自由变化的部分给出
在给定的入流边界迹上,,无需再给伴随数据。因此,伴随在原始出流边界接收数据;离开特征边界点时,有 。
原二维页面直接令物面数值通量为零,与内侧状态无关。但所述 PDE 的光滑边界迹具有物理通量
固定向量 不可能沿闭合多边形处处相切。在所有 的位置,零物理通量要求 。这会在物面的原始出流部分额外规定零值。在正值、无源分支中, 沿每条直线特征保持不变。从远场以 进入并最终到达迎风物面的特征,不可能在那里同时满足 。在 处,特征速度退化,因此不能靠把边界条件除以 来修复推导。
所以,当前零通量数值闭合不能提供上述连续伴随比较所需的光滑正值经典边值问题。这并不证明任何弱解或测度值解表述都不可能存在;这些表述需要明确的熵/边界迹规范和相应灵敏度分析,而当前页面没有建立这些理论。有限维残差、转置和离散梯度仍然有明确的定义。
仅在新增对照中,对内、外边界都按特征方向处理:在 处规定 ,在 处采用内侧物理通量。区域、内部数值通量及其 保持与原页面相同。精确原始解为 。
保留类似阻力的目标泛函
但在给定入流处,目标使用已给定的边界迹,而不是一个自由的内侧值。这种目标取值方式的改变属于对照的一部分,不会暗中应用到原算例。连续伴随为
从任一内部点沿 方向追踪射线,直到第一个边界交点:若碰到物面则取 ,若碰到外边界则取零。这是精确的特征线计算,不是离散转置求解。物面切线或角点引出的射线上存在跳跃。比较采用体积加权 L2 误差;格点范数排除边界节点。这里不宣称光滑意义下的最大范数收敛,也不以这个对照证明原零通量壁面的连续一致性。
Write the steady PDE as , the functional as , and use
The pages solve . Our continuous multiplier has the opposite sign: on conservation equations. Those residuals are integrated balances, so there is no additional division by control-volume measure. Rows replaced by algebraic boundary constraints carry constraint multipliers; their values are not point samples of the continuous field.
Three tests answer different questions:
A decreasing error on finitely many meshes is numerical evidence, not a theorem of convergence. Boundary layers or characteristic jumps can prevent maximum-norm convergence even when an integral norm decreases.
A discrepancy is a diagnostic opportunity, not by itself proof of ill-posedness. Specify the state and trace spaces, admissible data, functional, and norms first. Hadamard well-posedness requires existence, uniqueness and continuous dependence. A correctly derived continuous dual can be well posed while a discrete boundary closure is inconsistent; a well-posed equation with the wrong objective boundary data is simply the wrong dual. Constraint multipliers, limited regularity and finite reference error are other sources of apparent disagreement.
Include the homogeneous linearized primal boundary constraints in the domain of the operator . If this is a bounded isomorphism between suitable spaces, its Banach adjoint is also a bounded isomorphism between the dual spaces. This is a sufficient local framework, not a conclusion obtained merely by writing a formal differential expression. An unbounded PDE operator must first be given a domain/graph norm or a suitable weak realization. For a nonlinear primal, existence and uniqueness alone do not guarantee a differentiable solution map; the local smoothness and invertibility hypotheses must be checked on the chosen branch.
For , a primal null mode implies the necessary compatibility condition
A nonzero element of makes a solvable dual nonunique. Orthogonality to the primal kernel alone is not a general infinite-dimensional existence theorem: closed range and stability still matter. The required estimate has the form
for homogeneous dual boundary data; inhomogeneous boundary data contribute their appropriate trace norm. A bounded inverse at every mesh size is not enough either: the stability bound must be uniform in the properly weighted discrete norms. Raw Euclidean condition numbers without accounting for integrated residuals and control-volume weights are not themselves a continuum diagnosis.
For , fixed primal inflow and objective variation , the Green identity for the primal linearization gives
In the original 2-D body example, normals point out of the fluid domain: the upwind body is therefore primal outflow. A characteristic already carrying cannot satisfy zero physical flux there. This is first a failure of the assumed smooth positive primal problem, not merely a poor discrete approximation to an otherwise established dual. At a putative wall state ,
for every variation : the flux constraint loses its first-order information while the characteristic speed degenerates. Copying the wall condition onto a formal dual cannot restore a valid smooth solution branch. Weak/entropy formulations would need separate trace and differentiability analysis.
For the characteristic-boundary contrast, every interior ray reaches outflow in finite distance. Boundary data determine the field almost everywhere; tangency/corner jumps are compatible with a nonsmooth solution space and do not by themselves imply ill-posedness. The ratio requires compatibility at characteristic boundaries: in this particular body objective its numerator and denominator contain a cancelling normal factor. One must not infer blow-up from the denominator alone.
Advection–diffusion. Positive diffusion provides an elliptic principal part, not a universal invertibility proof for arbitrary lower-order and Robin coefficients. Homogeneous kernels and the assumptions of applicable energy or maximum-principle estimates still need attention. The independent solves support this parameter set; they do not prove a theorem for all parameters. Setting the wall dual to zero instead of the objective-required one can define a different well-posed problem, but yields the wrong functional sensitivity. Imposing both state and normal derivative at the far field is generally an overconstraint. Treating a replaced-row multiplier as a wall field sample is yet another, algebraic interpretation error.
Quasi-1-D Euler. The speeds explain the two inlet/one outlet primal conditions and reversed dual counts for the stated positive subsonic branch. Counts are necessary, not sufficient: propagation and the boundary constraints together must yield an invertible boundary-value operator. At a sonic point is singular, so the current representation using fails; throat compatibility/regularity analysis is required. This does not imply that all transonic adjoints fail to exist. Shocks require shock-position variations and internal conditions. These phenomena are outside the current numerical evidence; see the continuous nozzle analysis of Giles and Pierce. The distinct effects of strong/weak boundary treatment on numerical duals are discussed by Duivesteijn et al..
The teaching sequence is therefore: identify the continuous primal and chosen branch; derive the dual including its boundary domain; examine existence, uniqueness and stability; only then test consistent, stable approximation of that problem. A counterexample should retain its failed premise visibly, rather than silently substituting another model and declaring success.
The actual flux is nonlinear:
For a state variation ,
For , stationarity gives
on portions where the primal trace is free. On a prescribed inflow trace , no adjoint datum is prescribed. Thus the adjoint receives its data at primal outflow, with , away from characteristic points.
The original two-dimensional pages set the numerical body flux to zero independently of the interior state. But a smooth trace of the stated PDE has physical flux
The fixed vector is not tangent to the closed polygon. Wherever , zero physical flux requires . This would impose an additional zero value at body outflow. In the positive, source-free branch, is constant along each straight characteristic. A characteristic entering from the far field with and ending on the upwind side of the body cannot also have there. At the characteristic speed degenerates, so one cannot repair the derivation by dividing the boundary condition by .
Consequently the present zero-flux numerical closure does not provide the smooth, positive classical boundary-value problem needed for the above continuous-dual comparison. This is not a proof that no weak or measure-valued formulation could ever be defined. Such a formulation would require an entropy/boundary-trace specification and an appropriate sensitivity analysis, absent from these pages. The finite-dimensional residual, transpose, and discrete gradients remain well defined.
For the new comparison only, treat both outer and inner boundaries by characteristics: prescribe where , and use the physical interior flux where . The domain, interior numerical flux and its remain the pages' own. The exact primal is .
Keep the drag-like functional
but at prescribed inflow use the prescribed trace in the functional, not a free interior value. This readout change is part of the contrast and is not silently applied to the original example. The continuous dual is
At an interior point, follow the ray in direction to its first boundary intersection. The value is if that intersection is the body, and zero otherwise. This is an exact characteristic calculation, not a transpose solve. It has jumps along rays from body tangencies/corners. The comparison uses volume-weighted L2 error; boundary nodes are excluded from the node-centred norm. No smooth maximum-norm convergence is claimed. This contrast does not validate continuous consistency of the original zero-flux wall.
读取边界线段 → 沿 +β 发射射线 → 选最近正向交点 → 物面取 1/|β|、远场取 0 → 体积加权比较
Read boundary segments → trace along +β → select first forward hit → body: 1/|β|, outer: 0 → volume-weighted comparison
下方代码可以完整复制到本地文件运行。它独立计算连续参考,不调用本页有限体积 Jacobian 的转置。二维代码内嵌两次细化的多边形网格;这里输出参考场,后面的多网格交互保留已计算的加密结果。
Copy the complete code below into a local file and run it. It computes the continuous reference independently of the page’s finite-volume Jacobian transpose. The 2-D scripts include the polygonal mesh refined twice; they output reference fields, while the refinement viewer below retains the precomputed multi-grid comparisons.
python3 -m pip install numpy scipy
python3 reference_transport.pyimport numpy as np
import json
mesh=json.loads('{"X":[[0.804,-0.04],[0.6125777772109133,0.4725777772109134],[0.10000000000000003,0.51],[-0.165239682094289,0.22523968209428905],[-0.29600000000000004,-0.03999999999999995],[-0.25857777721091335,-0.3985777772109132],[0.09999999999999991,-0.5900000000000001],[0.519239682094289,-0.45923968209428906],[1.2972000000000001,-0.022000000000000002],[0.9414940753805006,0.8644940753805004],[0.05500000000000008,1.1355],[-0.695458123066357,0.7284581230663572],[-1.0178,-0.02199999999999987],[-0.7467940753805007,-0.8237940753805003],[0.05499999999999979,-1.1795],[0.8901581230663569,-0.8571581230663573],[1.9,0],[1.3435028842544403,1.34350288425444],[1.1634144591899854e-16,1.9],[-1.34350288425444,1.3435028842544403],[-1.9,2.326828918379971e-16],[-1.3435028842544405,-1.34350288425444],[-3.490243377569956e-16,-1.9],[1.3435028842544399,-1.3435028842544405],[1.0506000000000002,-0.031],[1.1193470376902503,0.4212470376902502],[0.777035926295707,0.6685359262957069],[0.7082888886054567,0.2162888886054567],[0.9138179631478536,0.31876796314785344],[0.4982470376902503,0.9999970376902502],[0.07750000000000005,0.82275],[0.3562888886054567,0.4912888886054567],[0.42726796314785354,0.7456429631478534],[-0.3202290615331785,0.9319790615331786],[-0.430348902580323,0.4768489025803231],[-0.03261984104714448,0.3676198410471445],[-0.17642445129016146,0.6497994512901616],[-0.8566290615331785,0.3532290615331786],[-0.6569,-0.03099999999999991],[-0.2306198410471445,0.09261984104714455],[-0.5436244512901616,0.22292445129016158],[-0.8822970376902504,-0.4228970376902501],[-0.5026859262957071,-0.6111859262957068],[-0.2772888886054567,-0.2192888886054566],[-0.5797929631478536,-0.32109296314785335],[-0.34589703769025043,-1.0016470376902502],[0.07749999999999985,-0.88475],[-0.07928888860545671,-0.4942888886054566],[-0.21259296314785361,-0.7479679631478533],[0.47257906153317836,-1.0183290615331786],[0.704698902580323,-0.6581989025803232],[0.3096198410471444,-0.5246198410471445],[0.3910994512901613,-0.7714744512901617],[1.0936790615331784,-0.43957906153317866],[0.6616198410471446,-0.24961984104714452],[0.8776494512901616,-0.3445994512901616],[1.5986,-0.011000000000000001],[1.62175144212722,0.67175144212722],[1.3203514421272202,0.66075144212722],[1.1424984798174704,1.1039984798174702],[0.6717514421272203,1.6217514421272199],[0.47074703769025034,1.38224703769025],[0.027500000000000097,1.51775],[-0.6717514421272199,1.62175144212722],[-0.64425144212722,1.2395014421272201],[-1.0194805036603984,1.0359805036603986],[-1.6217514421272199,0.6717514421272203],[-1.2977290615331785,0.3642290615331787],[-1.4588999999999999,-0.010999999999999819],[-1.6217514421272203,-0.6717514421272199],[-1.1806514421272203,-0.6827514421272199],[-1.0451484798174706,-1.0836484798174701],[-0.6717514421272205,-1.6217514421272199],[-0.3733970376902505,-1.3618970376902502],[0.027499999999999723,-1.53975],[0.6717514421272197,-1.6217514421272203],[0.6992514421272198,-1.2615014421272202],[1.1168305036603985,-1.100330503660399],[1.6217514421272199,-0.6717514421272203],[1.3950790615331785,-0.42857906153317865],[0.9273000000000001,-0.035500000000000004],[0.9822089815739269,0.14388398157392673],[0.8110534258766551,0.26752842587665504],[0.7561444443027283,0.08814444430272834],[0.8691767129383277,0.11601421293832753],[1.1739000000000002,-0.026500000000000003],[1.2082735188451252,0.19962351884512508],[1.016582500419052,0.37000750041905184],[1.0952412502095261,0.1717537502095259],[1.0304205565353755,0.6428705565353753],[0.8592650008381038,0.7665150008381036],[0.8454269447217803,0.4936519447217802],[0.9379237506285779,0.5682612506285778],[0.6948068517533101,0.5705568517533102],[0.6604333329081851,0.344433332908185],[0.7529301388149826,0.4190426388149826],[0.6021519447217802,0.7070894447217801],[0.3917784258766551,0.618465925876655],[0.484433332908185,0.4819333329081851],[0.5432926388149826,0.5945113888149827],[0.7198705565353755,0.9322455565353753],[0.46275750041905195,0.8728200004190518],[0.6610112506285778,0.8196675006285776],[0.2766235188451252,1.067748518845125],[0.06625000000000006,0.979125],[0.2523839815739268,0.7841964815739266],[0.264503750209526,0.9259725002095259],[0.08875000000000005,0.6663749999999999],[0.22814444430272837,0.5006444443027284],[0.24026421293832756,0.6424204629383274],[-0.0494622256450807,0.7362747256450808],[-0.10452214616865296,0.5087096461686531],[0.03369007947642778,0.43880992052357226],[-0.00788607308432646,0.5875423230843264],[-0.1326145307665892,1.0337395307665893],[-0.24832675641166996,0.7908892564116701],[-0.09103837820583496,0.885007128205835],[-0.5078435922997677,0.8302185922997678],[-0.56290351282334,0.6026535128233401],[-0.30338667693524224,0.5633241769352424],[-0.405615134617505,0.696771384617505],[-0.297794292337306,0.35104429233730605],[-0.09892976157071673,0.29642976157071677],[-0.20115821925297947,0.4298769692529796],[-0.4869866769352423,0.34988667693524234],[-0.38712214616865304,0.15777214616865307],[-0.19792976157071673,0.1589297615707168],[-0.34245821925297953,0.2544082192529796],[-0.7760435922997677,0.5408435922997679],[-0.70012675641167,0.2880767564116701],[-0.631515134617505,0.4453651346175051],[-0.9372145307665893,0.16561453076658939],[-0.83735,-0.02649999999999989],[-0.6002622256450808,0.09596222564508083],[-0.768738378205835,0.13078837820583508],[-0.47645000000000004,-0.035499999999999934],[-0.26330992052357227,0.026309920523572297],[-0.43178607308432654,0.061136073084326566],[-0.6183464815739268,-0.17604648157392663],[-0.42854092587665515,-0.27019092587665494],[-0.2866444443027284,-0.12964444430272826],[-0.4524954629383276,-0.15284546293832746],[-0.9500485188451252,-0.22244851884512498],[-0.731045000419052,-0.37199500041905176],[-0.784197500209526,-0.1992475002095258],[-0.8145455565353755,-0.6233455565353752],[-0.6247400008381039,-0.7174900008381035],[-0.5412394447217803,-0.46613944472178004],[-0.6778925006285779,-0.5447425006285777],[-0.3806318517533102,-0.50488185175331],[-0.267933332908185,-0.3089333329081849],[-0.40458638881498266,-0.3875363888149825],[-0.35763944472178033,-0.6795769447217801],[-0.14594092587665516,-0.621128425876655],[-0.16893333290818502,-0.4464333329081849],[-0.2632863888149827,-0.5630051388149825],[-0.5463455565353755,-0.9127205565353753],[-0.27924500041905204,-0.8748075004190518],[-0.45199250062857793,-0.7961487506285776],[-0.14544851884512533,-1.090573518845125],[0.06624999999999982,-1.032125],[-0.06754648157392688,-0.8163589815739267],[-0.1064975002095261,-0.9534662502095258],[0.08874999999999988,-0.7373750000000001],[0.010355555697271597,-0.5421444443027283],[-0.028595462938327643,-0.6792517129383275],[0.2342997256450806,-0.8281122256450808],[0.3503596461686529,-0.6480471461686531],[0.20480992052357216,-0.5573099205235723],[0.2195548230843264,-0.6927110730843267],[0.26378953076658906,-1.0989145307665893],[0.43183925641166987,-0.8949017564116701],[0.24904462820583484,-0.963513378205835],[0.6813685922997677,-0.9377435922997679],[0.7974285128233399,-0.7576785128233403],[0.5478991769352421,-0.7148366769352424],[0.6146338846175049,-0.8262901346175051],[0.6119692923373059,-0.5587192923373061],[0.41442976157071665,-0.4919297615707168],[0.4811644692529794,-0.6033832192529797],[0.7911741769352423,-0.5013991769352424],[0.7696346461686531,-0.2971096461686531],[0.5904297615707168,-0.3544297615707168],[0.6908019692529794,-0.4279144692529796],[0.9919185922997676,-0.648368592299768],[0.98566425641167,-0.39208925641167014],[0.8915463846175049,-0.5748838846175053],[1.1954395307665893,-0.23078953076658934],[0.9641247256450809,-0.1877997256450808],[1.079782128205835,-0.20929462820583508],[0.7328099205235723,-0.14480992052357225],[0.8484673230843267,-0.16630482308432656],[1.4479000000000002,-0.0165],[1.45947572106361,0.32487572106361],[1.3087757210636102,0.31937572106361],[1.7492999999999999,-0.0055000000000000005],[1.7608757210636101,0.33587572106361],[1.61017572106361,0.33037572106361],[1.4710514421272203,0.6662514421272201],[1.48262716319083,1.00762716319083],[1.3319271631908303,1.00212716319083],[1.2198492399087353,0.5409992399087351],[1.2430006820359554,1.2237506820359552],[1.2314249609723453,0.8823749609723451],[1.1309227587538604,0.7626227587538602],[1.0419962775989855,0.9842462775989853],[0.8066227587538604,1.2431227587538602],[0.7061205565353754,1.123370556535375],[1.0076271631908302,1.48262716319083],[0.9071249609723453,1.362874960972345],[0.5712492399087353,1.501999239908735],[0.3358757210636102,1.76087572106361],[0.23537351884512522,1.641123518845125],[0.4844970376902503,1.19112203769025],[0.013750000000000108,1.708875],[0.2491235188451252,1.449998518845125],[0.2628735188451252,1.258873518845125],[0.041250000000000085,1.326625],[-0.30837572106360994,1.3786257210636101],[-0.29462572106360996,1.1875007210636102],[-0.3358757210636099,1.7608757210636101],[-0.3221257210636099,1.5697507210636101],[-0.65800144212722,1.4306264421272201],[-1.00762716319083,1.48262716319083],[-0.9938771631908301,1.29150216319083],[-0.48224025183019925,1.0857402518301993],[-1.1814916939574194,1.1897416939574195],[-0.8318659728938091,1.1377409728938095],[-0.6698547825967884,0.9839797825967886],[-0.8574693133633777,0.8822193133633779],[-1.1586047825967885,0.7001047825967887],[-0.9965935922997677,0.5463435922997679],[-1.48262716319083,1.0076271631908302],[-1.3206159728938092,0.8538659728938094],[-1.4597402518301992,0.5179902518301995],[-1.76087572106361,0.33587572106361024],[-1.5988645307665892,0.18211453076658946],[-1.0771790615331784,0.35872906153317863],[-1.6794499999999999,-0.005499999999999793],[-1.3783145307665892,0.17661453076658942],[-1.1577645307665891,0.1711145307665894],[-1.23835,-0.016499999999999845],[-1.31977572106361,-0.34687572106360987],[-1.0992257210636103,-0.3523757210636099],[-1.7608757210636101,-0.33587572106360986],[-1.54032572106361,-0.34137572106360986],[-1.4012014421272203,-0.67725144212722],[-1.4826271631908305,-1.00762716319083],[-1.2620771631908303,-1.01312716319083],[-1.0314742399087353,-0.5528242399087351],[-1.1943256820359556,-1.2135756820359551],[-1.1128999609723453,-0.8831999609723451],[-0.9637227587538605,-0.7532727587538601],[-0.8959712775989856,-0.9537212775989852],[-0.7092727587538605,-1.2227727587538602],[-0.5600955565353756,-1.0928455565353752],[-1.0076271631908305,-1.48262716319083],[-0.8584499609723455,-1.3526999609723451],[-0.5225742399087355,-1.491824239908735],[-0.3358757210636104,-1.76087572106361],[-0.18669851884512542,-1.6309485188451252],[-0.35964703769025047,-1.18177203769025],[0.013749999999999686,-1.719875],[-0.17294851884512538,-1.450823518845125],[-0.15919851884512534,-1.270698518845125],[0.04124999999999976,-1.3596249999999999],[0.36337572106360977,-1.40062572106361],[0.3771257210636098,-1.22050072106361],[0.3358757210636097,-1.7608757210636101],[0.34962572106360973,-1.5807507210636103],[0.6855014421272198,-1.4416264421272202],[1.0076271631908298,-1.4826271631908305],[1.02137716319083,-1.3025021631908302],[0.5859152518301991,-1.1399152518301994],[1.2301666939574192,-1.2219166939574198],[0.9080409728938091,-1.1809159728938097],[0.7947047825967885,-1.0593297825967887],[1.0034943133633778,-0.9787443133633782],[1.2559547825967885,-0.7644547825967888],[1.1426185922997676,-0.642868592299768],[1.4826271631908299,-1.0076271631908305],[1.3692909728938092,-0.8860409728938097],[1.5084152518301992,-0.5501652518301995],[1.76087572106361,-0.33587572106361013],[1.6475395307665892,-0.21428953076658933],[1.2443790615331785,-0.43407906153317866],[1.4968395307665894,-0.21978953076658933],[1.3461395307665893,-0.22528953076658934]],"C2N":[[1,81,85,84],[81,25,82,85],[85,82,29,83],[84,85,83,28],[25,86,89,82],[86,9,87,89],[89,87,26,88],[82,89,88,29],[29,88,93,92],[88,26,90,93],[93,90,10,91],[92,93,91,27],[28,83,96,95],[83,29,92,96],[96,92,27,94],[95,96,94,2],[2,94,100,99],[94,27,97,100],[100,97,33,98],[99,100,98,32],[27,91,103,97],[91,10,101,103],[103,101,30,102],[97,103,102,33],[33,102,107,106],[102,30,104,107],[107,104,11,105],[106,107,105,31],[32,98,110,109],[98,33,106,110],[110,106,31,108],[109,110,108,3],[3,108,114,113],[108,31,111,114],[114,111,37,112],[113,114,112,36],[31,105,117,111],[105,11,115,117],[117,115,34,116],[111,117,116,37],[37,116,121,120],[116,34,118,121],[121,118,12,119],[120,121,119,35],[36,112,124,123],[112,37,120,124],[124,120,35,122],[123,124,122,4],[4,122,128,127],[122,35,125,128],[128,125,41,126],[127,128,126,40],[35,119,131,125],[119,12,129,131],[131,129,38,130],[125,131,130,41],[41,130,135,134],[130,38,132,135],[135,132,13,133],[134,135,133,39],[40,126,138,137],[126,41,134,138],[138,134,39,136],[137,138,136,5],[5,136,142,141],[136,39,139,142],[142,139,45,140],[141,142,140,44],[39,133,145,139],[133,13,143,145],[145,143,42,144],[139,145,144,45],[45,144,149,148],[144,42,146,149],[149,146,14,147],[148,149,147,43],[44,140,152,151],[140,45,148,152],[152,148,43,150],[151,152,150,6],[6,150,156,155],[150,43,153,156],[156,153,49,154],[155,156,154,48],[43,147,159,153],[147,14,157,159],[159,157,46,158],[153,159,158,49],[49,158,163,162],[158,46,160,163],[163,160,15,161],[162,163,161,47],[48,154,166,165],[154,49,162,166],[166,162,47,164],[165,166,164,7],[7,164,170,169],[164,47,167,170],[170,167,53,168],[169,170,168,52],[47,161,173,167],[161,15,171,173],[173,171,50,172],[167,173,172,53],[53,172,177,176],[172,50,174,177],[177,174,16,175],[176,177,175,51],[52,168,180,179],[168,53,176,180],[180,176,51,178],[179,180,178,8],[8,178,184,183],[178,51,181,184],[184,181,56,182],[183,184,182,55],[51,175,187,181],[175,16,185,187],[187,185,54,186],[181,187,186,56],[56,186,190,189],[186,54,188,190],[190,188,9,86],[189,190,86,25],[55,182,192,191],[182,56,189,192],[192,189,25,81],[191,192,81,1],[9,193,195],[193,57,194],[195,194,59],[193,194,195],[57,196,198],[196,17,197],[198,197,58],[196,197,198],[59,199,201],[199,58,200],[201,200,18],[199,200,201],[57,198,194],[198,58,199],[194,199,59],[198,199,194],[9,195,87],[195,59,202],[87,202,26],[195,202,87],[59,201,204],[201,18,203],[204,203,60],[201,203,204],[26,205,90],[205,60,206],[90,206,10],[205,206,90],[59,204,202],[204,60,205],[202,205,26],[204,205,202],[10,206,208],[206,60,207],[208,207,62],[206,207,208],[60,203,210],[203,18,209],[210,209,61],[203,209,210],[62,211,213],[211,61,212],[213,212,19],[211,212,213],[60,210,207],[210,61,211],[207,211,62],[210,211,207],[10,208,101],[208,62,214],[101,214,30],[208,214,101],[62,213,216],[213,19,215],[216,215,63],[213,215,216],[30,217,104],[217,63,218],[104,218,11],[217,218,104],[62,216,214],[216,63,217],[214,217,30],[216,217,214],[11,218,220],[218,63,219],[220,219,65],[218,219,220],[63,215,222],[215,19,221],[222,221,64],[215,221,222],[65,223,225],[223,64,224],[225,224,20],[223,224,225],[63,222,219],[222,64,223],[219,223,65],[222,223,219],[11,220,115],[220,65,226],[115,226,34],[220,226,115],[65,225,228],[225,20,227],[228,227,66],[225,227,228],[34,229,118],[229,66,230],[118,230,12],[229,230,118],[65,228,226],[228,66,229],[226,229,34],[228,229,226],[12,230,232],[230,66,231],[232,231,68],[230,231,232],[66,227,234],[227,20,233],[234,233,67],[227,233,234],[68,235,237],[235,67,236],[237,236,21],[235,236,237],[66,234,231],[234,67,235],[231,235,68],[234,235,231],[12,232,129],[232,68,238],[129,238,38],[232,238,129],[68,237,240],[237,21,239],[240,239,69],[237,239,240],[38,241,132],[241,69,242],[132,242,13],[241,242,132],[68,240,238],[240,69,241],[238,241,38],[240,241,238],[13,242,244],[242,69,243],[244,243,71],[242,243,244],[69,239,246],[239,21,245],[246,245,70],[239,245,246],[71,247,249],[247,70,248],[249,248,22],[247,248,249],[69,246,243],[246,70,247],[243,247,71],[246,247,243],[13,244,143],[244,71,250],[143,250,42],[244,250,143],[71,249,252],[249,22,251],[252,251,72],[249,251,252],[42,253,146],[253,72,254],[146,254,14],[253,254,146],[71,252,250],[252,72,253],[250,253,42],[252,253,250],[14,254,256],[254,72,255],[256,255,74],[254,255,256],[72,251,258],[251,22,257],[258,257,73],[251,257,258],[74,259,261],[259,73,260],[261,260,23],[259,260,261],[72,258,255],[258,73,259],[255,259,74],[258,259,255],[14,256,157],[256,74,262],[157,262,46],[256,262,157],[74,261,264],[261,23,263],[264,263,75],[261,263,264],[46,265,160],[265,75,266],[160,266,15],[265,266,160],[74,264,262],[264,75,265],[262,265,46],[264,265,262],[15,266,268],[266,75,267],[268,267,77],[266,267,268],[75,263,270],[263,23,269],[270,269,76],[263,269,270],[77,271,273],[271,76,272],[273,272,24],[271,272,273],[75,270,267],[270,76,271],[267,271,77],[270,271,267],[15,268,171],[268,77,274],[171,274,50],[268,274,171],[77,273,276],[273,24,275],[276,275,78],[273,275,276],[50,277,174],[277,78,278],[174,278,16],[277,278,174],[77,276,274],[276,78,277],[274,277,50],[276,277,274],[16,278,280],[278,78,279],[280,279,80],[278,279,280],[78,275,282],[275,24,281],[282,281,79],[275,281,282],[80,283,285],[283,79,284],[285,284,17],[283,284,285],[78,282,279],[282,79,283],[279,283,80],[282,283,279],[16,280,185],[280,80,286],[185,286,54],[280,286,185],[80,285,287],[285,17,196],[287,196,57],[285,196,287],[54,288,188],[288,57,193],[188,193,9],[288,193,188],[80,287,286],[287,57,288],[286,288,54],[287,288,286]],"bnd":{"1-84":"wall","28-84":"wall","2-95":"wall","28-95":"wall","17-197":"far","58-197":"far","18-200":"far","58-200":"far","2-99":"wall","32-99":"wall","3-109":"wall","32-109":"wall","18-209":"far","61-209":"far","19-212":"far","61-212":"far","3-113":"wall","36-113":"wall","4-123":"wall","36-123":"wall","19-221":"far","64-221":"far","20-224":"far","64-224":"far","4-127":"wall","40-127":"wall","5-137":"wall","40-137":"wall","20-233":"far","67-233":"far","21-236":"far","67-236":"far","5-141":"wall","44-141":"wall","6-151":"wall","44-151":"wall","21-245":"far","70-245":"far","22-248":"far","70-248":"far","6-155":"wall","48-155":"wall","7-165":"wall","48-165":"wall","22-257":"far","73-257":"far","23-260":"far","73-260":"far","7-169":"wall","52-169":"wall","8-179":"wall","52-179":"wall","23-269":"far","76-269":"far","24-272":"far","76-272":"far","1-191":"wall","55-191":"wall","8-183":"wall","55-183":"wall","17-284":"far","79-284":"far","24-281":"far","79-281":"far"}}')
beta=np.array([1.,.5]);X=np.array(mesh['X']);segments=[]
for key,bc in mesh['bnd'].items():
a,b=np.array(key.split('-'),int)-1
segments.append((X[a],X[b],bc))
def cross(a,b): return a[0]*b[1]-a[1]*b[0]
def ray(p):
hits=[]
for a,b,bc in segments:
d=b-a;den=cross(beta,d)
if abs(den)<1e-14: continue
t=cross(a-p,d)/den;z=cross(a-p,beta)/den
if t>1e-9 and -1e-9<=z<=1+1e-9: hits.append((t,bc))
if not hits: raise ValueError('No forward boundary intersection')
return 1/np.linalg.norm(beta) if min(hits)[1]=='wall' else 0.
if __name__ == '__main__':
points=np.array([X[np.array(c)-1].mean(axis=0) for c in mesh['C2N']])
lam=np.array([ray(p) for p in points])
np.savetxt('continuous_transport.csv',np.column_stack([points,lam]),delimiter=',',header='x,y,lambda')
print('Characteristic-boundary contrast only:',len(lam),'samples; range',lam.min(),lam.max())
点积测试查转置,差分与复数步长查同一离散目标的梯度;它们都不能证明离散伴随逼近了连续伴随。本节先推导连续问题,再用独立参考解与网格加密比较。
A dot test checks the transpose; finite differences and the complex step check the gradient of the same discrete objective. None proves approximation of the continuous dual. Here we derive the continuous problem first, then compare with an independent reference under refinement.
本页用 AhTψh=−gh,因此连续源项敏感度的符号是 λh=−ψh。守恒残差已做控制体积分,不再除以体积。
This page solves AhTψh=−gh, hence the source-sensitivity sign λh=−ψh. Conservation residuals are already integrated: do not divide by volume.
若 J=∫Ωj(u)+∫∂Ωb(u),域内得到下式。原始入流的状态固定,v=0,不在那里给伴随条件;原始出流的 v 自由,由边界项给伴随条件。
For J=∫Ωj(u)+∫∂Ωb(u) the volume equation is below. At primal inflow the state is prescribed, v=0, and no dual datum is imposed there. At primal outflow v is free, so the boundary term supplies the dual datum.
本页的壁面数值通量直接为零,与内侧状态无关。但原 PDE 的物理通量是 12u2β·n;固定的 β=(1,0.5) 不可能沿闭合物面处处相切。β·n≠0 处零物理通量要求 u=0。对从远场 u=1 进入、最后撞到迎风面的特征线,这与沿特征保持 u=1 矛盾;u=0 还使特征速度退化。
The original numerical wall flux is zero independently of the interior state. The PDE physical flux is 12u2β·n, but the fixed β=(1,0.5) cannot be tangent everywhere to a closed body. Where β·n≠0, zero physical flux requires u=0. A characteristic entering from the far field at u=1 and ending on the upwind body cannot satisfy that: its state stays one. At u=0 the characteristic speed also degenerates.
因此不能宣称原零通量算例具有这里所需的光滑、正值连续伴随参考。这不否定已有离散梯度,也不是排除所有弱解理论;它说明必须先另行定义弱解、边界迹及其灵敏度,不能把二维标量的“壁面”直接当作 Euler 滑移壁面。
The original zero-flux example therefore does not supply the smooth positive continuous reference required here. Its discrete gradients remain valid. This does not exclude every weak-solution theory; such a theory would need its own boundary traces and sensitivities. A scalar “wall” cannot simply be treated as an Euler slip wall.
仅在本节对照中,把内、外边界都改为:入流给 u=1,出流外推。目标仍是壁面 12u2(e·n) 的积分,但入流段使用已给定的边界迹,不对内侧未知量求导。原页面第 1–17 节的主线算例不变。此时精确原始解 u=1,e=β/|β|,域内 −β·∇λ=0。
For this contrast only, prescribe u=1 at inflow on both boundaries and extrapolate at outflow. The objective is still the wall integral of 12u2(e·n), but uses the prescribed trace on inflow rather than differentiating an interior unknown there. The main line, Sections 1–17, is unchanged. Now the exact primal is u=1, e=β/|β|, and −β·∇λ=0 inside.
从任一内点沿 +β 追踪射线,到第一个边界交点:遇到物面取 1/|β|,遇到远场取 0。这就是连续伴随的独立计算,不解离散转置。它沿物面切线/角点发出的特征有跳跃;所以检查体积加权 L2,不要求最大范数收敛。
Trace a ray from an interior point along +β to its first boundary intersection: assign 1/|β| at the body, zero at the outer boundary. This independently computes the continuous dual without a discrete transpose. Jumps issue from body tangencies/corners; use volume-weighted L2 and do not demand maximum-norm convergence.
| 层Level | DOF | L2 | max |
|---|---|---|---|
| 0 | 24 | 2.497e-01 | 5.816e-01 |
| 1 | 80 | 2.237e-01 | 5.588e-01 |
| 2 | 288 | 1.935e-01 | 5.270e-01 |
| 3 | 1088 | 1.630e-01 | 5.723e-01 |
表中是特征边界对照,不是原零通量边界的验证。格点范数排除边界节点;离散耗散把连续伴随的跳跃抹宽,L2 误差下降较慢。下图可切换网格、连续场、离散场与差值。
These are results for the characteristic-boundary contrast, not validation of the original zero-flux boundary. The node norm excludes boundary nodes. Numerical dissipation broadens the dual jumps, so L2 decreases slowly. The viewer switches grid, continuous field, discrete field and difference.
伴随不一致与伴随问题不适定不是同一件事。适定性要求在明确的函数空间、边界迹与数据范数下:解存在、唯一,并连续依赖于数据。比较之前先固定同一个原始问题、目标泛函、边界条件和乘子含义;边界层、有限参考误差、错误的目标边界项,都可能造成差异,不能仅凭一张误差图判定不适定。
Adjoint inconsistency and an ill-posed adjoint problem are different diagnoses. Well-posedness requires existence, uniqueness and continuous dependence on data in specified function spaces, trace spaces and norms. First match the primal problem, functional, boundary conditions and multiplier interpretation. Boundary layers, finite reference error, an incorrect objective boundary term can all cause discrepancies; an error plot alone cannot diagnose ill-posedness.
在线性化状态及边界约束都已确定后,记原始线性算子为 𝒜,伴随方程为 𝒜*λ=g。若 𝒜 是适当空间之间的有界同构,正确配对的对偶问题也有有界逆。若存在非零零模态 v,则可解性至少要求 g[v]=0;伴随自身有非零齐次解时,解就不唯一。无穷维情形还必须检查值域闭性/稳定估计,不能只数边界条件。
After fixing the linearized state and boundary constraints, denote the primal operator by 𝒜 and the dual equation by 𝒜*λ=g. If 𝒜 is a bounded isomorphism between suitable spaces, its correctly paired dual also has a bounded inverse. A nonzero primal null mode v requires at least g[v]=0 for solvability; a nonzero homogeneous dual mode makes the dual nonunique. In infinite dimensions one must also check closed range/stability estimates, not merely count boundary conditions.
非线性原始解存在唯一,也不自动保证其导数存在:还需要所选解支的局部可微性和线性化可逆性。下面讨论的都是已说明的光滑解支;不把有限网格上能解出的转置系统当作连续理论的替代。
Even existence and uniqueness of a nonlinear primal do not automatically ensure differentiability: local smoothness of the chosen solution branch and invertibility of its linearization are additional requirements. The discussion concerns the stated smooth branch; a solvable finite-grid transpose system is not a substitute for continuous analysis.
取区间 0<x<1、常数速度 c>0,原始入口固定,目标含体积分及出口项 bu(1)。分部积分给出下式,唯一解从原始出口向上游积分:
On 0<x<1, take constant speed c>0, fixed primal inflow and an objective with a volume term and outlet term bu(1). Integration by parts gives the following unique solution, integrated upstream from primal outflow:
少条件:不规定出口伴随值时可加任意常数,解不唯一。多条件:若还照搬原始入口而规定 λ(0)=0,则必须满足 b+∫01gdx=0;例如 g=1,b=0 时正确解是 (1−x)/c,额外入口条件使问题无解。失去一致稳定性:上述数据到解的放大含 1/c,因此不能在 c→0 时声称有与 c 无关的稳定界。每个固定 c>0 仍然可以适定。
Missing data: without the outlet dual value, an arbitrary additive constant remains, so the solution is nonunique. Excess data: also copying the primal inflow condition as λ(0)=0 requires b+∫01gdx=0. For g=1,b=0, the correct solution is (1−x)/c and the extra inlet condition makes it impossible. Loss of uniform stability: data amplification contains 1/c, so no bound independent of c is justified as c→0. Each fixed c>0 can still define a well-posed problem.
法向始终取计算域的外法向,内边界的法向指向物体。因此迎风物面是流体域的原始出流:特征已从远场携带 u=1 到达这里,再要求零物理通量就是不相容的额外限制。这里首先缺少的是原始光滑正值解,故没有合法的该分支线性化状态;不能跳过这一步,只给形式伴随补几个边界值。
The normal always points out of the fluid domain, into the body on its inner boundary. The upwind body is therefore primal outflow from the fluid: a characteristic already carries u=1 from the far field, and requiring zero physical flux adds an incompatible restriction. The first obstruction is the absence of the stated smooth positive primal, hence of a valid linearization about that branch; appending boundary values to a formal dual cannot repair this.
若退到壁面 u=0,上式对任意 v 都为零:一阶线性化丢失了原非线性通量条件的约束信息,特征速度也同时退化。这个退化不能用“把零通量条件照抄给伴随”来修复。要讨论允许停滞、间断或弱解的新问题,必须另建解空间、熵/边界迹条件及可微性理论。
At a wall state u=0, this first variation vanishes for every v: linearization loses information contained in the nonlinear flux constraint while the characteristic speed degenerates. Copying a zero-flux condition onto the adjoint does not repair it. A new problem allowing stagnation, discontinuities or weak solutions needs its own solution space, entropy/boundary traces and differentiability theory.
对于本节相容的特征边界对照,每条内部特征沿 +β 都能在有限路程内到达出流边界,因此边界数据确定内部场(除去零测集上的取值约定)。切线或角点发出的跳跃不等于不适定;应在允许跳跃的空间中讨论。一般边界公式中的 bu/(a·n) 在特征边界附近需要兼容性分析,本对照的物面分子与分母同时含法向因子并相消,不能仅看分母趋零就宣称伴随发散。
For the compatible contrast, every interior characteristic followed along +β reaches outflow in finite distance, determining the interior field up to values on sets of measure zero. Jumps from tangencies or corners do not imply ill-posedness; use a space allowing them. The general ratio bu/(a·n) requires compatibility analysis near characteristic boundaries. In this contrast its body numerator and denominator share a cancelling normal factor, so a vanishing denominator alone does not establish adjoint blow-up.
即使每个有限网格上的 AhT 都可逆,也不保证在网格相关的正确加权范数中,其逆有与 h 无关的界。人工耗散或边界闭合可能让每个有限系统可解,却不能给出目标连续问题的稳定极限。要判断一致性,应先建立连续问题,再检查残差与目标的逼近、边界乘子的对应关系以及离散稳定性;若这些前提不成立,就把它保留为反例并准确指出失败环节。
Even if every finite-grid AhT is invertible, its inverse need not be bounded independently of h in the appropriate mesh-weighted norms. Artificial dissipation or boundary closure may make every finite system solvable without providing a stable limit for the intended continuous problem. Establish the continuous problem first, then check residual/functional approximation, boundary-multiplier interpretation and discrete stability. If a premise fails, retain the example and identify the failed premise explicitly.
下图展示已离线计算的连续参考与离散场,可切换网格;它不是在浏览器里重新求解连续 PDE。图 A.2 给出场与误差。完整的符号、边界项和证据范围见连续伴随推导(新增模型的几何变分见附录 B)。
The viewer displays offline-computed continuous references and discrete fields at selectable resolutions; it does not rerun a continuous PDE solve in the browser. Figure A.2 shows fields and errors. See the continuous-adjoint derivation for signs, boundary terms and evidence limits (geometry variations of the added model: Appendix B).
令 F=u²β/2,L=∫Γinner u²(e·n)/2 ds。给 L 加上 ∫Ω ψ∇·F dΩ(原始方程成立时它为零),符号与正文的离散约定一致。固定几何时 δF=uβδu。对残差变分积分一次,得到内部方程和边界项。
Let F=u²β/2 and L=∫Γinner u²(e·n)/2 ds. Add ∫Ω ψ∇·F dΩ to L (it is zero when the primal equation holds), with the sign of the discrete convention in the main text. For fixed geometry, δF=uβδu. Integrating by parts gives the volume equation and boundary term.
Γ₊ 是边界上 β·n>0 的部分(原始出流),Γ₋ 是 β·n<0 的部分(原始入流),|β|=√1.25。固定入流数据时 δu=0,所以不再给原始入流边界指定额外伴随值。离散目标因此在入流面上用给定的 u_in(第 7 节),不用内侧迹。原始出流处 δu 自由,必须消去方括号中的系数。即使 β·n 在某点接近零,内边界 e 与 β 平行使比值保持 −1/|β|;恰好相切的射线及角点仍需按几乎处处的输运解理解。
Γ₊ is the part of the boundary where β·n>0 (primal outflow), Γ₋ where β·n<0 (primal inflow); |β|=√1.25. For fixed inflow data δu=0, so no additional adjoint value is prescribed on primal inflow; the discrete objective accordingly uses the prescribed u_in on inflow faces (Section 7), not the interior trace. At primal outflow δu is free, requiring the bracketed coefficient to vanish. Because e is parallel to β, the inner-boundary ratio stays −1/|β| near grazing; exactly tangent rays and corners still require an almost-everywhere transport interpretation.
从每个内部点沿 +β 追踪,首次遇到内边界则 ψ=−1/|β|,首次遇到外边界则 ψ=0。这是独立几何射线计算,不调用离散矩阵。两类射线的分界可能产生伴随跳变,它由终端目标数据造成,不是原始流场激波。若把乘子的符号取反(加上 −∫λ∇·F),则 λ=−ψ。
Trace +β from each interior point. Set ψ=−1/|β| if the first intersection is the inner boundary and zero if it is outer. This independent geometric ray calculation does not call a discrete matrix. Interfaces between the two ray families can have adjoint jumps caused by terminal objective data, not primal shocks. With the opposite sign of the multiplier (adding −∫λ∇·F), λ=−ψ.
for point x in fluid domain:
hits = positive_ray_intersections(x, beta, all_boundary_edges)
first = hit_with_smallest_positive_distance(hits)
psi_ref[x] = -1/norm(beta) if first.is_inner else 0改变入流水平 u∞ 时,入流边界剩余项为 ∫Γ₋ u(ψβ·n+χinner e·n)δu∞ ds。改变几何时还必须包含移动边界和 u_in 的位置导数,不能只保留法向项。在本例全局解析场下,L 对所有有效边界形变及 u∞ 恒为零,可由孔洞内散度定理独立证明;因此完整连续总导数也为零。粗网格离散梯度不必为零,它应通过重算差分与复数残差来检验;加密时它是否趋于零,见下面的表。
Changing u∞ leaves the inflow term ∫Γ₋ u(ψβ·n+χinner e·n)δu∞ ds. Geometry variations additionally require moving-boundary and position-dependent u_in terms, not merely normal derivatives. For this global analytic field, L is identically zero for valid boundary shapes and all u∞, independently by the divergence theorem inside the hole. Its complete continuous total derivatives are zero. Coarse discrete gradients need not vanish and are checked by reconverged differences and complex residuals; whether they vanish under refinement is the table below.
下表是存储的加密研究(refine.js),与上方的精度开关联动:每一层的 L_h、dL/du∞、48 个基准网格坐标上的 max|dL/dX|(细网格的形状梯度经细分映射的转置送回基准网格),以及离散伴随与射线参考之差(格点页的 L2 差与附录 B 一样不计边界节点)。最后两列比较内边界入流面旁的伴随误差(格点页只取两侧都是入流半面的节点:入流与出流交界处的特征角点上,参考值取出流一侧的末端值,不作为最大范数的比较点):目标用 u⁺ 时它很小且不随加密增长,用内侧迹 u⁻ 时它随加密增大。
The table below is the stored refinement study (refine.js), following the order selector above: per level, L_h, dL/du∞, max|dL/dX| over the 48 coordinates of the base mesh (the fine-mesh shape gradient sent back through the transpose of the subdivision map), and the difference between the discrete adjoint and the ray reference (on the node page the L2 difference leaves out the boundary nodes, as in Appendix B). The last two columns compare the adjoint error next to inner inflow faces (on the node page only nodes whose half-faces are all inflow: at a characteristic corner between inflow and outflow the reference takes the outflow terminal value and is not a max-norm datum): small and not growing with u⁺ in the objective, growing with refinement with the interior trace u⁻.
/* Continuous reference from boundary intersections alone; no discrete Jacobian. */
(function(root){function reference(mesh,p,node=-1){const beta=[1,.5],hits=[];for(const cell of mesh.C2N)for(let j=0;j<cell.length;j++){const ia=cell[j],ib=cell[(j+1)%cell.length],key=[Math.min(ia,ib),Math.max(ia,ib)].join('-'),bc=mesh.bnd[key];if(!bc)continue;const A=mesh.X[ia-1],B=mesh.X[ib-1],d=[B[0]-A[0],B[1]-A[1]],q=[A[0]-p[0],A[1]-p[1]],vn=d[1]-.5*d[0];if(Math.abs(vn)<1e-12)continue;const t=(q[0]*d[1]-q[1]*d[0])/vn,z=(q[0]*beta[1]-q[1]*beta[0])/vn;if(node>=0&&(ia-1===node||ib-1===node)&&vn>0)hits.push({t:0,bc,hit:p.slice()});else if(t>1e-9&&z>=-1e-10&&z<=1+1e-10)hits.push({t,bc,hit:[p[0]+t,p[1]+.5*t]});}if(!hits.length)throw Error('No characteristic terminal intersection');hits.sort((a,b)=>a.t-b.t);return {...hits[0],psi:hits[0].bc==='wall'?-1/Math.sqrt(1.25):0};}if(typeof module!=='undefined')module.exports=reference;else root.TransportRay=reference;})(typeof window!=='undefined'?window:globalThis);