简介:本资源是一套基于有限体积法(FVM)求解NACA0012翼型欧拉方程流场的MATLAB实现代码包,面向计算流体力学初学者、航空航天专业本科生及数值方法实践者,聚焦无粘可压缩流体模拟这一典型CFD教学与科研场景。压缩包共21个.m文件,涵盖网格生成(mesh_axis.m、n_grid.m)、通量计算(WENO5LF.m、fs_vanl.m、flux2.m)、边界条件设置(set_boundary.m)、时间推进主程序(main.m)及可视化模块(fdisplay.m)等完整求解链环节,总大小仅17KB,轻量易读,适合逐行调试与算法原理验证。已有680人学习下载,资源结构清晰、模块职责明确,附带多版本通量差分格式(如WENO5LF2d、dflux_.m)与对比测试例程(test_routines.m),便于理解高阶格式对激波捕捉的影响及FVM在复杂几何边界上的离散实现逻辑。
1. 有限体积法不是“把微分方程切块就完事”:它为什么是CFD仿真里最扛造、最易落地的离散化骨架?
你手头有个带复杂边界的散热器模型,网格画好了,但用中心差分一跑就发散;或者在做多相流界面追踪时,守恒量总在迭代中悄悄“蒸发”——这时候翻教材看到“有限体积法(FVM)”,第一反应可能是:“哦,又是种数值方法”。但真实工程现场里,FVM从来不是教科书里的一个名词选项,而是唯一能让你在非结构网格、强非线性、物理量突变场景下,稳住质量/动量/能量三重守恒底线的离散化骨架。它不追求解在节点上的高阶光滑性,而死磕每个控制体上通量的净流入=该体积分变化率——这个“守恒即正义”的底层契约,让它成为OpenFOAM、ANSYS Fluent、COMSOL等工业级CFD求解器默认且不可绕过的底层引擎。本篇不讲泛泛的积分推导,只聚焦一线工程师用FVM从零搭起一个可验证、可调试、可嵌入自定义物理模型的最小可行路径:从控制体构建、通量重构、离散方程组装,到边界条件硬编码与残差监控闭环。适合正在啃CFD代码、调试自研求解器、或被商业软件黑匣子卡住脖子的流体力学/热仿真/电化学建模从业者。
2. 用Python手写一个二维稳态对流-扩散方程的FVM求解器:从控制体划分到离散方程生成
有限体积法的起点不是数学公式,而是物理空间的切割逻辑。我们不碰复杂的非结构网格生成器,直接用最直白的二维结构化网格——这恰恰是理解FVM本质的黄金入口:网格越简单,通量计算和守恒验证越透明,翻车时定位越快。
2.1 构建双层网格:节点网格(Node Grid)与控制体中心网格(CV Center Grid)
FVM的核心控制体(Control Volume, CV)必须包围每个计算节点。在结构化网格中,最常用的是交错网格(Staggered Grid):速度分量u、v分别定义在x、y方向面心,而压力p、温度T等标量定义在单元中心。但为降低入门门槛,我们先采用同位网格(Colocated Grid)——所有变量都定义在同一个节点上,后续再补上压力-速度耦合的修正技巧。
import numpy as np def create_colocated_grid(nx, ny, Lx, Ly): """ 生成二维结构化同位网格 :param nx: x方向节点数(含边界) :param ny: y方向节点数(含边界) :param Lx: x方向物理长度 :param Ly: y方向物理长度 :return: x_nodes, y_nodes: 节点坐标矩阵 (ny, nx) dx, dy: 网格步长(标量,假设均匀) """ x_nodes = np.linspace(0, Lx, nx) y_nodes = np.linspace(0, Ly, ny) X, Y = np.meshgrid(x_nodes, y_nodes, indexing='ij') # 注意:'ij'使X为i方向(x),Y为j方向(y) dx = Lx / (nx - 1) # 均匀网格,dx = Δx dy = Ly / (ny - 1) # dy = Δy return X, Y, dx, dy # 示例:创建11×11节点网格(10×10个控制体) X, Y, dx, dy = create_colocated_grid(nx=11, ny=11, Lx=1.0, Ly=1.0)逻辑说明:
np.meshgrid(..., indexing='ij')是关键。它让X[i,j]对应第i个x节点、第j个y节点的坐标(i从0到nx-1,j从0到ny-1),这与后续离散方程中phi[i,j]的索引完全一致。若用默认'xy',索引顺序会颠倒,导致通量拼接错位——这是新手最常踩的“玄学bug”,调试三天找不到原因。
2.2 定义控制体(CV)及其面:东、西、北、南四面通量接口
每个内部节点(i,j)对应一个矩形控制体,其四条边即为通量进出的界面。注意:控制体边界位于相邻节点中点。例如,节点(i,j)的东界面(East face)位于x = (X[i,j] + X[i+1,j])/2,其面积为dy(二维中面为线段,面积即长度)。
def get_cv_faces(i, j, X, Y, dx, dy, nx, ny): """ 获取节点(i,j)所在控制体的四个面中心坐标及面面积 返回字典,键为 'east', 'west', 'north', 'south' 每个值为 (face_x, face_y, area) 元组 """ faces = {} # East face: 位于 i 和 i+1 之间,j 行 if i < nx - 1: xe = (X[i, j] + X[i+1, j]) / 2.0 ye = Y[i, j] area_e = dy # 二维中,面是垂直于x轴的线段,长度=dy faces['east'] = (xe, ye, area_e) else: faces['east'] = None # 边界节点无东面 # West face: 位于 i-1 和 i 之间 if i > 0: xw = (X[i-1, j] + X[i, j]) / 2.0 yw = Y[i, j] area_w = dy faces['west'] = (xw, yw, area_w) else: faces['west'] = None # North face: 位于 j 和 j+1 之间,i 列 if j < ny - 1: xn = X[i, j] yn = (Y[i, j] + Y[i, j+1]) / 2.0 area_n = dx faces['north'] = (xn, yn, area_n) else: faces['north'] = None # South face: 位于 j-1 和 j 之间 if j > 0: xs = X[i, j] ys = (Y[i, j-1] + Y[i, j]) / 2.0 area_s = dx faces['south'] = (xs, ys, area_s) else: faces['south'] = None return faces # 验证:取中心节点 (5,5),打印其四面 faces_55 = get_cv_faces(5, 5, X, Y, dx, dy, 11, 11) print("Node (5,5) CV faces:", faces_55)参数说明:
area_e = dy这一行看似反直觉(为什么不是dy * 1?),是因为我们在二维平面问题中,将问题视为单位厚度(z方向=1),故面面积即为面在xy平面的长度。若拓展到三维,此处应为dy * dz。此简化是FVM二维教学实现的通用约定,但务必在代码注释中明确,避免未来升级三维时遗忘乘dz。
2.3 离散对流-扩散方程:通量 = 对流项 + 扩散项,每面独立计算
我们以标量输运方程为例: [ \nabla \cdot (\rho \mathbf{U} \phi) = \nabla \cdot (\Gamma \nabla \phi) + S_\phi ] 其中 (\phi) 是待求量(如温度),(\rho) 密度,(\mathbf{U}) 速度场,(\Gamma) 扩散系数,(S_\phi) 源项。
对控制体积分后,离散形式为: [ \sum_{\text{faces}} F_\phi = \sum_{\text{faces}} D_\phi + V \cdot S_\phi ] 其中 (F_\phi = (\rho \mathbf{U} \cdot \mathbf{n}) \phi) 是对流通量,(D_\phi = \Gamma \frac{\partial \phi}{\partial n}) 是扩散通量,(V = dx \cdot dy) 是控制体体积。
关键在于:每个面的 (\phi) 值不能直接用节点值,需通过插值获得。我们采用最稳健的中心差分插值(CD)计算扩散通量,对流项则用迎风格式(Upwind)避免数值振荡:
def compute_face_flux(phi, U, V, Gamma, S, i, j, face, dx, dy, nx, ny): """ 计算单个面(east/west/north/south)上的总通量 F_phi - D_phi :param phi: 标量场 (nx, ny) 矩阵 :param U: x方向速度场 (nx, ny),U[i,j] 在节点(i,j) :param V: y方向速度场 (nx, ny) :param Gamma: 扩散系数标量或 (nx, ny) 矩阵 :param S: 源项标量或 (nx, ny) 矩阵 :param face: 'east', 'west', 'north', 'south' :return: face_flux: 标量,该面净通量(流入为正) """ # 获取该面信息 face_info = get_cv_faces(i, j, X, Y, dx, dy, nx, ny)[face] if face_info is None: return 0.0 _, _, area = face_info # 1. 对流通量 F_phi = (rho * U_normal) * phi_face # 假设 rho=1.0,U_normal 为速度在面法向的分量 if face == 'east': U_normal = U[i, j] # u在东面法向(+x)分量 # phi_face 插值:东面介于 (i,j) 和 (i+1,j),用线性插值 if i < nx - 1: phi_face = 0.5 * (phi[i, j] + phi[i+1, j]) else: phi_face = phi[i, j] # 边界,取自身 # 迎风:若U_normal > 0,上游是西边节点(i,j),否则是东边(i+1,j) if U_normal > 0: phi_face = phi[i, j] else: phi_face = phi[i+1, j] F_phi = U_normal * phi_face * area elif face == 'west': U_normal = U[i-1, j] # u在西面法向(-x)分量,取左邻节点u if i > 0: if U_normal > 0: phi_face = phi[i-1, j] else: phi_face = phi[i, j] F_phi = U_normal * phi_face * area else: F_phi = 0.0 elif face == 'north': U_normal = V[i, j] # v在北面法向(+y)分量 if j < ny - 1: if U_normal > 0: phi_face = phi[i, j] else: phi_face = phi[i, j+1] F_phi = U_normal * phi_face * area else: F_phi = 0.0 elif face == 'south': U_normal = V[i, j-1] # v在南面法向(-y)分量,取下邻节点v if j > 0: if U_normal > 0: phi_face = phi[i, j-1] else: phi_face = phi[i, j] F_phi = U_normal * phi_face * area else: F_phi = 0.0 else: F_phi = 0.0 # 2. 扩散通量 D_phi = Gamma * (dphi/dn) * area # dphi/dn 用中心差分:(phi_upstream - phi_downstream) / distance if face == 'east': if i < nx - 1: dphi_dn = (phi[i+1, j] - phi[i, j]) / dx else: dphi_dn = 0.0 # 边界,设梯度为0(Neumann) D_phi = -Gamma * dphi_dn * area # 负号:扩散通量与梯度反向 elif face == 'west': if i > 0: dphi_dn = (phi[i, j] - phi[i-1, j]) / dx else: dphi_dn = 0.0 D_phi = -Gamma * dphi_dn * area elif face == 'north': if j < ny - 1: dphi_dn = (phi[i, j+1] - phi[i, j]) / dy else: dphi_dn = 0.0 D_phi = -Gamma * dphi_dn * area elif face == 'south': if j > 0: dphi_dn = (phi[i, j] - phi[i, j-1]) / dy else: dphi_dn = 0.0 D_phi = -Gamma * dphi_dn * area else: D_phi = 0.0 # 3. 总面通量 = 对流 + 扩散 total_flux = F_phi + D_phi return total_flux # 初始化phi场(全0),U,V场(简单剪切流) phi = np.zeros((11, 11)) U = np.ones((11, 11)) * 0.5 # u=0.5 everywhere V = np.zeros((11, 11)) # v=0 Gamma = 0.01 S = np.zeros((11, 11)) # 计算节点(5,5)的东面通量 flux_east_55 = compute_face_flux(phi, U, V, Gamma, S, 5, 5, 'east', dx, dy, 11, 11) print(f"Node (5,5) East face flux: {flux_east_55:.6f}")逻辑说明:这段代码暴露了FVM最核心的“手工感”——每个面的通量都要单独写逻辑。
if face == 'east': ... elif face == 'west': ...不是代码坏味道,而是物理本质:东面的法向、速度分量、插值邻居、距离,全部与其他面不同。强行抽象成一个函数反而掩盖物理意义,增加调试难度。这也是为什么工业求解器源码里,fvm::surfaceIntegrate或fvPatchField的实现总是按面枚举。
3. 组装全局离散方程组:从面通量到系数矩阵A与右端项b
有了每个面的通量计算能力,下一步就是将所有控制体的守恒方程汇成线性方程组 ( A \cdot \phi = b )。这里没有捷径:必须遍历每个内部节点(i,j),收集其四面通量贡献,并将其整理为对\(\phi[i,j]\)、\(\phi[i±1,j]\)、\(\phi[i,j±1]\)的系数。
3.1 系数矩阵A的稀疏结构:五对角(2D)与七对角(3D)的必然性
在二维结构化网格中,每个内部节点最多与4个邻居耦合(上、下、左、右),因此离散后的系数矩阵A是五对角矩阵(pentadiagonal)。其非零元素仅出现在主对角线及上下各两条次对角线上。使用scipy.sparse.dia_matrix或csr_matrix存储是标准做法,但为透彻理解,我们先用密集矩阵演示(小规模验证用):
from scipy.sparse import lil_matrix from scipy.sparse.linalg import spsolve def assemble_fvm_matrix(nx, ny, dx, dy, U, V, Gamma, S): """ 组装FVM离散后的稀疏系数矩阵A和右端项b :return: A (sparse), b (dense vector) """ N = nx * ny # 总自由度数 A = lil_matrix((N, N), dtype=np.float64) b = np.zeros(N) # 遍历每个节点 (i,j),映射到全局索引 k = i*ny + j (C风格行优先) for i in range(nx): for j in range(ny): k = i * ny + j # 当前节点全局索引 # 初始化该行系数:a_P (主对角线) 和 a_nb (邻居) a_P = 0.0 # phi[i,j] 的系数 a_W = 0.0 # phi[i-1,j] a_E = 0.0 # phi[i+1,j] a_S = 0.0 # phi[i,j-1] a_N = 0.0 # phi[i,j+1] b_k = 0.0 # 右端项(含源项、边界通量) # 1. 处理东面通量对 a_P 和 a_E 的贡献 # 东面通量 = F_east + D_east = (U_e * phi_face) + (-Gamma * dphi/dx * dy) # 对于中心差分扩散:D_east = -Gamma * (phi[i+1,j]-phi[i,j])/dx * dy # => D_east = (-Gamma*dy/dx)*phi[i+1,j] + (Gamma*dy/dx)*phi[i,j] # 对流项迎风:若U_e>0,则F_east = U_e * phi[i,j] => 贡献 +U_e*dy to a_P # 若U_e<0,则F_east = U_e * phi[i+1,j] => 贡献 +U_e*dy to a_E if i < nx - 1: U_e = U[i, j] # 东面u速度 # 扩散项系数(中心差分) D_coeff = Gamma * dy / dx a_E -= D_coeff a_P += D_coeff # 对流项系数(迎风) if U_e > 0: a_P += U_e * dy else: a_E += U_e * dy # 2. 西面通量:类似,注意U_w取U[i-1,j] if i > 0: U_w = U[i-1, j] D_coeff = Gamma * dy / dx a_W -= D_coeff a_P += D_coeff if U_w > 0: a_W += U_w * dy else: a_P += U_w * dy # 3. 北面通量:用V[i,j],面积=dx if j < ny - 1: V_n = V[i, j] D_coeff = Gamma * dx / dy a_N -= D_coeff a_P += D_coeff if V_n > 0: a_P += V_n * dx else: a_N += V_n * dx # 4. 南面通量:用V[i,j-1] if j > 0: V_s = V[i, j-1] D_coeff = Gamma * dx / dy a_S -= D_coeff a_P += D_coeff if V_s > 0: a_S += V_s * dx else: a_P += V_s * dx # 5. 源项 S_phi * V,V = dx*dy b_k += S[i, j] * dx * dy # 6. 将系数填入稀疏矩阵A A[k, k] = a_P if i > 0: A[k, k-ny] = a_W # (i-1,j) -> k-ny if i < nx - 1: A[k, k+ny] = a_E # (i+1,j) -> k+ny if j > 0: A[k, k-1] = a_S # (i,j-1) -> k-1 if j < ny - 1: A[k, k+1] = a_N # (i,j+1) -> k+1 # 转换为CSR格式加速求解 A = A.tocsr() return A, b # 组装矩阵(小网格测试) A, b = assemble_fvm_matrix(11, 11, dx, dy, U, V, Gamma, S) print(f"Assembled matrix shape: {A.shape}, nnz: {A.nnz}")参数说明:
k = i * ny + j是C风格行优先索引(i是x方向索引,ny是y方向节点数)。这是FVM代码中最易出错的索引映射点之一。若用Fortran风格列优先(k = j * nx + i),整个邻居索引k±ny,k±1都要重写。务必在项目开头统一文档注明索引约定,并在所有循环中严格遵守。
3.2 边界条件的硬编码实现:Dirichlet与Neumann的两种注入方式
边界条件不是“加在方程后面”,而是直接改写系数矩阵A和右端项b的对应行。这是FVM落地最关键的实操细节。
Dirichlet边界(固定值):例如左边界
phi[0,j] = phi_L。此时节点(0,j)的方程不再求解,而是将phi[0,j]的已知值“移项”到右端项b中,同时清空该行除对角元外的所有系数,并将对角元设为1。Neumann边界(固定梯度):例如右边界
∂φ/∂x = 0(绝热)。这通过修改东面通量实现——将东面扩散通量中的dφ/dx替换为0,即D_east = 0,从而不产生额外系数。
def apply_dirichlet_bc(A, b, phi_field, nx, ny, boundary_type, value, location): """ 应用Dirichlet边界条件 :param boundary_type: 'left', 'right', 'bottom', 'top' :param location: 索引列表,如 left->i=0, all j :param value: 标量或 (len(location),) 向量 """ A = A.tolil() # 转为LIL格式便于逐行修改 if boundary_type == 'left': for j in range(ny): k = 0 * ny + j # i=0, j # 清空整行,只留对角元=1 A[k, :] = 0 A[k, k] = 1.0 b[k] = value if np.isscalar(value) else value[j] elif boundary_type == 'right': for j in range(ny): k = (nx-1) * ny + j A[k, :] = 0 A[k, k] = 1.0 b[k] = value elif boundary_type == 'bottom': for i in range(nx): k = i * ny + 0 A[k, :] = 0 A[k, k] = 1.0 b[k] = value elif boundary_type == 'top': for i in range(nx): k = i * ny + (ny-1) A[k, :] = 0 A[k, k] = 1.0 b[k] = value return A.tocsr(), b # 应用左边界phi=1.0,右边界phi=0.0 A, b = apply_dirichlet_bc(A, b, phi, 11, 11, 'left', 1.0, None) A, b = apply_dirichlet_bc(A, b, phi, 11, 11, 'right', 0.0, None) # 求解 phi_vec = spsolve(A, b) phi_solution = phi_vec.reshape((11, 11)) # 恢复为二维 print("Solution solved. Max phi:", phi_solution.max(), "Min phi:", phi_solution.min())逻辑说明:
apply_dirichlet_bc函数中A[k, :] = 0是精髓。它抹去了该边界节点与其他所有节点的耦合关系,强制其解等于给定值。这比在迭代求解器中“固定值”更底层、更可靠。很多初学者试图在Jacobi或Gauss-Seidel迭代中跳过边界节点更新,结果因内部节点仍受边界影响而收敛缓慢甚至发散——真正的边界条件,必须在离散方程组层面硬编码。
4. 避坑:FVM实现中5个血泪经验总结(现象→原因→解决)
FVM看似原理清晰,但工程落地时,90%的时间花在排查以下五类“幽灵bug”上。这些不是理论错误,而是离散化过程中的物理-数值失配。
4.1 现象:残差(Residual)震荡不降,或迭代1000步后解明显不守恒
原因:通量计算中,对流项与扩散项的符号约定不一致。例如,对流项定义为F = (U·n) * phi(n为面外法向),而扩散项却用了D = Gamma * (dphi/dn)(dn为面内法向),导致两者在面法向定义上相反,通量叠加时符号抵消错误。
解决:统一法向定义。在代码中明确定义face_normal = +1表示从当前CV指向外部,则对流项F = (U·n) * phi,扩散项D = -Gamma * (dphi/dn)(负号源于Fick定律)。所有面的n必须按同一套规则生成(如东面n=+x,西面n=-x)。
4.2 现象:网格加密后解不收敛,或出现非物理振荡(Gibbs现象)
原因:对流项使用了中心差分(CD),而非迎风(Upwind)或混合格式(Hybrid)。CD在Peclet数Pe = |U|*dx/Gamma > 2时必然振荡。
解决:强制对流项用一阶迎风。若需更高精度,改用二阶迎风(QUICK)或TVD格式,但必须配套通量限制器(limiter)。永远不要在未评估Pe数前用CD。可在预处理中计算每个面的Pe_face并打印最大值。
4.3 现象:施加Dirichlet边界后,相邻内部节点的解异常跳变(如边界φ=1,第一个内点φ=0.3)
原因:边界节点的“控制体”被错误地当作完整CV处理。例如,左边界节点(0,j)的控制体只有半宽(dx/2),但代码中仍用dx*dy计算体积,且通量计算仍用全尺寸面面积,导致守恒失衡。
解决:为边界节点定义半控制体(Half CV)。左边界节点的东面面积仍为dy,但西面不存在,体积为(dx/2)*dy。在assemble_fvm_matrix中,对边界节点单独计算体积V_cv并用于源项S*V_cv。
4.4 现象:并行计算时,分区边界(processor boundary)处解不连续,残差突增
原因:分区边界被当作物理边界处理,应用了默认Neumann(零梯度),但实际应传递相邻分区的phi值。
解决:在并行FVM中,分区边界是内部面(internal face),必须通过MPI通信交换phi值,并用其参与通量插值。这要求将边界节点分为owned和ghost两类,ghost节点值由邻居分区更新。OpenFOAM的processorFvPatchField即为此设计。
4.5 现象:求解器在某个时间步突然崩溃,报错“matrix is singular”
原因:存在孤立控制体——某个节点的所有面都是边界,且边界条件未完全指定(如只给了Neumann,没给Dirichlet)。此时方程组秩亏,解不唯一。
解决:在组装矩阵前,运行连通性检查(connected component analysis)。对每个节点,统计其有效面数(非None)。若某节点面数为0(如单点域),或所有面均为Neumann,则必须人工添加一个Dirichlet条件(哪怕设为0)来锚定解。这是CFD前处理的“后悔药”。
5. 验证与调试:用解析解、守恒律、网格收敛性三重校验你的FVM代码
写完FVM求解器,别急着跑新模型。先用三个低成本、高信度的方法把它“拷问”一遍。这比调参、换模型重要十倍。
5.1 解析解验证:一维稳态对流-扩散方程的精确解
一维无限域中,方程d/dx(U φ) = d/dx(Gamma dφ/dx)的解析解为: [ \phi(x) = C_1 + C_2 \exp\left( \frac{U}{\Gamma} x \right) ] 给定边界条件φ(0)=1,φ(L)=0,可得: [ \phi(x) = \frac{ \exp(Pe \cdot x/L) - \exp(Pe) }{ 1 - \exp(Pe) }, \quad Pe = \frac{U L}{\Gamma} ]
def analytical_1d(x, L, U, Gamma, phi_left=1.0, phi_right=0.0): """一维解析解""" Pe = U * L / Gamma if abs(Pe) < 1e-6: return phi_left + (phi_right - phi_left) * x / L exp_pe = np.exp(Pe) exp_term = np.exp(Pe * x / L) return (exp_term - exp_pe) / (1.0 - exp_pe) # 生成解析解 x_analy = np.linspace(0, 1.0, 100) phi_analy = analytical_1d(x_analy, 1.0, 0.5, 0.01) # 用我们的FVM解(1D简化版)对比 # (此处省略1D FVM组装,逻辑同2D,但只有一维循环) # phi_fvm_1d = solve_1d_fvm(...) # plt.plot(x_analy, phi_analy, 'r-', label='Analytical') # plt.plot(x_fvm, phi_fvm_1d, 'bo', label='FVM (nx=20)') # plt.legend(); plt.show() # 计算L2误差 # error_L2 = np.sqrt(np.mean((phi_fvm_1d - np.interp(x_fvm, x_analy, phi_analy))**2)) # print(f"L2 error: {error_L2:.2e}")价值点:解析解验证是唯一能确认你代码无系统性偏差的方法。若L2误差随网格加密(
nx增大)以O(dx²)下降,说明你的离散格式阶数正确;若误差不降甚至增大,说明通量符号、插值逻辑或边界处理有硬伤。
5.2 守恒律验证:全局质量/能量平衡的实时监控
FVM的灵魂是守恒。在每次迭代中,计算全局净通量与源项积分之和,它必须恒等于零(稳态)或等于全局时间变化率(瞬态)。
def check_global_conservation(phi, U, V, Gamma, S, dx, dy, nx, ny): """ 计算全局守恒残差:sum(Faces) - sum(S * V) 理论值应为0(稳态)或 ≈ d(sum(phi*V))/dt(瞬态) """ total_flux = 0.0 total_source = 0.0 V_total = dx * dy * nx * ny # 遍历所有面(只算一次,避免重复) for i in range(nx): for j in range(ny): # 东面:只被节点(i,j)拥有,当i<nx-1时 if i < nx - 1: flux = compute_face_flux(phi, U, V, Gamma, S, i, j, 'east', dx, dy, nx, ny) total_flux += flux # 西面:只被节点(i,j)拥有,当i>0时 if i > 0: flux = compute_face_flux(phi, U, V, Gamma, S, i, j, 'west', dx, dy, nx, ny) total_flux += flux # 北面:只被节点(i,j)拥有,当j<ny-1时 if j < ny - 1: flux = compute_face_flux(phi, U, V, Gamma, S, i, j, 'north', dx, dy, nx, ny) total_flux += flux # 南面:只被节点(i,j)拥有,当j>0时 if j > <p> <a href="https://download.csdn.net/download/weixin_42691065/27525013" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>