diff --git a/.gitignore b/.gitignore index cd101a39..29481f85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/build/* oneflow_bin/* **/project_tmp/* +**/__pycache__/* diff --git a/example/1d-linear-convection/weno3/python/02/weno3.py b/example/1d-linear-convection/weno3/python/02/weno3.py new file mode 100644 index 00000000..c3e7395e --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02/weno3.py @@ -0,0 +1,576 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.nx): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, 0:cfd.ntcell-1] = q[0:cfd.ntcell-1] + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcell-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nx + 1): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nx + 1): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nx + 1): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied + 1): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 2): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied+1]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def visualizeEno(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied+1]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order ENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + self.ist = 0 + self.ishift + self.ied = self.nx - 1 + self.ishift + self.ntcell = self.nx + 2 * self.ishift + self.isize = iorder * (iorder + 1) + + self.il = np.zeros(self.nx + 1, dtype=int) + self.ir = np.zeros(self.nx + 1, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcell)) + self.up1_2m = np.zeros(self.nx + 1) + self.up1_2p = np.zeros(self.nx + 1) + self.flux = np.zeros(self.nx + 1) + self.res = np.zeros(self.nx) + + # Field module variables + self.u = np.zeros(self.ntcell) + self.un = np.zeros(self.ntcell) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def performEnoOrderAnalysis(): + iorder_max = 7 + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + u_list = [] + for iorder in range(1, iorder_max+1): + u = RunEno(solver, mesh, iorder) + u_list.append(u) + + plot_eno_OrderAnalysis(solver, mesh.xcc, u_list, u_analytical) + +def performEnoTimestepAnalysis(): + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + u_list = [] + dt_list = [] + solver.dt = 0.025/4 + #solver.dt = 0.025/16 + n = 12 + solver.rk = 3 + iorder = 7 + for i in range(0, n): + u = RunEno(solver, mesh, iorder) + u_list.append(u) + dt_list.append(solver.dt) + print(f'i={i+1},N={n},T={solver.T:.3f},dt={solver.dt},nt={int(solver.T/solver.dt)},runge_kutta{solver.rk},ENO{iorder},{fluxnames[solver.iflux]} FLUX') + solver.dt /= 2 + + plot_eno_TimestepAnalysis(solver, mesh.xcc, u_list, u_analytical, dt_list, iorder) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_eno_OrderAnalysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using [1-7]th-order ENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + lable = 'Numerical (Rusanov)ENO' + str(i+1) + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02a/weno3.py b/example/1d-linear-convection/weno3/python/02a/weno3.py new file mode 100644 index 00000000..352bb148 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02a/weno3.py @@ -0,0 +1,483 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.nx): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, 0:cfd.ntcell-1] = q[0:cfd.ntcell-1] + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcell-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nx + 1): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nx + 1): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nx + 1): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied + 1): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 2): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nx + 1): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.nx): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied+1]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + self.ist = 0 + self.ishift + self.ied = self.nx - 1 + self.ishift + self.ntcell = self.nx + 2 * self.ishift + self.isize = iorder * (iorder + 1) + + self.il = np.zeros(self.nx + 1, dtype=int) + self.ir = np.zeros(self.nx + 1, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcell)) + self.up1_2m = np.zeros(self.nx + 1) + self.up1_2p = np.zeros(self.nx + 1) + self.flux = np.zeros(self.nx + 1) + self.res = np.zeros(self.nx) + + # Field module variables + self.u = np.zeros(self.ntcell) + self.un = np.zeros(self.ntcell) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied+1]) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + #计算理论解 + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02b/weno3.py b/example/1d-linear-convection/weno3/python/02b/weno3.py new file mode 100644 index 00000000..60b76a2a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02b/weno3.py @@ -0,0 +1,489 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, 0:cfd.ntcells-1] = q[0:cfd.ntcells-1] + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied -1 + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied - 1 + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift + self.ied = self.ncells + self.ishift + self.ntcells = self.ncells + 2 * self.ishift + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + self.up1_2m = np.zeros(self.nnodes) + self.up1_2p = np.zeros(self.nnodes) + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) + + # Field module variables + self.u = np.zeros(self.ntcells) + self.un = np.zeros(self.ntcells) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02c/weno3.py b/example/1d-linear-convection/weno3/python/02c/weno3.py new file mode 100644 index 00000000..f73a9fad --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02c/weno3.py @@ -0,0 +1,490 @@ +import numpy as np +import matplotlib.pyplot as plt + +# 初始条件 +def initial_condition(x): + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +# 理论解 +def analytical_solution(x, t, a, L): + # 初始条件沿 x - at 平移 + x_shifted = x - a * t + return initial_condition((x_shifted + L) % L) # 周期边界条件 + +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i + 1] - cfd.flux[i]) / cfd.mesh.dx + +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +def EnoReconstruction(q, cfd): + # Choose the stencil by ENO method + cfd.dd[0, :] = q + + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruction u(j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +#---------------------------------------------------------------------------# +#nonlinear weights for upwind direction +#---------------------------------------------------------------------------# +def wc3L(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +#---------------------------------------------------------------------------# +#nonlinear weights for downwind direction +#---------------------------------------------------------------------------# +def wc3R(v1,v2,v3): + eps = 1.0e-6 + + # smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # computing nonlinear weights w1,w2 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # candiate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + +def weno3L_periodic(cfd,u,f): + #i:ist-1,ist,...,ied + #j:0,1,...,nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +def weno3R_periodic(cfd,u,f): + #i:ist,ist+1,...,ied,ied+1 + #j:0,1,...,nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + + +def WenoReconstruction(q, cfd): + # Reconstruction u(j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + + cp = 0.5 * ( cfd.solver.c + abs(cfd.solver.c) ) + cm = 0.5 * ( cfd.solver.c - abs(cfd.solver.c) ) + + flux[i] = cp * u_L + cm * u_R + +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # 左状态通量 + F_R = cfd.solver.c * u_R # 右状态通量 + alpha = abs(cfd.solver.c) # 最大波速 + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + + +def boundary(u, cfd): + for i in range(-cfd.ighost, 1): + u[cfd.ist - 1 + i] = u[cfd.ied -1 + i] + for i in range(1, cfd.ighost + 2): + u[cfd.ied - 1 + i] = u[cfd.ist - 1 + i] + +def update_oldfield(qn, q): + qn[:] = q[:] + +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # 可视化 + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + #计算理论解 + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Solver: + def __init__(self): + #interpolation :0 Eno; 1 Weno + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift + self.ied = self.ncells + self.ishift + self.ntcells = self.ncells + 2 * self.ishift + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + self.up1_2m = np.zeros(self.nnodes) + self.up1_2p = np.zeros(self.nnodes) + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) + + # Field module variables + self.u = np.zeros(self.ntcells) + self.un = np.zeros(self.ntcells) + +def RunEno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},cfd{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def RunWeno(solver, mesh, iorder): + cfd = Cfd(solver, mesh, iorder) + init_coef(cfd.iorder, cfd.coef) + init_field(cfd) + + simu_time = solver.T + t = 0.0 + dt = solver.dt + while t < simu_time: + runge_kutta(cfd) + if t + dt > simu_time: + dt = simu_time - t + t += dt + print(f'T={t:.3f},runge_kutta{solver.rk},WENO{cfd.iorder},{fluxnames[solver.iflux]} FLUX') + return np.copy(cfd.u[cfd.ist:cfd.ied]) + +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + solver.interpolation = 0 + u = RunEno(solver, mesh, solver.iorder) + u_list.append(u) + solver.interpolation = 1 + u = RunWeno(solver, mesh, solver.iorder) + u_list.append(u) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # 定义一个包含不同颜色、线形和标记的列表 + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02d/weno3.py b/example/1d-linear-convection/weno3/python/02d/weno3.py new file mode 100644 index 00000000..437652c9 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02d/weno3.py @@ -0,0 +1,526 @@ +import numpy as np +import matplotlib.pyplot as plt + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + # Select right-biased stencil for each node + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.iflux == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.c * u_L # Flux from left state + F_R = cfd.solver.c * u_R # Flux from right state + alpha = abs(cfd.solver.c) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.solver.c + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + + u_L = up1_2m[i] + u_R = up1_2p[i] + + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Get ordinal suffix for number formatting (st, nd, rd, th) +def get_ordinal_numbers(order): + if order == 1: + return 'st' + elif order == 2: + return 'nd' + elif order == 3: + return 'rd' + else: + return 'th' + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.T, cfd.solver.c, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + ordinal1 = get_ordinal_numbers(cfd.iorder) + ordinal2 = get_ordinal_numbers(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.T:.3f} using {cfd.iorder}{ordinal1}-order WENO and {cfd.solver.rk}{ordinal2}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +# Solver class: stores numerical method parameters +class Solver: + def __init__(self): + # interpolation: 0 for ENO, 1 for WENO + self.interpolation = 0 + self.iflux = 0 + self.rk = 1 + self.c = 1.0 + self.T = 0.625 + #self.dt = .0025 + self.dt = .025 + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder # Number of ghost cells + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + + # Interface values and fluxes + self.up1_2m = np.zeros(self.nnodes) # Left interface value + self.up1_2p = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.iorder, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.solver.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.solver.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.solver.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = Solver() + u_analytical = analytical_solution(mesh.xcc, solver.T, solver.c, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + # ENO + solver.interpolation = 0 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_eno = run_simulation(cfd, solver.T) + u_list.append(u_eno) + + # WENO + solver.interpolation = 1 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_weno = run_simulation(cfd, solver.T) + u_list.append(u_weno) + + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + ordinal = get_ordinal_numbers(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.T:.3f} using 3rd-order ENO&WENO and {solver.rk}{ordinal}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02e/weno3.py b/example/1d-linear-convection/weno3/python/02e/weno3.py new file mode 100644 index 00000000..6184625e --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02e/weno3.py @@ -0,0 +1,515 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.up1_2m, cfd.up1_2p, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.solver.interpolation == 0: + EnoReconstruction(q, cfd) + elif cfd.solver.interpolation == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.iorder): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + # Select right-biased stencil for each node + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.iorder): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.up1_2m[i] = 0 + cfd.up1_2p[i] = 0 + for m in range(cfd.iorder): + cfd.up1_2m[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.up1_2p[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.up1_2m ) + weno3R_periodic( cfd, q, cfd.up1_2p ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(up1_2m, up1_2p, flux, cfd): + if cfd.solver.flux_type == 0: + rusanov_flux(up1_2m, up1_2p, flux, cfd) + else: + engquist_osher_flux(up1_2m, up1_2p, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(up1_2m, up1_2p, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = up1_2m[i] + u_R = up1_2p[i] + F_L = cfd.solver.wave_speed * u_L # Flux from left state + F_R = cfd.solver.wave_speed * u_R # Flux from right state + alpha = abs(cfd.solver.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(up1_2m, up1_2p, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.solver.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = up1_2m[i] + u_R = up1_2p[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( iorder, coef ): + if iorder == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif iorder == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif iorder == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif iorder == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif iorder == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif iorder == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif iorder == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk = cfd.solver.rk + if rk == 1: + runge_kutta_1(cfd) + elif rk == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.solver.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.solver.final_time, cfd.solver.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.iorder) + rk_str = p.ordinal(cfd.solver.rk) + plt.title(f'1D Convection Equation at t = {cfd.solver.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.interpolation = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, solver, mesh, iorder): + self.solver = solver + self.mesh = mesh + self.nx = mesh.nx + self.iorder = iorder + self.ighost = iorder # Number of ghost cells + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.iorder={self.iorder}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((iorder + 1, iorder)) + self.dd = np.zeros((iorder, self.ntcells)) + + # Interface values and fluxes + self.up1_2m = np.zeros(self.nnodes) # Left interface value + self.up1_2p = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.iorder, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.solver.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.solver.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.solver.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + solver = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, solver.final_time, solver.wave_speed, mesh.L) + + solver.rk = 1 + solver.dt = 0.0025 + solver.iorder = 3 + + u_list = [] + # ENO + solver.interpolation = 0 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_eno = run_simulation(cfd, solver.final_time) + u_list.append(u_eno) + + # WENO + solver.interpolation = 1 + cfd = Cfd(solver, mesh, iorder=3) + init_field(cfd) + u_weno = run_simulation(cfd, solver.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(solver, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(solver, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(solver.rk) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {solver.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02f/weno3.py b/example/1d-linear-convection/weno3/python/02f/weno3.py new file mode 100644 index 00000000..5a1d33ee --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02f/weno3.py @@ -0,0 +1,549 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.config.reconstruction_scheme == 0: + EnoReconstruction(q, cfd) + elif cfd.config.reconstruction_scheme == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(0, cfd.ntcells-1): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.nnodes): + cfd.il[i] = i - 1 + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.il[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.il[i]+cfd.ishift]): + cfd.il[i] -= 1 + + # Select right-biased stencil for each node + for i in range(cfd.nnodes): + cfd.ir[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.ir[i]-1+cfd.ishift]) <= abs(cfd.dd[m, cfd.ir[i]+cfd.ishift]): + cfd.ir[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.nnodes): + k1 = cfd.il[i] + k2 = cfd.ir[i] + l1 = i - k1 + l2 = i - k2 + cfd.q_face_left[i] = 0 + cfd.q_face_right[i] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[i] += q[k1 + cfd.ishift + m] * cfd.coef[l1, m] + cfd.q_face_right[i] += q[k2 + cfd.ishift + m] * cfd.coef[l2, m] + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstructionOld(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.5 * cfd.un[j] + 0.5 * cfd.u[j] + 0.5 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = cfd.u[j] + dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = 0.75 * cfd.un[j] + 0.25 * cfd.u[j] + 0.25 * dt * cfd.res[i] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ncells): + j = i + cfd.ishift + cfd.u[j] = c1 * cfd.un[j] + c2 * cfd.u[j] + c3 * dt * cfd.res[i] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ishift = self.ighost + 1 + #self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + config.rk_order = 1 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02g/weno3.py b/example/1d-linear-convection/weno3/python/02g/weno3.py new file mode 100644 index 00000000..e0d87613 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02g/weno3.py @@ -0,0 +1,513 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.config.reconstruction_scheme == 0: + EnoReconstruction(q, cfd) + elif cfd.config.reconstruction_scheme == 1: + WenoReconstruction(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + #for i in range(cfd.ist, cfd.ied): + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ishift = self.ighost + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ishift # Start index of physical cells + self.ied = self.ncells + self.ist # End index of physical cells + self.ntcells = self.ncells + 2 * self.ishift # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ishift={self.ishift}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + # Stencil selection arrays + self.il = np.zeros(self.nnodes, dtype=int) + self.ir = np.zeros(self.nnodes, dtype=int) + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02h/weno3.py b/example/1d-linear-convection/weno3/python/02h/weno3.py new file mode 100644 index 00000000..2aea59b1 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02h/weno3.py @@ -0,0 +1,552 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + if cfd.config.reconstruction_scheme == 0: + #EnoReconstruction(q, cfd) + cfd.reconstructor.reconstruct(q, cfd) + elif cfd.config.reconstruction_scheme == 1: + #WenoReconstruction(q, cfd) + cfd.reconstructor.reconstruct(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + #for i in range(cfd.ist, cfd.ied): + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + print(f"Reconstructor:reconstruct") + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + def reconstruct(self, q, cfd): + #print(f"EnoReconstructor:reconstruct") + EnoReconstruction(q, cfd) + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + #print(f"WenoReconstructor:reconstruct") + WenoReconstruction(q, cfd) + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ighost # Start index of physical cells + self.ied = self.ist + self.ncells # End index of physical cells + self.ntcells = self.ncells + 2 * self.ighost # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.createReconstructor() + + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + + def createReconstructor(self): + if self.config.reconstruction_scheme == 0: #ENO + self.reconstructor = EnoReconstructor(self.spatial_order, self.ntcells) + elif self.config.reconstruction_scheme == 1: #WENO + self.reconstructor = WenoReconstructor() + + #self.reconstructor.reconstruct() + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02i/weno3.py b/example/1d-linear-convection/weno3/python/02i/weno3.py new file mode 100644 index 00000000..52dc13ed --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02i/weno3.py @@ -0,0 +1,584 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# --------------------------------------------------------------------------- # +# Reconstruction methods +# --------------------------------------------------------------------------- # +def EnoReconstruction(q, cfd): + """ENO reconstruction of interface values""" + # Choose stencil by ENO method based on smoothest polynomial + cfd.dd[0, :] = q + + # Compute divided differences + for m in range(1, cfd.spatial_order): + for j in range(cfd.ntcells-m): + cfd.dd[m, j] = cfd.dd[m-1, j+1] - cfd.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + cfd.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(cfd.dd[m, cfd.lmc[i]-1]) < abs(cfd.dd[m, cfd.lmc[i]]): + cfd.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = cfd.lmc[i-1] + k2 = cfd.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * cfd.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * cfd.coef[r2, m] + +# WENO (Weighted Essentially Non-Oscillatory) reconstruction +def WenoReconstruction(q, cfd): + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +fluxnames = [ + 'Rusanov', + 'Engquist-Osher', +] + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + F_L = cfd.config.wave_speed * u_L # Flux from left state + F_R = cfd.config.wave_speed * u_R # Flux from right state + alpha = abs(cfd.config.wave_speed) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * alpha * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.ighost): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.ighost): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + #for i in range(cfd.ist, cfd.ied): + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Visualize numerical and analytical solutions +def visualize(cfd): + with open('solution.plt', 'w') as f: + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + f.write(f"{cfd.mesh.xcc[j]:20.10e}{cfd.u[i]:20.10e}\n") + + # Visualization setup + u_numerical = np.copy(cfd.u[cfd.ist:cfd.ied]) + print(f'u_numerical.size={u_numerical.size}') + print(f'cfd.mesh.xcc.size={cfd.mesh.xcc.size}') + # Compute analytical solution + u_analytical = analytical_solution(cfd.mesh.xcc, cfd.config.final_time, cfd.config.wave_speed, cfd.mesh.L) + print(f'u_analytical.size={u_analytical.size}') + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.scatter(cfd.mesh.xcc, u_numerical, facecolor="none", edgecolor="blue", s=20, linewidths=0.5, label=f'Numerical (Rusanov)') + plt.plot(cfd.mesh.xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + + p = inflect.engine() + iorder_str = p.ordinal(cfd.spatial_order) + rk_str = p.ordinal(cfd.config.rk_order) + plt.title(f'1D Convection Equation at t = {cfd.config.final_time:.3f} using {iorder_str}-order WENO and {rk_str}-order Runge-Kutta methods') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class SimulationConfig: + def __init__(self): + self.reconstruction_scheme = 0 # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + print(f"Reconstructor:reconstruct") + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + #EnoReconstruction(q, cfd) + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + self.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + #print(f"WenoReconstructor:reconstruct") + #WenoReconstruction(q, cfd) + + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.spatial_order = spatial_order + self.ighost = spatial_order # Number of ghost cells + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + self.ist = 0 + self.ighost # Start index of physical cells + self.ied = self.ist + self.ncells # End index of physical cells + self.ntcells = self.ncells + 2 * self.ighost # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.ighost={self.ighost}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.createReconstructor() + + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + init_coef(self.spatial_order, self.coef) + + def createReconstructor(self): + if self.config.reconstruction_scheme == 0: #ENO + self.reconstructor = EnoReconstructor(self.spatial_order, self.ntcells) + elif self.config.reconstruction_scheme == 1: #WENO + self.reconstructor = WenoReconstructor() + + #self.reconstructor.reconstruct() + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.reconstruction_scheme = 0 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.reconstruction_scheme = 1 + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/02j/weno3.py b/example/1d-linear-convection/weno3/python/02j/weno3.py new file mode 100644 index 00000000..ff51dd1c --- /dev/null +++ b/example/1d-linear-convection/weno3/python/02j/weno3.py @@ -0,0 +1,512 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + inviscid_flux(cfd.q_face_left, cfd.q_face_right, cfd.flux, cfd) + for i in range(cfd.ncells): + cfd.res[i] = -(cfd.flux[i+1] - cfd.flux[i]) / cfd.mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied + # j: 0, 1, ..., nx + for i in range(cfd.ist - 1, cfd.ied): + j = i - cfd.ist + 1 + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(cfd.ist, cfd.ied + 1): + j = i - cfd.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + for i in range(cfd.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + # Left ghost cells = right interior cells + for ig in range(cfd.nghosts): + u[cfd.ist - 1 - ig] = u[cfd.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(cfd.nghosts): + u[cfd.ied + ig] = u[cfd.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + for i in range(cfd.ist, cfd.ied): + j = i - cfd.ist + if 0.5 <= cfd.mesh.xcc[j] <= 1.0: + cfd.u[i] = 2.0 + else: + cfd.u[i] = 1.0 + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.5 * cfd.un[i] + 0.5 * cfd.u[i] + 0.5 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = cfd.u[i] + dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = 0.75 * cfd.un[i] + 0.25 * cfd.u[i] + 0.25 * dt * cfd.res[j] + boundary(cfd.u, cfd) + + residual(cfd.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(cfd.ist,cfd.ied): + j = i - cfd.ist + cfd.u[i] = c1 * cfd.un[i] + c2 * cfd.u[i] + c3 * dt * cfd.res[j] + boundary(cfd.u, cfd) + update_oldfield(cfd.un, cfd.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + # Select left-biased stencil for each node + for i in range(cfd.ist-1,cfd.ied+1): + self.lmc[i] = i + for m in range(1, cfd.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(cfd.ist,cfd.ied+1): + j = i - cfd.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + cfd.q_face_left[j] = 0 + cfd.q_face_right[j] = 0 + for m in range(cfd.spatial_order): + cfd.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + cfd.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + weno3L_periodic( cfd, q, cfd.q_face_left ) + weno3R_periodic( cfd, q, cfd.q_face_right ) + +class SimulationConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_mesh(self, mesh): + """设置网格""" + self.mesh = mesh + return self + + def with_reconstruction(self, scheme, order=None): + """设置重建方案""" + self.recon_scheme = scheme + if order is not None: + self.spatial_order = order + + if scheme == "weno" and order is None: + self.spatial_order = 5 # WENO默认5阶 + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh, spatial_order): + self.config = config + self.mesh = mesh + self.nx = mesh.nx + self.ncells = mesh.ncells + self.nnodes = mesh.nnodes + + self.spatial_order = spatial_order + self.nghosts = spatial_order # Number of ghost cells + self.ist = 0 + self.nghosts # Start index of physical cells + self.ied = self.ist + self.ncells # End index of physical cells + self.ntcells = self.ncells + 2 * self.nghosts # Total cells including ghost regions + print(f"self.ncells={self.ncells}") + print(f"self.spatial_order={self.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + self.createReconstructor() + + # Interface values and fluxes + self.q_face_left = np.zeros(self.nnodes) # Left interface value + self.q_face_right = np.zeros(self.nnodes) # Right interface value + self.flux = np.zeros(self.nnodes) + self.res = np.zeros(self.ncells) # Residual array + + # Solution arrays + self.u = np.zeros(self.ntcells) # Current solution + self.un = np.zeros(self.ntcells) # Previous time step solution + + def createReconstructor(self): + if self.config.recon_scheme == "eno": #ENO + self.reconstructor = EnoReconstructor(self.spatial_order, self.ntcells) + elif self.config.recon_scheme == "weno": #WENO + self.reconstructor = WenoReconstructor() + + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return cfd.u[cfd.ist:cfd.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + config = SimulationConfig() + + #config.rk_order = 1 + config.rk_order = 3 + config.dt = 0.0025 + + u_list = [] + # ENO + config.with_reconstruction("eno",3) + + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_eno = run_simulation(cfd, config.final_time) + u_list.append(u_eno) + + # WENO + config.with_reconstruction("weno",3) + + cfd = Cfd(config, mesh, spatial_order=3) + init_field(cfd) + u_weno = run_simulation(cfd, config.final_time) + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config.final_time, config.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03/weno3.py b/example/1d-linear-convection/weno3/python/03/weno3.py new file mode 100644 index 00000000..85f722b8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03/weno3.py @@ -0,0 +1,610 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + weno3L_periodic( cfd, q, solution.q_face_left ) + weno3R_periodic( cfd, q, solution.q_face_right ) + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, mesh, config): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.mesh = mesh + self.config = config + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, domain): + self.config = config + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = self._create_reconstructor() + + def _create_reconstructor(self): + if self.config.recon_scheme == "eno": #ENO + reconstructor = EnoReconstructor(self.config.spatial_order, self.domain.ntcells) + elif self.config.recon_scheme == "weno": #WENO + reconstructor = WenoReconstructor() + return reconstructor + + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + + domain = cfd.domain + solution = cfd.solution + + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + domain_eno3 = ComputationalDomain(mesh, config_eno3) + cfd_eno3 = Cfd(config_eno3, domain_eno3) + + + u_list = [] + # ENO + init_field(cfd_eno3) + u_eno = run_simulation(cfd_eno3, config_eno3.final_time) + u_list.append(u_eno) + + # WENO + + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + domain_weno3 = ComputationalDomain(mesh, config_weno3) + cfd_weno3 = Cfd(config_weno3, domain_weno3) + + init_field(cfd_weno3) + u_weno = run_simulation(cfd_weno3, config_weno3.final_time) + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03a/weno3.py b/example/1d-linear-convection/weno3/python/03a/weno3.py new file mode 100644 index 00000000..8d3f7068 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03a/weno3.py @@ -0,0 +1,625 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + weno3L_periodic( cfd, q, solution.q_face_left ) + weno3R_periodic( cfd, q, solution.q_face_right ) + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, mesh, config): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.mesh = mesh + self.config = config + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, domain): + self.config = config + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + +# --------------------------------------------------------------------------- # +# Simulation runners +# --------------------------------------------------------------------------- # +def run_simulation(cfd, final_time): + t = 0.0 + dt_old = cfd.config.dt + dt = dt_old + + domain = cfd.domain + solution = cfd.solution + + while t < final_time: + if t + dt > final_time: + dt = final_time - t + cfd.config.dt = dt # temporary adjustment for last step + runge_kutta(cfd) + t += dt + cfd.config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + domain_eno3 = ComputationalDomain(mesh, config_eno3) + cfd_eno3 = Cfd(config_eno3, domain_eno3) + + + u_list = [] + # ENO + init_field(cfd_eno3) + u_eno = run_simulation(cfd_eno3, config_eno3.final_time) + u_list.append(u_eno) + + # WENO + + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + domain_weno3 = ComputationalDomain(mesh, config_weno3) + cfd_weno3 = Cfd(config_weno3, domain_weno3) + + init_field(cfd_weno3) + u_weno = run_simulation(cfd_weno3, config_weno3.final_time) + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +# Main execution function +def main(): + performEnoWenoAnalysis() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03b/weno3.py b/example/1d-linear-convection/weno3/python/03b/weno3.py new file mode 100644 index 00000000..48d6c3ea --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03b/weno3.py @@ -0,0 +1,630 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# 3rd-order WENO reconstruction for left interface with periodic boundary +def weno3L_periodic(cfd,u,f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + +# 3rd-order WENO reconstruction for right interface with periodic boundary +def weno3R_periodic(cfd,u,f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Initialize flow field with piecewise constant distribution +def init_field(cfd): + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + weno3L_periodic( cfd, q, solution.q_face_left ) + weno3R_periodic( cfd, q, solution.q_face_right ) + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, mesh, config): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.mesh = mesh + self.config = config + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, domain): + self.config = config + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + domain_eno3 = ComputationalDomain(mesh, config_eno3) + cfd_eno3 = Cfd(config_eno3, domain_eno3) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + domain_weno3 = ComputationalDomain(mesh, config_weno3) + cfd_weno3 = Cfd(config_weno3, domain_weno3) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03c/weno3.py b/example/1d-linear-convection/weno3/python/03c/weno3.py new file mode 100644 index 00000000..04db501b --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03c/weno3.py @@ -0,0 +1,616 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +def wc3L(v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +def wc3R(v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L_periodic( cfd, q, solution.q_face_left ) + self.weno3R_periodic( cfd, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L_periodic(self, cfd, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R_periodic(self, cfd, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + domain = cfd.domain + solution = cfd.solution + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = wc3R(v1,v2,v3) + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03d/weno3.py b/example/1d-linear-convection/weno3/python/03d/weno3.py new file mode 100644 index 00000000..3ad59e23 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03d/weno3.py @@ -0,0 +1,613 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3R(v1,v2,v3) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v2-v1)**2 + s1 = (v3-v2)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v1 + 0.5 * v2 + q1 = 1.5 * v2 - 0.5 * v3 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03e/weno3.py b/example/1d-linear-convection/weno3/python/03e/weno3.py new file mode 100644 index 00000000..112924be --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03e/weno3.py @@ -0,0 +1,613 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3R(v1,v2,v3) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03f/weno3.py b/example/1d-linear-convection/weno3/python/03f/weno3.py new file mode 100644 index 00000000..be29ec56 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03f/weno3.py @@ -0,0 +1,614 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + f[j] = self.wc3L(v1,v2,v3) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def run(self): + self.init_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03g/weno3.py b/example/1d-linear-convection/weno3/python/03g/weno3.py new file mode 100644 index 00000000..c8660a84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03g/weno3.py @@ -0,0 +1,645 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +def initial_condition(x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + +def analytical_solution(x, t, a, L): + """Analytical solution with periodic boundary conditions""" + x_shifted = (x - a * t + L) % L + return initial_condition(x_shifted) + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + #print(f"EnoReconstructor:reconstruct") + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def init_field(self): + domain = self.domain + solution = self.solution + for i in range(domain.ist, domain.ied): + j = i - domain.ist + if 0.5 <= domain.mesh.xcc[j] <= 1.0: + solution.u[i] = 2.0 + else: + solution.u[i] = 1.0 + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def initial_condition(self, x): + """Initial condition: step function from 1.0 to 2.0 in [0.5, 1.0]""" + u0 = np.zeros_like(x) + for i in range(len(x)): + if 0.5 <= x[i] <= 1.0: + u0[i] = 2.0 + else: + u0[i] = 1.0 + return u0 + + def init_field_new(self): + domain = self.domain + solution = self.solution + sol = self.initial_condition(domain.mesh.xcc) + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = sol[j] + boundary(solution.u, self) + update_oldfield(solution.un, solution.u) + + def exact_solution(self): + """Analytical solution with periodic boundary conditions""" + x = self.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.mesh.L + x_shifted = (x - c * T + L) % L + return initial_condition(x_shifted) + + def run(self): + #self.init_field() + self.init_field_new() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = analytical_solution(mesh.xcc, config_weno3.final_time, config_weno3.wave_speed, mesh.L) + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03h/weno3.py b/example/1d-linear-convection/weno3/python/03h/weno3.py new file mode 100644 index 00000000..052c2eb2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03h/weno3.py @@ -0,0 +1,695 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Select Runge-Kutta time integration scheme +def runge_kutta(cfd): + rk_order = cfd.config.rk_order + if rk_order == 1: + runge_kutta_1(cfd) + elif rk_order == 2: + runge_kutta_2(cfd) + else: + runge_kutta_3(cfd) + +# 1st-order explicit Euler time integration +def runge_kutta_1(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 2nd-order Runge-Kutta (Heun's method) time integration +def runge_kutta_2(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.5 * solution.un[i] + 0.5 * solution.u[i] + 0.5 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# 3rd-order Runge-Kutta (SSPRK3) time integration +def runge_kutta_3(cfd): + dt = cfd.config.dt + domain = cfd.domain + solution = cfd.solution + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = solution.u[i] + dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = 0.75 * solution.un[i] + 0.25 * solution.u[i] + 0.25 * dt * solution.res[j] + boundary(solution.u, cfd) + + residual(solution.u, cfd) + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(domain.ist,domain.ied): + j = i - domain.ist + solution.u[i] = c1 * solution.un[i] + c2 * solution.u[i] + c3 * dt * solution.res[j] + boundary(solution.u, cfd) + update_oldfield(solution.un, solution.u) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(config, domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + runge_kutta(self) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = cfd_weno3.exact_solution() + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/03i/weno3.py b/example/1d-linear-convection/weno3/python/03i/weno3.py new file mode 100644 index 00000000..433134df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/03i/weno3.py @@ -0,0 +1,738 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Compute residual (flux divergence) for all cells +def residual(q, cfd): + reconstruction(q, cfd) + solution = cfd.solution + mesh = cfd.domain.mesh + inviscid_flux(solution.q_face_left, solution.q_face_right, solution.flux, cfd) + for i in range(mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / mesh.dx + +# Choose reconstruction method based on solver setting +def reconstruction(q, cfd): + cfd.reconstructor.reconstruct(q, cfd) + +# Compute inviscid flux using selected Riemann solver +def inviscid_flux(q_face_left, q_face_right, flux, cfd): + if cfd.config.flux_type == 0: + rusanov_flux(q_face_left, q_face_right, flux, cfd) + else: + engquist_osher_flux(q_face_left, q_face_right, flux, cfd) + +# --------------------------------------------------------------------------- # +# Numerical fluxes +# --------------------------------------------------------------------------- # +def rusanov_flux(q_face_left, q_face_right, flux, cfd): + """Rusanov (local Lax-Friedrichs) flux""" + mesh = cfd.domain.mesh + + for i in range(mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = cfd.config.wave_speed + c_R = cfd.config.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +def engquist_osher_flux(q_face_left, q_face_right, flux, cfd): + """Engquist-Osher flux for linear convection""" + for i in range(cfd.nnodes): + c = cfd.config.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + residual(self.solution.u, self.cfd) + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + boundary(self.solution.u, self.cfd) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + domain = ComputationalDomain(config, mesh) + self.domain = domain + self.solution = Solution(config, domain) # 核心求解数据 + self.reconstructor = ReconstructorFactory.create(config, domain) + + # 初始化时间推进器(工厂创建,一行搞定) + self.integrator = TimeIntegratorFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = cfd_weno3.exact_solution() + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04/weno3.py b/example/1d-linear-convection/weno3/python/04/weno3.py new file mode 100644 index 00000000..2ab18139 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04/weno3.py @@ -0,0 +1,795 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + #residual(self.solution.u, self.cfd) + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + boundary(self.solution.u, self.cfd) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + return solution.u[domain.ist:domain.ied].copy() + + +# Perform ENO-WENO comparative analysis +def performEnoWenoAnalysis(): + mesh = Mesh() + + config_eno3 = (CfdConfig() + .with_reconstruction("eno",3)) + + config_eno3.rk_order = 1 + config_eno3.dt = 0.0025 + + cfd_eno3 = Cfd(config_eno3, mesh) + + + u_list = [] + # ENO + u_eno = cfd_eno3.run() + u_list.append(u_eno) + + # WENO + config_weno3 = (CfdConfig() + .with_reconstruction("weno",3)) + + config_weno3.rk_order = 1 + config_weno3.dt = 0.0025 + + cfd_weno3 = Cfd(config_weno3, mesh) + u_weno = cfd_weno3.run() + u_list.append(u_weno) + + u_analytical = cfd_weno3.exact_solution() + plot_EnoWeno_Analysis(config_weno3, mesh.xcc, u_list, u_analytical) + +# Plot ENO-WENO comparison results +def plot_EnoWeno_Analysis(config, xcc, u_list, u_analytical): + # Define line styles with different colors and markers + styles = [ + {'color': 'black', 'linestyle': '-', 'marker': 'o'}, + {'color': 'blue', 'linestyle': '--', 'marker': 's'}, + {'color': 'black', 'linestyle': '-', 'marker': '^'}, + {'color': 'blue', 'linestyle': '--', 'marker': 'v'}, + {'color': 'black', 'linestyle': '-', 'marker': '<'}, + {'color': 'blue', 'linestyle': '--', 'marker': '>'}, + {'color': 'black', 'linestyle': '-', 'marker': 'D'}, + ] + + n = len(u_list) + num_styles = len(styles) + + p = inflect.engine() + rk_str = p.ordinal(config.rk_order) + + plt.figure("OneFLOW-CFD Solver", figsize=(10, 6)) + plt.title(f'1D Convection Equation at t = {config.final_time:.3f} using 3rd-order ENO&WENO and {rk_str}-order Runge-Kutta methods') + for i in range(0, n): + if i == 0: + lable = 'Numerical (Rusanov)ENO3' + else: + lable = 'Numerical (Rusanov)WENO3' + style = styles[i % num_styles] + plt.plot(xcc, u_list[i], marker=style['marker'], markerfacecolor='none', linestyle=style['linestyle'], color=style['color'], \ + markersize=5, linewidth=0.5, alpha=1.0, label=f'{lable}') + plt.plot(xcc, u_analytical, 'r--', label='Analytical') + plt.xlabel('x') + plt.ylabel('u') + plt.legend() + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + performEnoWenoAnalysis() \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04a/core.py b/example/1d-linear-convection/weno3/python/04a/core.py new file mode 100644 index 00000000..001d5c58 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04a/core.py @@ -0,0 +1,737 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +def periodic_boundary(u, cfd): + """Apply periodic boundary conditions""" + domain = cfd.domain + # Left ghost cells = right interior cells + for ig in range(domain.nghosts): + u[domain.ist - 1 - ig] = u[domain.ied - 1 - ig] + + # Right ghost cells = left interior cells + for ig in range(domain.nghosts): + u[domain.ied + ig] = u[domain.ist + ig] + + +# Apply periodic boundary conditions +def boundary(u, cfd): + periodic_boundary(u, cfd) + +# Copy current solution to old solution array +def update_oldfield(qn, q): + qn[:] = q[:] + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + #residual(self.solution.u, self.cfd) + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + boundary(self.solution.u, self.cfd) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def apply_boundary_conditions(self, cfd_instance): + """应用边界条件""" + boundary(self.u, cfd_instance) + + def update_old_field(self): + """更新旧场""" + update_oldfield(self.un, self.u) + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.solution.apply_boundary_conditions(self) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04a/plotter.py b/example/1d-linear-convection/weno3/python/04a/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04a/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04a/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04a/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04a/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04b/core.py b/example/1d-linear-convection/weno3/python/04b/core.py new file mode 100644 index 00000000..86441655 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04b/core.py @@ -0,0 +1,810 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L),abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5*(c + abs(c)) + cm = 0.5*(c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + #boundary(self.solution.u, self.cfd) + self.cfd.boundary_condition.apply(self.solution.u) + + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04b/plotter.py b/example/1d-linear-convection/weno3/python/04b/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04b/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04b/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04b/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04b/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04c/core.py b/example/1d-linear-convection/weno3/python/04c/core.py new file mode 100644 index 00000000..48c09a02 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/core.py @@ -0,0 +1,752 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# 新增:从 flux.py 导入通量相关类 +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + #boundary(self.solution.u, self.cfd) + self.cfd.boundary_condition.apply(self.solution.u) + + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04c/flux.py b/example/1d-linear-convection/weno3/python/04c/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04c/plotter.py b/example/1d-linear-convection/weno3/python/04c/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04c/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04c/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04c/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/boundary.py b/example/1d-linear-convection/weno3/python/04d/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/core.py b/example/1d-linear-convection/weno3/python/04d/core.py new file mode 100644 index 00000000..0b9cca24 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/core.py @@ -0,0 +1,673 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# 已有 flux 导入 +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# 新增 boundary 导入 +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + #boundary(self.solution.u, self.cfd) + self.cfd.boundary_condition.apply(self.solution.u) + + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + # RK1核心逻辑:u = u + dt * res + self.compute_residual() # 复用公共残差计算 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() # 复用公共边界条件 + self.solution.update_old_field() # 同步old field + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() # 保存预测值 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred # 更新预测值 + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04d/flux.py b/example/1d-linear-convection/weno3/python/04d/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/plotter.py b/example/1d-linear-convection/weno3/python/04d/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04d/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04d/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04d/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/boundary.py b/example/1d-linear-convection/weno3/python/04e/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/core.py b/example/1d-linear-convection/weno3/python/04e/core.py new file mode 100644 index 00000000..bb245bf8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/core.py @@ -0,0 +1,563 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# 已有 flux 导入 +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# 新增 boundary 导入 +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04e/flux.py b/example/1d-linear-convection/weno3/python/04e/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/plotter.py b/example/1d-linear-convection/weno3/python/04e/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04e/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04e/time_integration.py b/example/1d-linear-convection/weno3/python/04e/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04e/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/boundary.py b/example/1d-linear-convection/weno3/python/04f/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/core.py b/example/1d-linear-convection/weno3/python/04f/core.py new file mode 100644 index 00000000..96868195 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/core.py @@ -0,0 +1,543 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +# Initialize reconstruction coefficients for different orders +def init_coef( spatial_order, coef ): + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + # Stencil selection arrays + self.lmc = np.zeros(self.ntcells, dtype=int) + + # Reconstruction coefficients and divided differences + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + + # Choose stencil by ENO method based on smoothest polynomial + self.dd[0, :] = q + + # Compute divided differences + for m in range(1, self.spatial_order): + for j in range(self.ntcells-m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + # Select left-biased stencil for each node + for i in range(domain.ist-1,domain.ied+1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i]-1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + # Reconstruct values at cell interfaces (j+1/2) + for i in range(domain.ist,domain.ied+1): + j = i - domain.ist + k1 = self.lmc[i-1] + k2 = self.lmc[i ] + r1 = i-1 - k1 + r2 = i - k2 + #print(f"i,k1,k2,r1,r2={i,k1,k2,r1,r2}") + solution.q_face_left[j] = 0 + solution.q_face_right[j] = 0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1+1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + # WENO (Weighted Essentially Non-Oscillatory) reconstruction + # Reconstruct values at cell interfaces (j+1/2) + domain = cfd.domain + solution = cfd.solution + self.weno3L( domain, q, solution.q_face_left ) + self.weno3R( domain, q, solution.q_face_right ) + + # 3rd-order WENO reconstruction for left interface with periodic boundary + def weno3L(self, domain, u, f): + # i: ist-1, ist, ..., ied-1 + # j: 0, 1, ..., nx + for i in range(domain.ist - 1, domain.ied): + j = i - ( domain.ist - 1 ) + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3L(v1,v2,v3) + f[j] = self.wc3R(v3,v2,v1) + + # 3rd-order WENO reconstruction for right interface with periodic boundary + def weno3R(self, domain, u, f): + # i: ist, ist+1, ..., ied, ied+1 + # j: 0, 1, ..., nx + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i-1] + v2 = u[i ] + v3 = u[i+1] + #f[j] = self.wc3R(v1,v2,v3) + f[j] = self.wc3L(v3,v2,v1) + + def wc3L(self,v1,v2,v3): + """WENO-3 nonlinear weights for left-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 2.0/3.0 + d1 = 1.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + + def wc3R(self,v1,v2,v3): + """WENO-3 nonlinear weights for right-biased stencil""" + eps = 1.0e-6 + + # Smoothness indicators + s0 = (v3-v2)**2 + s1 = (v2-v1)**2 + + # Compute nonlinear weights w0, w1 + d0 = 1.0/3.0 + d1 = 2.0/3.0 + + c0 = d0 / ( (eps+s0)**2 ) + c1 = d1 / ( (eps+s1)**2 ) + + w0 = c0 / ( c0 + c1 ) + w1 = c1 / ( c0 + c1 ) + + # Candidate stencils + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + + # Reconstructed value at interface + f = ( w0*q0 + w1*q1 ) + + return f + +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + """ + 静态工厂方法(无需实例化工厂类) + :param config: CfdConfig实例(含recon_scheme/spatial_order) + :param domain: ComputationalDomain实例(含ntcells) + :return: Reconstructor子类实例 + """ + scheme = config.recon_scheme.lower() + if scheme == "eno": + # ENO需要空间阶数和总网格数 + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + # WENO无需额外参数(可根据需求扩展,如传入order) + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04f/flux.py b/example/1d-linear-convection/weno3/python/04f/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/mesh.py b/example/1d-linear-convection/weno3/python/04f/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/plotter.py b/example/1d-linear-convection/weno3/python/04f/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04f/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04f/time_integration.py b/example/1d-linear-convection/weno3/python/04f/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04f/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/boundary.py b/example/1d-linear-convection/weno3/python/04g/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/core.py b/example/1d-linear-convection/weno3/python/04g/core.py new file mode 100644 index 00000000..794fe5cf --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/core.py @@ -0,0 +1,333 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class + + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04g/flux.py b/example/1d-linear-convection/weno3/python/04g/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/mesh.py b/example/1d-linear-convection/weno3/python/04g/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/plotter.py b/example/1d-linear-convection/weno3/python/04g/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/reconstructor.py b/example/1d-linear-convection/weno3/python/04g/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04g/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04g/time_integration.py b/example/1d-linear-convection/weno3/python/04g/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04g/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/boundary.py b/example/1d-linear-convection/weno3/python/04h/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/core.py b/example/1d-linear-convection/weno3/python/04h/core.py new file mode 100644 index 00000000..a221aab6 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/core.py @@ -0,0 +1,259 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +class ComputationalDomain: + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + print(f"mesh.ncells={mesh.ncells}") + print(f"self.config.spatial_order={self.config.spatial_order}") + print(f"self.nghosts={self.nghosts}") + print(f"self.ist={self.ist}") + print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # 按格式规则计算nghosts + if scheme == "eno": + nghosts = order # ENO:nghosts = 空间阶数 + elif scheme == "weno": + nghosts = order // 2 + 1 # WENO:nghosts = 阶数//2 +1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + # 校验:避免nghosts为0或负数 + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + # 可选:提供便捷的索引校验/计算方法,增强复用性 + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = ComputationalDomain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04h/flux.py b/example/1d-linear-convection/weno3/python/04h/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/initial_condition.py b/example/1d-linear-convection/weno3/python/04h/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/mesh.py b/example/1d-linear-convection/weno3/python/04h/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/plotter.py b/example/1d-linear-convection/weno3/python/04h/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/reconstructor.py b/example/1d-linear-convection/weno3/python/04h/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04h/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04h/time_integration.py b/example/1d-linear-convection/weno3/python/04h/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04h/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/boundary.py b/example/1d-linear-convection/weno3/python/04i/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/core.py b/example/1d-linear-convection/weno3/python/04i/core.py new file mode 100644 index 00000000..12227f4d --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/core.py @@ -0,0 +1,208 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: ComputationalDomain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + # 可选:添加数据重置/初始化方法,增强复用性 + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + #update_oldfield(self.un, self.u) + self.un[:] = self.u[:] + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04i/domain.py b/example/1d-linear-convection/weno3/python/04i/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/flux.py b/example/1d-linear-convection/weno3/python/04i/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/initial_condition.py b/example/1d-linear-convection/weno3/python/04i/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/mesh.py b/example/1d-linear-convection/weno3/python/04i/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/plotter.py b/example/1d-linear-convection/weno3/python/04i/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/reconstructor.py b/example/1d-linear-convection/weno3/python/04i/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04i/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04i/time_integration.py b/example/1d-linear-convection/weno3/python/04i/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04i/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/boundary.py b/example/1d-linear-convection/weno3/python/04j/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/core.py b/example/1d-linear-convection/weno3/python/04j/core.py new file mode 100644 index 00000000..3df375a6 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/core.py @@ -0,0 +1,169 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +# ---------------------- 4. 残差计算器(封装完整残差计算逻辑) ---------------------- +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + # 步骤1:界面重建(调用外部重建函数,保持兼容) + self._reconstruct() + + # 步骤2:计算无粘通量 + self._compute_inviscid_flux() + + # 步骤3:计算通量散度(残差核心) + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + # 向量化计算:残差[i] = -(flux[i+1] - flux[i])/dx + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx + +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04j/domain.py b/example/1d-linear-convection/weno3/python/04j/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/flux.py b/example/1d-linear-convection/weno3/python/04j/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/initial_condition.py b/example/1d-linear-convection/weno3/python/04j/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/mesh.py b/example/1d-linear-convection/weno3/python/04j/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/plotter.py b/example/1d-linear-convection/weno3/python/04j/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/reconstructor.py b/example/1d-linear-convection/weno3/python/04j/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04j/run_eno_weno.py new file mode 100644 index 00000000..baf1ed6a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/run_eno_weno.py @@ -0,0 +1,50 @@ +from core import CfdConfig, Mesh, Cfd +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/solution.py b/example/1d-linear-convection/weno3/python/04j/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04j/time_integration.py b/example/1d-linear-convection/weno3/python/04j/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04j/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/boundary.py b/example/1d-linear-convection/weno3/python/04k/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/config.py b/example/1d-linear-convection/weno3/python/04k/config.py new file mode 100644 index 00000000..47f8c769 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/config.py @@ -0,0 +1,39 @@ +# config.py +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = 0 # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/domain.py b/example/1d-linear-convection/weno3/python/04k/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/flux.py b/example/1d-linear-convection/weno3/python/04k/flux.py new file mode 100644 index 00000000..265ebdb8 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + 0: RusanovFluxCalculator, + 1: EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/initial_condition.py b/example/1d-linear-convection/weno3/python/04k/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/mesh.py b/example/1d-linear-convection/weno3/python/04k/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/plotter.py b/example/1d-linear-convection/weno3/python/04k/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/reconstructor.py b/example/1d-linear-convection/weno3/python/04k/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/residual.py b/example/1d-linear-convection/weno3/python/04k/residual.py new file mode 100644 index 00000000..b4d4d7dc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/residual.py @@ -0,0 +1,40 @@ +# residual.py + +from flux import FluxCalculatorFactory + +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + self._reconstruct() + self._compute_inviscid_flux() + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04k/run_eno_weno.py new file mode 100644 index 00000000..31b1215b --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/run_eno_weno.py @@ -0,0 +1,52 @@ +from solver import Cfd +from config import CfdConfig +from mesh import Mesh +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/solution.py b/example/1d-linear-convection/weno3/python/04k/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04k/solver.py b/example/1d-linear-convection/weno3/python/04k/solver.py new file mode 100644 index 00000000..a6a4ef49 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/solver.py @@ -0,0 +1,89 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +from config import CfdConfig +from residual import ResidualCalculator + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04k/time_integration.py b/example/1d-linear-convection/weno3/python/04k/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04k/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/boundary.py b/example/1d-linear-convection/weno3/python/04l/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/config.py b/example/1d-linear-convection/weno3/python/04l/config.py new file mode 100644 index 00000000..9ca34391 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/config.py @@ -0,0 +1,39 @@ +# config.py +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = "rusanov" # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme == "weno": + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/domain.py b/example/1d-linear-convection/weno3/python/04l/domain.py new file mode 100644 index 00000000..887191df --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/domain.py @@ -0,0 +1,55 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + if scheme == "eno": + nghosts = order + elif scheme == "weno": + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/flux.py b/example/1d-linear-convection/weno3/python/04l/flux.py new file mode 100644 index 00000000..feaa723a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + "rusanov": RusanovFluxCalculator, + "engquist-osher": EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) diff --git a/example/1d-linear-convection/weno3/python/04l/initial_condition.py b/example/1d-linear-convection/weno3/python/04l/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/mesh.py b/example/1d-linear-convection/weno3/python/04l/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/plotter.py b/example/1d-linear-convection/weno3/python/04l/plotter.py new file mode 100644 index 00000000..dc7e8111 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure(figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/reconstructor.py b/example/1d-linear-convection/weno3/python/04l/reconstructor.py new file mode 100644 index 00000000..1614cc84 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/reconstructor.py @@ -0,0 +1,166 @@ +# reconstructor.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def init_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + + +# ---------------------- 2. 抽象重构器基类 ---------------------- +class Reconstructor(ABC): + def __init__(self): + pass + + @abstractmethod + def reconstruct(self, q, cfd): + pass + + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + init_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] + + +# ---------------------- 4. WENO 重构器(3阶) ---------------------- +class WenoReconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self.weno3L(domain, q, solution.q_face_left) + self.weno3R(domain, q, solution.q_face_right) + + def weno3L(self, domain, u, f): + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3R(v3, v2, v1) + + def weno3R(self, domain, u, f): + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1 = u[i - 1] + v2 = u[i] + v3 = u[i + 1] + f[j] = self.wc3L(v3, v2, v1) + + def wc3L(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 2.0/3.0, 1.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 0.5 * v2 + 0.5 * v3 + q1 = -0.5 * v1 + 1.5 * v2 + return w0 * q0 + w1 * q1 + + def wc3R(self, v1, v2, v3): + eps = 1.0e-6 + s0 = (v3 - v2)**2 + s1 = (v2 - v1)**2 + d0, d1 = 1.0/3.0, 2.0/3.0 + c0 = d0 / ((eps + s0)**2) + c1 = d1 / ((eps + s1)**2) + w0 = c0 / (c0 + c1) + w1 = c1 / (c0 + c1) + q0 = 1.5 * v2 - 0.5 * v3 + q1 = 0.5 * v1 + 0.5 * v2 + return w0 * q0 + w1 * q1 + + +# ---------------------- 5. 重构器工厂 ---------------------- +class ReconstructorFactory: + """重建器工厂:根据配置自动创建对应类型的重建器实例""" + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + if scheme == "eno": + return EnoReconstructor( + spatial_order=config.spatial_order, + ntcells=domain.ntcells + ) + elif scheme == "weno": + return WenoReconstructor() + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/residual.py b/example/1d-linear-convection/weno3/python/04l/residual.py new file mode 100644 index 00000000..b4d4d7dc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/residual.py @@ -0,0 +1,40 @@ +# residual.py + +from flux import FluxCalculatorFactory + +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + self._reconstruct() + self._compute_inviscid_flux() + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04l/run_eno_weno.py new file mode 100644 index 00000000..31b1215b --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/run_eno_weno.py @@ -0,0 +1,52 @@ +from solver import Cfd +from config import CfdConfig +from mesh import Mesh +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 1 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 1 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/solution.py b/example/1d-linear-convection/weno3/python/04l/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04l/solver.py b/example/1d-linear-convection/weno3/python/04l/solver.py new file mode 100644 index 00000000..a6a4ef49 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/solver.py @@ -0,0 +1,89 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory, init_coef + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +from config import CfdConfig +from residual import ResidualCalculator + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04l/time_integration.py b/example/1d-linear-convection/weno3/python/04l/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04l/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/boundary.py b/example/1d-linear-convection/weno3/python/04m/boundary.py new file mode 100644 index 00000000..2d8af5a2 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/boundary.py @@ -0,0 +1,83 @@ +from abc import ABC, abstractmethod + +# ---------------------- 边界条件抽象基类(统一接口) ---------------------- +class BoundaryCondition(ABC): + """边界条件抽象基类:定义所有边界条件必须实现的接口""" + def __init__(self, cfd): + self.cfd = cfd + self.domain = cfd.domain + self.config = cfd.config # 可从配置读取边界参数(如进口速度、固壁温度等) + + @abstractmethod + def apply(self, u): + """ + 应用边界条件到解数组 + :param u: 包含ghost层的解数组(会直接修改该数组) + :return: None + """ + pass + +# ---------------------- 具体边界条件实现(可无限扩展) ---------------------- +class PeriodicBoundary(BoundaryCondition): + """周期边界条件(1D专用)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左ghost层 = 右物理层 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ied - 1 - ig] + + # 右ghost层 = 左物理层 + for ig in range(nghosts): + u[ied + ig] = u[ist + ig] + +class DirichletBoundary(BoundaryCondition): + """Dirichlet(固定值)边界条件(如进口固定速度、固壁零速度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界(进口)固定值(从配置读取) + left_value = self.config.get("left_boundary_value", 1.0) + for ig in range(nghosts): + u[ist - 1 - ig] = left_value + + # 右边界(出口)固定值(从配置读取) + right_value = self.config.get("right_boundary_value", 2.0) + for ig in range(nghosts): + u[ied + ig] = right_value + +class NeumannBoundary(BoundaryCondition): + """Neumann(零梯度)边界条件(如出口无梯度)""" + def apply(self, u): + nghosts = self.domain.nghosts + ist = self.domain.ist + ied = self.domain.ied + + # 左边界零梯度 + for ig in range(nghosts): + u[ist - 1 - ig] = u[ist + ig] + + # 右边界零梯度 + for ig in range(nghosts): + u[ied + ig] = u[ied - 1 - ig] + +# ---------------------- 边界条件工厂(动态创建实例) ---------------------- +class BoundaryConditionFactory: + """边界条件工厂:根据配置创建对应边界条件实例""" + @staticmethod + def create(cfd): + # 从配置读取边界类型(支持多边界组合,1D暂用单一类型) + bc_type = cfd.config.boundary_type.lower() + + if bc_type == "periodic": + return PeriodicBoundary(cfd) + elif bc_type == "dirichlet": + return DirichletBoundary(cfd) + elif bc_type == "neumann": + return NeumannBoundary(cfd) + else: + raise ValueError(f"不支持的边界类型:{bc_type}(可选:periodic/dirichlet/neumann)") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/config.py b/example/1d-linear-convection/weno3/python/04m/config.py new file mode 100644 index 00000000..b3ad4749 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/config.py @@ -0,0 +1,41 @@ +# config.py +class CfdConfig: + def __init__(self): + self.ic_type = "step" + self.recon_scheme = "eno" # 0=ENO, 1=WENO + self.flux_type = "rusanov" # 0=Rusanov, 1=Engquist-Osher + self.rk_order = 1 + self.wave_speed = 1.0 + self.final_time = 0.625 + self.dt = 0.025 + + self.boundary_type = "periodic" + self.left_boundary_value = 1.0 # Dirichlet左边界值 + self.right_boundary_value = 2.0 # Dirichlet右边界值 + + self.spatial_order = 2 + + def with_reconstruction(self, scheme, order=None): + """专用配置:重建方案(链式调用)""" + self.recon_scheme = scheme.lower() # 统一小写,避免大小写问题 + + # 智能默认阶数 + if order is not None: + self.spatial_order = order + else: + if self.recon_scheme.startswith("weno"): + self.spatial_order = 5 + elif self.recon_scheme == "eno": + self.spatial_order = 3 # ENO默认3阶 + else: + raise ValueError(f"不支持的重建格式:{scheme}(仅支持 eno/weno)") + + return self + + def with_boundary(self, bc_type, left_value=None, right_value=None): + self.boundary_type = bc_type + if left_value is not None: + self.left_boundary_value = left_value + if right_value is not None: + self.right_boundary_value = right_value + return self \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/domain.py b/example/1d-linear-convection/weno3/python/04m/domain.py new file mode 100644 index 00000000..ae1cca4e --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/domain.py @@ -0,0 +1,56 @@ +# domain.py +from mesh import Mesh + +class Domain: + """计算域:管理物理区域、ghost层、索引映射等逻辑,依赖 Mesh 提供几何信息""" + def __init__(self, config, mesh): + """ + 初始化计算域 + :param mesh: Mesh实例(静态网格属性) + :param config: CfdConfig实例(包含recon_scheme/spatial_order) + """ + self.config = config + self.mesh = mesh + + # 核心:根据重建格式动态计算nghosts + self.nghosts = self._calc_nghosts() + + # 基于nghosts推导索引 + self.ist = self.nghosts # 物理网格起始索引 + self.ied = self.ist + mesh.ncells # 物理网格结束索引 + self.ntcells = mesh.ncells + 2 * self.nghosts # 总网格数(含ghost) + + # 可选:调试信息(可后续移除) + # print(f"mesh.ncells={mesh.ncells}") + # print(f"self.config.spatial_order={self.config.spatial_order}") + # print(f"self.nghosts={self.nghosts}") + # print(f"self.ist={self.ist}") + # print(f"self.ied={self.ied}") + + def _calc_nghosts(self): + """内部方法:根据重建格式和阶数计算ghost层数量""" + scheme = self.config.recon_scheme.lower() + order = self.config.spatial_order + + if scheme is None: + raise ValueError("必须先通过 with_reconstruction 设置重建格式!") + + # ✅ 统一处理:所有以 "weno" 开头的都按 WENO 规则计算 ghost 层数 + if scheme == "eno": + nghosts = order + elif scheme.startswith("weno"): # ← 关键修改:支持 "weno", "weno3", "weno5", "weno-z" 等 + nghosts = order // 2 + 1 + else: + raise ValueError(f"未知重建格式 {scheme},无法计算ghost层!") + + if nghosts <= 0: + raise ValueError(f"计算得到的ghost层数量无效:{nghosts}(阶数{order},格式{scheme})") + return nghosts + + def is_physical_cell(self, idx): + """判断索引是否在物理网格范围内""" + return self.ist <= idx < self.ied + + def get_physical_indices(self): + """返回物理网格的索引范围(可直接用于循环)""" + return range(self.ist, self.ied) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/flux.py b/example/1d-linear-convection/weno3/python/04m/flux.py new file mode 100644 index 00000000..feaa723a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/flux.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象通量计算基类(统一接口) ---------------------- +class InviscidFluxCalculator(ABC): + """无粘通量计算抽象基类:定义一维CFD通量计算接口""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.mesh = cfd.domain.mesh + self.wave_speed = self.config.wave_speed + + @abstractmethod + def compute(self, q_face_left, q_face_right, flux): + """ + 计算无粘通量(核心接口) + :param q_face_left: 左界面值数组 + :param q_face_right: 右界面值数组 + :param flux: 输出通量数组 + :return: None + """ + pass + +# ---------------------- 2. 具体通量计算子类(隔离不同格式) ---------------------- +class RusanovFluxCalculator(InviscidFluxCalculator): + """Rusanov(Lax-Friedrichs)通量""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + u_L = q_face_left[i] + u_R = q_face_right[i] + c_L = self.wave_speed + c_R = self.wave_speed + F_L = c_L * u_L # Flux from left state + F_R = c_R * u_R # Flux from right state + Smax = max(abs(c_L), abs(c_R)) # Maximum wave speed + flux[i] = 0.5 * (F_L + F_R) - 0.5 * Smax * (u_R - u_L) + +class EngquistOsherFluxCalculator(InviscidFluxCalculator): + """Engquist-Osher通量(线性对流专用)""" + def compute(self, q_face_left, q_face_right, flux): + for i in range(self.mesh.nnodes): + c = self.wave_speed + cp = 0.5 * (c + abs(c)) + cm = 0.5 * (c - abs(c)) + u_L = q_face_left[i] + u_R = q_face_right[i] + flux[i] = cp * u_L + cm * u_R + +# ---------------------- 3. 通量计算器工厂(统一创建逻辑) ---------------------- +class FluxCalculatorFactory: + @staticmethod + def create(cfd): + """根据配置创建通量计算器实例""" + flux_type = cfd.config.flux_type + flux_mapping = { + "rusanov": RusanovFluxCalculator, + "engquist-osher": EngquistOsherFluxCalculator, + # 新增通量格式只需加键值对:2: LaxWendroffFluxCalculator + } + if flux_type not in flux_mapping: + raise ValueError(f"不支持的通量类型:{flux_type}(可选:{list(flux_mapping.keys())})") + return flux_mapping[flux_type](cfd) diff --git a/example/1d-linear-convection/weno3/python/04m/initial_condition.py b/example/1d-linear-convection/weno3/python/04m/initial_condition.py new file mode 100644 index 00000000..166b7dbd --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/initial_condition.py @@ -0,0 +1,81 @@ +# initial_condition.py +import numpy as np +from abc import ABC, abstractmethod + +# ---------------------- 1. 初始条件抽象基类 ---------------------- +class InitialCondition(ABC): + """初始条件基类""" + def __init__(self, config): + self.config = config + + @abstractmethod + def apply(self, solution): + """将初始条件应用到 solution 的内部区域""" + pass + + @abstractmethod + def evaluate_at(self, x): + """纯数学函数:给定 x,返回 u(x),不涉及网格或边界""" + pass + + def _apply_to_interior(self, solution, values): + domain = solution.domain + for i in range(domain.ist, domain.ied): + j = i - domain.ist + solution.u[i] = values[j] + + +# ---------------------- 2. 具体初始条件实现 ---------------------- +class StepFunctionIC(InitialCondition): + def evaluate_at(self, x): + u0 = np.ones_like(x) + mask = (x >= 0.5) & (x <= 1.0) + u0[mask] = 2.0 + return u0 + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class SineWaveIC(InitialCondition): + def evaluate_at(self, x): + L = self.config.get("domain_length", 2.0) + return np.sin(2 * np.pi * x / L) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +class GaussianPulseIC(InitialCondition): + def evaluate_at(self, x): + center = self.config.get("pulse_center", 0.5) + width = self.config.get("pulse_width", 0.1) + return np.exp(-((x - center) / width) ** 2) + + def apply(self, solution): + x = solution.domain.mesh.xcc + u0 = self.evaluate_at(x) + self._apply_to_interior(solution, u0) + + +# ---------------------- 3. 初始条件工厂 ---------------------- +class InitialConditionFactory: + _registry = { + 'step': StepFunctionIC, + 'sin': SineWaveIC, + 'gaussian': GaussianPulseIC, + } + + @classmethod + def create(cls, ic_type, config): + if ic_type not in cls._registry: + raise ValueError(f"未知的初始条件类型: {ic_type}(支持: {list(cls._registry.keys())})") + return cls._registry[ic_type](config) + + @classmethod + def register(cls, name, ic_class): + cls._registry[name] = ic_class \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/mesh.py b/example/1d-linear-convection/weno3/python/04m/mesh.py new file mode 100644 index 00000000..bb855313 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/mesh.py @@ -0,0 +1,26 @@ +# mesh.py +import numpy as np + +# Mesh class: defines computational grid +class Mesh: + def __init__(self): + self.xmin = 0.0 + self.xmax = 2.0 + self.ncells = 40 + self.nnodes = self.ncells + 1 + self.nx = self.ncells + self.x = np.zeros(self.nnodes) + self.xcc = np.zeros(self.ncells) + self.init_mesh() + + def init_mesh(self): + self.L = self.xmax - self.xmin + self.dx = self.L / self.ncells + + # Generate node coordinates + for i in range(self.nnodes): + self.x[i] = self.xmin + i * self.dx + + # Generate cell center coordinates + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/plotter.py b/example/1d-linear-convection/weno3/python/04m/plotter.py new file mode 100644 index 00000000..9f1a414f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/plotter.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import inflect + +class CFDPlotter: + """CFD可视化工具类:解耦绘图逻辑""" + def __init__(self): + # 预设样式(统一管理) + self.default_styles = { + "numerical": {"color": "blue", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + "analytical": {"color": "red", "linestyle": "--", "marker": "", "linewidth": 1.5}, + "comparison": [ + {"color": "black", "linestyle": "-", "marker": "o", "markerfacecolor": "none"}, + {"color": "blue", "linestyle": "--", "marker": "s", "markerfacecolor": "none"}, + {"color": "green", "linestyle": ":", "marker": "^", "markerfacecolor": "none"}, + ] + } + self.p = inflect.engine() + + def plot_quick(self, cfd_result, title=None, show=True, save_path=None): + """轻量即时绘图(快速验证结果)""" + plt.figure("OneFLOW-CFD Solver",figsize=(10, 6)) + + # 自动生成标题 + if title is None: + rk_str = self.p.ordinal(cfd_result["config"]["rk_order"]) + title = (f'1D Convection (t={cfd_result["config"]["final_time"]:.3f})\n' + f'{cfd_result["config"]["order"]}th-order {cfd_result["config"]["scheme"].upper()} + {rk_str}-order RK') + + # 绘制数值解 + plt.plot( + cfd_result["x"], cfd_result["numerical"], + label=f'Numerical ({cfd_result["config"]["scheme"].upper()})', + **self.default_styles["numerical"], + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + cfd_result["x"], cfd_result["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def plot_comparison(self, result_list, title=None, show=True, save_path=None): + """多格式/多精度对比绘图""" + plt.figure("OneFLOW-CFD Solver",figsize=(10, 6)) + + # 自动生成标题 + if title is None: + schemes = [f'{r["config"]["scheme"].upper()}{r["config"]["order"]}' for r in result_list] + rk_str = self.p.ordinal(result_list[0]["config"]["rk_order"]) + title = (f'1D Convection Comparison (t={result_list[0]["config"]["final_time"]:.3f})\n' + f'{", ".join(schemes)} + {rk_str}-order RK') + + # 绘制多个数值解 + for i, res in enumerate(result_list): + style = self.default_styles["comparison"][i % len(self.default_styles["comparison"])] + label = f'Numerical ({res["config"]["scheme"].upper()}{res["config"]["order"]})' + plt.plot( + res["x"], res["numerical"], + label=label, + **style, + markersize=5, linewidth=0.5 + ) + + # 绘制解析解 + plt.plot( + result_list[0]["x"], result_list[0]["analytical"], + label='Analytical', + **self.default_styles["analytical"] + ) + + # 通用样式 + self._set_common_style(title) + + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') + if show: + plt.show() + plt.close() + + def _set_common_style(self, title): + """统一设置图表样式""" + plt.title(title, fontsize=12) + plt.xlabel('x', fontsize=10) + plt.ylabel('u', fontsize=10) + plt.legend(fontsize=9) + plt.grid(True, color='gray', linestyle='--', linewidth=0.5, alpha=0.7) + plt.tight_layout() + +# 快捷函数:ENO/WENO对比绘图 +def plot_eno_weno_comparison(eno_result, weno_result, save_path=None): + plotter = CFDPlotter() + plotter.plot_comparison( + result_list=[eno_result, weno_result], + save_path=save_path + ) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py new file mode 100644 index 00000000..fa17547a --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/__init__.py @@ -0,0 +1,4 @@ +# reconstructor/__init__.py +from .factory import ReconstructorFactory +from .base import Reconstructor +# 可选择性导出具体类(通常不需要) \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/base.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/base.py new file mode 100644 index 00000000..3cb4763d --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/base.py @@ -0,0 +1,7 @@ +# reconstructor/base.py +from abc import ABC, abstractmethod + +class Reconstructor(ABC): + @abstractmethod + def reconstruct(self, q, cfd): + pass \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py new file mode 100644 index 00000000..e087fe57 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/eno.py @@ -0,0 +1,88 @@ +# reconstructor/eno.py +import numpy as np +from .base import Reconstructor # 👈 正确导入基类 + + +# ---------------------- 1. 重构系数初始化函数 ---------------------- +def _init_eno_coef(spatial_order, coef): + """Initialize reconstruction coefficients for different spatial orders.""" + if spatial_order == 1: + coef[0] = [1.0] + coef[1] = [1.0] + elif spatial_order == 2: + coef[0] = [3.0/2.0, -1.0/2.0] + coef[1] = [1.0/2.0, 1.0/2.0] + coef[2] = [-1.0/2.0, 3.0/2.0] + elif spatial_order == 3: + coef[0] = [ 11.0/6.0, -7.0/6.0, 1.0/3.0 ] + coef[1] = [ 1.0/3.0, 5.0/6.0, -1.0/6.0 ] + coef[2] = [ -1.0/6.0, 5.0/6.0, 1.0/3.0 ] + coef[3] = [ 1.0/3.0, -7.0/6.0, 11.0/6.0 ] + elif spatial_order == 4: + coef[0] = [ 25.0/12.0, -23.0/12.0, 13.0/12.0, -1.0/4.0 ] + coef[1] = [ 1.0/4.0, 13.0/12.0, -5.0/12.0, 1.0/12.0 ] + coef[2] = [ -1.0/12.0, 7.0/12.0, 7.0/12.0, -1.0/12.0 ] + coef[3] = [ 1.0/12.0, -5.0/12.0, 13.0/12.0, 1.0/4.0 ] + coef[4] = [ -1.0/4.0, 13.0/12.0, -23.0/12.0, 25.0/12.0 ] + elif spatial_order == 5: + coef[0] = [ 137.0/60.0, -163.0/60.0, 137.0/60.0, -21.0/20.0, 1.0/5.0 ] + coef[1] = [ 1.0/5.0, 77.0/60.0, -43.0/60.0, 17.0/60.0, -1.0/20.0 ] + coef[2] = [ -1.0/20.0, 9.0/20.0, 47.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[3] = [ 1.0/30.0, -13.0/60.0, 47.0/60.0, 9.0/20.0, -1.0/20.0 ] + coef[4] = [ -1.0/20.0, 17.0/60.0, -43.0/60.0, 77.0/60.0, 1.0/5.0 ] + coef[5] = [ 1.0/5.0, -21.0/20.0, 137.0/60.0, -163.0/60.0, 137.0/60.0 ] + elif spatial_order == 6: + coef[0] = [ 49.0/20.0, -71.0/20.0, 79.0/20.0, -163.0/60.0, 31.0/30.0, -1.0/6.0 ] + coef[1] = [ 1.0/6.0, 29.0/20.0, -21.0/20.0, 37.0/60.0, -13.0/60.0, 1.0/30.0 ] + coef[2] = [ -1.0/30.0, 11.0/30.0, 19.0/20.0, -23.0/60.0, 7.0/60.0, -1.0/60.0 ] + coef[3] = [ 1.0/60.0, -2.0/15.0, 37.0/60.0, 37.0/60.0, -2.0/15.0, 1.0/60.0 ] + coef[4] = [ -1.0/60.0, 7.0/60.0, -23.0/60.0, 19.0/20.0, 11.0/30.0, -1.0/30.0 ] + coef[5] = [ 1.0/30.0, -13.0/60.0, 37.0/60.0, -21.0/20.0, 29.0/20.0, 1.0/6.0 ] + coef[6] = [ -1.0/6.0, 31.0/30.0, -163.0/60.0, 79.0/20.0, -71.0/20.0, 49.0/20.0 ] + elif spatial_order == 7: + coef[0] = [ 363.0/140.0, -617.0/140.0, 853.0/140.0, -2341.0/420.0, 667.0/210.0, -43.0/42.0, 1.0/7.0 ] + coef[1] = [ 1.0/7.0, 223.0/140.0, -197.0/140.0, 153.0/140.0, -241.0/420.0, 37.0/210.0, -1.0/42.0 ] + coef[2] = [ -1.0/42.0, 13.0/42.0, 153.0/140.0, -241.0/420.0, 109.0/420.0, -31.0/420.0, 1.0/105.0 ] + coef[3] = [ 1.0/105.0, -19.0/210.0, 107.0/210.0, 319.0/420.0, -101.0/420.0, 5.0/84.0, -1.0/140.0 ] + coef[4] = [ -1.0/140.0, 5.0/84.0, -101.0/420.0, 319.0/420.0, 107.0/210.0, -19.0/210.0, 1.0/105.0 ] + coef[5] = [ 1.0/105.0, -31.0/420.0, 109.0/420.0, -241.0/420.0, 153.0/140.0, 13.0/42.0, -1.0/42.0 ] + coef[6] = [ -1.0/42.0, 37.0/210.0, -241.0/420.0, 153.0/140.0, -197.0/140.0, 223.0/140.0, 1.0/7.0 ] + coef[7] = [ 1.0/7.0, -43.0/42.0, 667.0/210.0, -2341.0/420.0, 853.0/140.0, -617.0/140.0, 363.0/140.0 ] + +# ---------------------- 3. ENO 重构器 ---------------------- +class EnoReconstructor(Reconstructor): + def __init__(self, spatial_order, ntcells): + self.spatial_order = spatial_order + self.ntcells = ntcells + self.lmc = np.zeros(self.ntcells, dtype=int) + self.coef = np.zeros((spatial_order + 1, spatial_order)) + self.dd = np.zeros((spatial_order, self.ntcells)) + _init_eno_coef(self.spatial_order, self.coef) + + def reconstruct(self, q, cfd): + """ENO reconstruction of interface values""" + self.dd[0, :] = q + for m in range(1, self.spatial_order): + for j in range(self.ntcells - m): + self.dd[m, j] = self.dd[m-1, j+1] - self.dd[m-1, j] + + domain = cfd.domain + solution = cfd.solution + + for i in range(domain.ist - 1, domain.ied + 1): + self.lmc[i] = i + for m in range(1, self.spatial_order): + if abs(self.dd[m, self.lmc[i] - 1]) < abs(self.dd[m, self.lmc[i]]): + self.lmc[i] -= 1 + + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + k1 = self.lmc[i - 1] + k2 = self.lmc[i] + r1 = i - 1 - k1 + r2 = i - k2 + solution.q_face_left[j] = 0.0 + solution.q_face_right[j] = 0.0 + for m in range(self.spatial_order): + solution.q_face_left[j] += q[k1 + m] * self.coef[r1 + 1, m] + solution.q_face_right[j] += q[k2 + m] * self.coef[r2, m] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py new file mode 100644 index 00000000..bf5795bc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/factory.py @@ -0,0 +1,35 @@ +# reconstructor/factory.py +from .eno import EnoReconstructor +from .weno3 import Weno3Reconstructor + +class ReconstructorFactory: + _schemes = { + "eno": EnoReconstructor, + "weno3": Weno3Reconstructor, # ← 注意:这里用 "weno3" + # "weno5": Weno5Reconstructor, # 未来扩展 + } + + @staticmethod + def create(config, domain): + scheme = config.recon_scheme.lower() + order = getattr(config, 'spatial_order', None) + + # ✅ 关键:将 "weno" 自动映射为 "weno3", "weno5" 等 + if scheme == "weno": + if order is None: + raise ValueError("使用 'weno' 时必须设置 config.spatial_order") + scheme = f"weno{order}" + + # 检查是否支持 + if scheme not in ReconstructorFactory._schemes: + supported = list(ReconstructorFactory._schemes.keys()) + raise ValueError(f"不支持的重建格式:'{scheme}'(支持:{supported})") + + recon_cls = ReconstructorFactory._schemes[scheme] + + # 根据 scheme 类型创建实例 + if scheme.startswith("eno"): + return recon_cls(order, domain.ntcells) + elif scheme.startswith("weno"): + return recon_cls() # WENO 类通常无参 + # 可扩展 elif... diff --git a/example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py b/example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py new file mode 100644 index 00000000..3d440309 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/reconstructor/weno3.py @@ -0,0 +1,56 @@ +# reconstructor/weno3.py +import numpy as np +from .base import Reconstructor + +class Weno3Reconstructor(Reconstructor): + def reconstruct(self, q, cfd): + domain = cfd.domain + solution = cfd.solution + self._reconstruct_left_interfaces(domain, q, solution.q_face_left) + self._reconstruct_right_interfaces(domain, q, solution.q_face_right) + + def _reconstruct_left_interfaces(self, domain, u, qL): + """在每个 i+1/2 界面,计算左单元贡献的 qL (即 u_{i+1/2}^-)""" + for i in range(domain.ist - 1, domain.ied): + j = i - (domain.ist - 1) + v1, v2, v3 = u[i-1], u[i], u[i+1] + qL[j] = self._reconstruct_from_right_biased_stencil(v3, v2, v1) + + def _reconstruct_right_interfaces(self, domain, u, qR): + """在每个 i+1/2 界面,计算右单元贡献的 qR (即 u_{i+1/2}^+)""" + for i in range(domain.ist, domain.ied + 1): + j = i - domain.ist + v1, v2, v3 = u[i-1], u[i], u[i+1] + qR[j] = self._reconstruct_from_left_biased_stencil(v3, v2, v1) + + def _reconstruct_from_left_biased_stencil(self, v1, v2, v3): + """使用左偏 stencil (v1,v2,v3) 重建界面值(对应 u_{i+1/2}^+)""" + eps = 1e-6 + beta0 = (v3 - v2)**2 # smoothness indicator for stencil [v2, v3] + beta1 = (v2 - v1)**2 # smoothness indicator for stencil [v1, v2] + + d0, d1 = 2/3, 1/3 # optimal linear weights (for right value) + alpha0 = d0 / (eps + beta0)**2 + alpha1 = d1 / (eps + beta1)**2 + w0 = alpha0 / (alpha0 + alpha1) + w1 = alpha1 / (alpha0 + alpha1) + + q0 = 1.5 * v2 - 0.5 * v3 # reconstruction from [v2, v3] + q1 = 0.5 * v1 + 0.5 * v2 # reconstruction from [v1, v2] + return w0 * q0 + w1 * q1 + + def _reconstruct_from_right_biased_stencil(self, v1, v2, v3): + """使用右偏 stencil (v1,v2,v3) 重建界面值(对应 u_{i+1/2}^-)""" + eps = 1e-6 + beta0 = (v3 - v2)**2 + beta1 = (v2 - v1)**2 + + d0, d1 = 1/3, 2/3 # optimal linear weights (for left value) + alpha0 = d0 / (eps + beta0)**2 + alpha1 = d1 / (eps + beta1)**2 + w0 = alpha0 / (alpha0 + alpha1) + w1 = alpha1 / (alpha0 + alpha1) + + q0 = 1.5 * v2 - 0.5 * v3 # from [v2, v3] + q1 = 0.5 * v1 + 0.5 * v2 # from [v1, v2] + return w0 * q0 + w1 * q1 diff --git a/example/1d-linear-convection/weno3/python/04m/residual.py b/example/1d-linear-convection/weno3/python/04m/residual.py new file mode 100644 index 00000000..b4d4d7dc --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/residual.py @@ -0,0 +1,40 @@ +# residual.py + +from flux import FluxCalculatorFactory + +class ResidualCalculator: + """残差计算器:封装「重建→通量→散度」完整流程""" + def __init__(self, cfd): + self.cfd = cfd + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.mesh = self.domain.mesh + self.reconstructor = self.cfd.reconstructor + + # 初始化通量计算器(工厂创建) + self.flux_calculator = FluxCalculatorFactory.create(cfd) + + def compute(self): + """计算完整残差(对外唯一接口)""" + self._reconstruct() + self._compute_inviscid_flux() + self._compute_flux_divergence() + + def _reconstruct(self): + """私有方法:界面值重建""" + self.reconstructor.reconstruct(self.solution.u, self.cfd) + + def _compute_inviscid_flux(self): + """私有方法:计算无粘通量""" + self.flux_calculator.compute( + self.solution.q_face_left, + self.solution.q_face_right, + self.solution.flux + ) + + def _compute_flux_divergence(self): + """私有方法:计算通量散度(残差 = -dF/dx)""" + solution = self.solution + for i in range(self.mesh.ncells): + solution.res[i] = -(solution.flux[i+1] - solution.flux[i]) / self.mesh.dx \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/run_eno_weno.py b/example/1d-linear-convection/weno3/python/04m/run_eno_weno.py new file mode 100644 index 00000000..fd7f3874 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/run_eno_weno.py @@ -0,0 +1,52 @@ +from solver import Cfd +from config import CfdConfig +from mesh import Mesh +from plotter import plot_eno_weno_comparison, CFDPlotter + +def performEnoWenoAnalysis(): + # 1. 初始化网格 + #mesh = Mesh(ncells=100, L=2.0) + mesh = Mesh() + plotter = CFDPlotter() + + # 2. 配置并运行ENO3求解(使用你的链式调用) + print("Running ENO3 solver...") + config_eno3 = CfdConfig() # 初始化默认配置 + config_eno3.with_reconstruction("eno", 3) # 显式指定3阶(也可省略,ENO默认3阶) + # 可选:覆盖默认值(如dt) + config_eno3.dt = 0.0025 + config_eno3.rk_order = 2 + + cfd_eno3 = Cfd(config_eno3, mesh) + cfd_eno3.run() # 求解并生成result字典 + + # 可选:快速验证ENO3结果 + # plotter.plot_quick(cfd_eno3.result, title="ENO3 Quick Check") + + # 3. 配置并运行WENO3求解(注意:WENO默认5阶,这里显式指定3阶) + print("Running WENO3 solver...") + config_weno3 = CfdConfig() + config_weno3.with_reconstruction("weno", 3) # 显式指定3阶(默认是5阶) + # 可选:覆盖默认值 + config_weno3.dt = 0.0025 + config_weno3.rk_order = 2 + + cfd_weno3 = Cfd(config_weno3, mesh) + cfd_weno3.run() + + # 4. 可选:保存结果(供离线绘图) + # cfd_eno3.save_result("eno3_result.npz") + # cfd_weno3.save_result("weno3_result.npz") + + # 5. 绘制ENO/WENO对比图 + print("Plotting comparison results...") + plot_eno_weno_comparison( + eno_result=cfd_eno3.result, + weno_result=cfd_weno3.result, + save_path="eno_weno_comparison.png" # 可选:保存图片 + ) + +if __name__ == "__main__": + # 主程序入口 + performEnoWenoAnalysis() + print("Analysis completed!") \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/solution.py b/example/1d-linear-convection/weno3/python/04m/solution.py new file mode 100644 index 00000000..92a46d3f --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/solution.py @@ -0,0 +1,40 @@ +# solution.py +import numpy as np +from initial_condition import InitialConditionFactory + +class Solution: + def __init__(self, config, domain): + """ + 初始化求解过程中的动态数据 + :param domain: Domain实例(用于确定数组尺寸) + """ + self.domain = domain + mesh = domain.mesh + + # 界面值和通量(维度依赖mesh.nnodes) + self.q_face_left = np.zeros(mesh.nnodes) # 左界面值 + self.q_face_right = np.zeros(mesh.nnodes) # 右界面值 + self.flux = np.zeros(mesh.nnodes) # 通量 + + # 残差(维度依赖mesh.ncells) + self.res = np.zeros(mesh.ncells) # 残差 + + # 解数组(维度依赖ntcells,含ghost层) + self.u = np.zeros(domain.ntcells) # 当前解 + self.un = np.zeros(domain.ntcells) # 上一时间步解 + + self.initialize_from_config(config) + + def reset_solution(self): + """重置解数组为初始状态""" + self.u.fill(0.0) + self.un.fill(0.0) + + def initialize_from_config(self, config): + """根据配置初始化场""" + ic = InitialConditionFactory.create(config.ic_type, config) + ic.apply(self) + + def update_old_field(self): + """更新旧场""" + self.un[:] = self.u[:] \ No newline at end of file diff --git a/example/1d-linear-convection/weno3/python/04m/solver.py b/example/1d-linear-convection/weno3/python/04m/solver.py new file mode 100644 index 00000000..b2024d2c --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/solver.py @@ -0,0 +1,90 @@ +import numpy as np +import matplotlib.pyplot as plt +import inflect +from abc import ABC, abstractmethod + +# Flux +from flux import InviscidFluxCalculator, RusanovFluxCalculator, EngquistOsherFluxCalculator, FluxCalculatorFactory + +# Boundary +from boundary import BoundaryCondition, PeriodicBoundary, DirichletBoundary, NeumannBoundary, BoundaryConditionFactory + +# Time integration +from time_integration import TimeIntegrator, RK1Integrator, RK2Integrator, RK3Integrator, TimeIntegratorFactory + +# Mesh 👈 新增这一行 +from mesh import Mesh + +#from reconstructor import Reconstructor, EnoReconstructor, WenoReconstructor, ReconstructorFactory +from reconstructor import ReconstructorFactory + +from initial_condition import InitialCondition, StepFunctionIC, SineWaveIC, GaussianPulseIC, InitialConditionFactory + +from domain import Domain +from solution import Solution # 👈 新增 + +from config import CfdConfig +from residual import ResidualCalculator + +# Cfd class: main data structure containing all CFD data +class Cfd: + def __init__(self, config, mesh): + self.config = config + self.domain = Domain(config, mesh) + self.solution = Solution(config, self.domain) + self.reconstructor = ReconstructorFactory.create(config, self.domain) + self.residual_calculator = ResidualCalculator(self) + self.integrator = TimeIntegratorFactory.create(self) + self.boundary_condition = BoundaryConditionFactory.create(self) + + def exact_solution(self): + """通用对流问题的解析解:u(x, T) = u0(x - c*T),周期边界""" + x = self.domain.mesh.xcc + T = self.config.final_time + c = self.config.wave_speed + L = self.domain.mesh.L + + # 周期平移:确保在 [x0, x0 + L) 内 + x_shifted = (x - c * T + L) % L + + # 获取 IC 实例并评估 + ic = InitialConditionFactory.create(self.config.ic_type, self.config) + return ic.evaluate_at(x_shifted) + + def run(self): + # 应用初始边界条件并同步 old field + self.boundary_condition.apply(self.solution.u) + self.solution.update_old_field() + + t = 0.0 + dt_old = self.config.dt + dt = dt_old + + domain = self.domain + solution = self.solution + config = self.config + + while t < config.final_time: + if t + dt > config.final_time: + dt = config.final_time - t + config.dt = dt # temporary adjustment for last step + #runge_kutta(self) + self.integrator.step(dt) + t += dt + config.dt = dt_old + + # 整理标准化结果 + u_numerical = self.solution.u[self.domain.ist:self.domain.ied].copy() + self.result = { + "x": domain.mesh.xcc, + "numerical": u_numerical, + "analytical": self.exact_solution(), + "config": { + "scheme": self.config.recon_scheme, + "order": self.config.spatial_order, + "rk_order": self.config.rk_order, + "final_time": self.config.final_time + } + } + + return u_numerical diff --git a/example/1d-linear-convection/weno3/python/04m/time_integration.py b/example/1d-linear-convection/weno3/python/04m/time_integration.py new file mode 100644 index 00000000..54dc4277 --- /dev/null +++ b/example/1d-linear-convection/weno3/python/04m/time_integration.py @@ -0,0 +1,111 @@ +# time_integration.py +from abc import ABC, abstractmethod + +# ---------------------- 1. 抽象时间推进器基类(统一接口) ---------------------- +class TimeIntegrator(ABC): + """时间推进器抽象基类:定义一维CFD时间推进的核心接口""" + def __init__(self, cfd): + self.cfd = cfd # 持有CFD实例,获取配置/域/求解数据 + self.config = cfd.config + self.domain = cfd.domain + self.solution = cfd.solution + self.residual_calculator = cfd.residual_calculator + + @abstractmethod + def step(self, dt): + """ + 单次时间步推进(核心接口) + :param dt: 时间步长 + :return: None + """ + pass + + # 公共逻辑:复用残差计算、边界条件、数组索引映射 + def compute_residual(self): + """计算残差(所有RK方法都需要,封装为公共方法)""" + self.residual_calculator.compute() + + def apply_boundary(self): + """应用边界条件(公共逻辑)""" + self.cfd.boundary_condition.apply(self.solution.u) + + def map_idx(self, i): + """物理网格索引 → 残差数组索引(公共映射逻辑)""" + return i - self.domain.ist + +# ---------------------- 2. 具体RK时间推进器实现(复用公共逻辑) ---------------------- +class RK1Integrator(TimeIntegrator): + """1阶显式欧拉(RK1)""" + def step(self, dt): + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] += dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK2Integrator(TimeIntegrator): + """2阶Heun方法(RK2)""" + def step(self, dt): + # 阶段1:预测步 + self.compute_residual() + u_pred = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u_pred[i] += dt * self.solution.res[j] + self.solution.u[:] = u_pred + self.apply_boundary() + + # 阶段2:校正步 + self.compute_residual() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = 0.5 * self.solution.un[i] + 0.5 * self.solution.u[i] + 0.5 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +class RK3Integrator(TimeIntegrator): + """3阶SSPRK3(强稳定保号RK3)""" + def step(self, dt): + # 阶段1 + self.compute_residual() + u1 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u1[i] += dt * self.solution.res[j] + self.solution.u[:] = u1 + self.apply_boundary() + + # 阶段2 + self.compute_residual() + u2 = self.solution.u.copy() + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + u2[i] = 0.75 * self.solution.un[i] + 0.25 * self.solution.u[i] + 0.25 * dt * self.solution.res[j] + self.solution.u[:] = u2 + self.apply_boundary() + + # 阶段3 + self.compute_residual() + c1, c2, c3 = 1.0/3.0, 2.0/3.0, 2.0/3.0 + for i in range(self.domain.ist, self.domain.ied): + j = self.map_idx(i) + self.solution.u[i] = c1 * self.solution.un[i] + c2 * self.solution.u[i] + c3 * dt * self.solution.res[j] + self.apply_boundary() + self.solution.update_old_field() + +# ---------------------- 3. 时间推进器工厂(统一创建逻辑) ---------------------- +class TimeIntegratorFactory: + """时间推进器工厂:根据配置创建对应RK实例""" + @staticmethod + def create(cfd): + rk_order = cfd.config.rk_order + integrator_mapping = { + 1: RK1Integrator, + 2: RK2Integrator, + 3: RK3Integrator, + # 新增RK4只需:4: RK4Integrator + } + if rk_order not in integrator_mapping: + raise ValueError(f"不支持的RK阶数:{rk_order}(可选:{list(integrator_mapping.keys())})") + return integrator_mapping[rk_order](cfd) \ No newline at end of file diff --git a/example/figure/1d/03u/cfd.png b/example/figure/1d/03u/cfd.png deleted file mode 100644 index c985f2cb..00000000 Binary files a/example/figure/1d/03u/cfd.png and /dev/null differ diff --git a/example/figure/1d/04c/cfd.png b/example/figure/1d/04c/cfd.png deleted file mode 100644 index d464467d..00000000 Binary files a/example/figure/1d/04c/cfd.png and /dev/null differ diff --git a/example/figure/1d/04e1/testprj.py b/example/figure/1d/04e1/testprj.py new file mode 100644 index 00000000..888a4c81 --- /dev/null +++ b/example/figure/1d/04e1/testprj.py @@ -0,0 +1,132 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as patches + +def plot_mixed_line( xst, xed, y0 ): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + x = np.array([xst, x1, x2, xed]) # 按照1/4, 1/2, 1/4的比例分割 + plt.plot([x[0], x[1]], [y0, y0], 'k-', linewidth=1) # 第一段实线 + plt.plot([x[1], x[2]], [y0, y0], 'k--', linewidth=1) # 第二段虚线 + plt.plot([x[2], x[3]], [y0, y0], 'k-', linewidth=1) # 第三段实线 + return + +def plot_cfd_line( x_points, y0, lr ): + # 绘制除中间 5 个点和特定边缘点外的其他点 (内部红色,边缘黑色) + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, facecolor='red', edgecolor='black', linewidth=1) + + # 绘制左侧第三点 (i=-4) 和右侧第三点 (i=4) 为纯黑色点 + special_black_points = np.array([-4, 4],dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + # 绘制中间 6 个点 (i=-2, -1, 0, 1, 2, 3) + iend = 8 + #if lr == 'R': + # iend = 9 + middle_points = x_points[3:iend] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + # 绘制中间 6 个点的黑实线连接 + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + # 添加左起第三点和第四点之间的分段连线(-4到-2) + xl_st = x_points[2] + xl_ed = x_points[3] + plot_mixed_line(xl_st,xl_ed, y0) + + # 添加右起第三点和第四点之间的分段连线(2到4) + #ii = -4 + #if lr == 'L': + #ii = -5 + ii = -5 + xr_st = x_points[ii] + xr_ed = x_points[-3] + plot_mixed_line(xr_st,xr_ed, y0) + + return + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', + facecolor=rectangle_color, + zorder=0) + + # 将矩形添加到坐标轴上 + ax = plt.gca() + ax.add_patch(rect) + return + +def plot_label(y0, xv, lr, txt_name): + # 添加标签和其他设置(保持不变) + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + #if lr == 'L': + # plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + #if lr == 'R': + # plt.text(3, y0-0.5, '$i+3$', fontsize=12, ha='center') + + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + if lr == 'R': + lrname = r'$u_{i-\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(+4, y0+0.3, '$x=L$', fontsize=12, ha='center') + + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + return + +# 设置字体为 Times New Roman +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +# 设置图形大小和样式 +plt.figure(figsize=(12, 5)) + +# 定义新的点坐标 (从 i=-6 到 i=6,基于 x_points) +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6],dtype=np.float64) + +y0 = 0.5 +plot_cfd_line( x_points, y0, 'L' ) + +xv = 0.5*(x_points[5]+x_points[6]) +plt.plot([xv, xv], [y0-0.5, y0+0.5], 'k--') # 绘制垂直线 + +# 添加圆角矩形背景 +rectangle_color = (150/255, 150/255, 200/255) # RGB颜色 + +width = 4.4 +height = 0.2 + +plot_rect(-2.2,y0-0.1,width,height,rectangle_color) +plot_label(y0,xv-0.3,'L','(a) Left-side reconstruction') + +xv = 0.5*(x_points[5]+x_points[4]) + +y1 = -2.0 +plot_cfd_line( x_points, y1, 'R' ) +plt.plot([xv, xv], [y1-0.5, y1+0.5], 'k--') # 绘制垂直线 + +#plot_rect(-1.2,y1-0.1,width,height,rectangle_color) +plot_rect(-2.2,y1-0.1,width,height,rectangle_color) +plot_label(y1,xv+0.3,'R','(b) Right-side reconstruction') + + +plt.axis('equal') +plt.axis('off') + +plt.savefig('cfd.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05/testprj.py b/example/figure/1d/05/testprj.py new file mode 100644 index 00000000..4e98c539 --- /dev/null +++ b/example/figure/1d/05/testprj.py @@ -0,0 +1,114 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches +from matplotlib.path import Path # 新增导入 +from matplotlib.patches import PathPatch # 新增导入 + +def draw_curly_brace(x1, x2, y_attach, text, height=0.6, control_h=0.4, text_offset=0.5): + ax = plt.gca() + mid = (x1 + x2) / 2 + + # 左半边 + verts_left = [(x1, y_attach), + (x1, y_attach - height), + (mid, y_attach - height - control_h), + (mid, y_attach - height)] + codes_left = [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3] + ax.add_patch(PathPatch(Path(verts_left, codes_left), facecolor='none', edgecolor='k', lw=1.5)) + + # 右半边 + verts_right = [(mid, y_attach - height), + (mid, y_attach - height - control_h), + (x2, y_attach - height), + (x2, y_attach)] + codes_right = [Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.LINETO] + ax.add_patch(PathPatch(Path(verts_right, codes_right), facecolor='none', edgecolor='k', lw=1.5)) + + # 文字 + ax.text(mid, y_attach - height - control_h - text_offset, text, ha='center', va='top', fontsize=12) + +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + x = np.array([xst, x1, x2, xed]) + plt.plot([x[0], x[1]], [y0, y0], 'k-', linewidth=1) + plt.plot([x[1], x[2]], [y0, y0], 'k--', linewidth=1) + plt.plot([x[2], x[3]], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + ax = plt.gca() + ax.add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') # 你原代码这里是i=1,可能打错,我保留 + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(+4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 5)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + +y0 = 0.5 +plot_cfd_line(x_points, y0) +xv = 0.5 * (x_points[5] + x_points[6]) # 0.5 +plt.plot([xv, xv], [y0-0.5, y0+0.5], 'k--') +rectangle_color = (150/255, 150/255, 200/255) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 上图添加三层大括号(从外到内) +draw_curly_brace(-2, 0, y0-0.5, '$r=2$', height=0.9, control_h=0.7, text_offset=0.6) +draw_curly_brace(-1, 1, y0-0.5, '$r=1$', height=0.7, control_h=0.5, text_offset=0.5) +draw_curly_brace(0, 2, y0-0.5, '$r=0$', height=0.5, control_h=0.3, text_offset=0.4) + +y1 = -2.0 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-0.5, y1+0.5], 'k--') +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +# 下图添加三层大括号(从外到内) +draw_curly_brace(-1, 1, y1-0.5, '$r=2$', height=0.9, control_h=0.7, text_offset=0.6) +draw_curly_brace(0, 2, y1-0.5, '$r=1$', height=0.7, control_h=0.5, text_offset=0.5) +draw_curly_brace(1, 3, y1-0.5, '$r=0$', height=0.5, control_h=0.3, text_offset=0.4) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_with_braces.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05a/testprj.py b/example/figure/1d/05a/testprj.py new file mode 100644 index 00000000..af8fc4c8 --- /dev/null +++ b/example/figure/1d/05a/testprj.py @@ -0,0 +1,122 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches +from matplotlib.path import Path +from matplotlib.patches import PathPatch + +def draw_curly_brace(x1, x2, y_attach, text, height=0.6, control_h=0.35, upward=True): + ax = plt.gca() + mid = (x1 + x2) / 2 + direction = 1 if upward else -1 + + tip_y = y_attach + direction * height + curve_y = tip_y + direction * control_h + + # 左半部分 + verts_left = [(x1, y_attach), (x1, tip_y), (mid, curve_y), (mid, tip_y)] + codes_left = [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3] + ax.add_patch(PathPatch(Path(verts_left, codes_left), facecolor='none', edgecolor='k', lw=1.8)) + + # 右半部分 + verts_right = [(mid, tip_y), (mid, curve_y), (x2, tip_y), (x2, y_attach)] + codes_right = [Path.MOVETO, Path.CURVE3, Path.CURVE3, Path.LINETO] + ax.add_patch(PathPatch(Path(verts_right, codes_right), facecolor='none', edgecolor='k', lw=1.8)) + + # 文字统一放在同一高度(每个子图内部 r 标签在同一水平线) + text_y_pos = y_attach + 2.0 + va = 'bottom' if upward else 'top' + ax.text(mid, text_y_pos, text, ha='center', va=va, fontsize=12) + +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + # 边缘红点 + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + # 特殊黑点 i=-4 和 i=4 + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + # 中间黑点 i=-2 到 i=3 + middle_points = x_points[3:9] # [-2, -1, 0, 1, 2, 3] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + # 左右混合虚实线 + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') # 你原代码就是这样,保留 + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(+4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 10)) # 高度加大,留出大括号空间 + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# ------------------- 上图 (a) ------------------- +y0 = 3.0 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+2.2], 'k--', linewidth=1.5) # 稍加长 +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 大括号(向上,r 标签高度完全一致) +draw_curly_brace(-2, 0, y0, r'$r=2$', height=1.3, control_h=0.35) +draw_curly_brace(-1, 1, y0, r'$r=1$', height=0.9, control_h=0.35) +draw_curly_brace(0, 2, y0, r'$r=0$', height=0.55, control_h=0.35) + +# ------------------- 下图 (b) ------------------- +y1 = -4.0 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+2.2], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +# 大括号(同样参数) +draw_curly_brace(-1, 1, y1, r'$r=2$', height=1.3, control_h=0.35) +draw_curly_brace(0, 2, y1, r'$r=1$', height=0.9, control_h=0.35) +draw_curly_brace(1, 3, y1, r'$r=0$', height=0.55, control_h=0.35) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_final_with_braces.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05b/testprj.py b/example/figure/1d/05b/testprj.py new file mode 100644 index 00000000..99c113ba --- /dev/null +++ b/example/figure/1d/05b/testprj.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 简单折线括号(∏形) +def draw_simple_bracket(x1, x2, y_base, text, vert_bottom_y, linestyle='-', linewidth=1.5): + top_y = y_base + 0.8 # 所有括号顶横统一高度 + mid = (x1 + x2) / 2 + + # 左竖线 + plt.plot([x1, x1], [vert_bottom_y, top_y], color='k', ls=linestyle, lw=linewidth) + # 顶横线 + plt.plot([x1, x2], [top_y, top_y], color='k', ls=linestyle, lw=linewidth) + # 右竖线 + plt.plot([x2, x2], [top_y, vert_bottom_y], color='k', ls=linestyle, lw=linewidth) + + # 文字统一高度 + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0 - 0.5, '$i+2$', fontsize=12, ha='center') # ← 这里修复了 + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 8)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) +y0 = 2.2 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+2.0], 'k--', linewidth=1.5) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 折线括号 +draw_simple_bracket(-2, 0, y0, r'$r=2$', vert_bottom_y=y0 + 0.10, linestyle=':', linewidth=1.5) +draw_simple_bracket(-1, 1, y0, r'$r=1$', vert_bottom_y=y0 + 0.32, linestyle='--', linewidth=1.5) +draw_simple_bracket( 0, 2, y0, r'$r=0$', vert_bottom_y=y0 + 0.54, linestyle='-', linewidth=2.0) + +# 下图 (b) +y1 = -3.0 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+2.0], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', vert_bottom_y=y1 + 0.10, linestyle=':', linewidth=1.5) +draw_simple_bracket( 0, 2, y1, r'$r=1$', vert_bottom_y=y1 + 0.32, linestyle='--', linewidth=1.5) +draw_simple_bracket( 1, 3, y1, r'$r=0$', vert_bottom_y=y1 + 0.54, linestyle='-', linewidth=2.0) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_simple_brackets_final.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05c/testprj.py b/example/figure/1d/05c/testprj.py new file mode 100644 index 00000000..46dbeab4 --- /dev/null +++ b/example/figure/1d/05c/testprj.py @@ -0,0 +1,108 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 新版简单折线括号:横线高度可自定义,竖线全部到底 +def draw_simple_bracket(x1, x2, y_base, text, top_y, linestyle='-', linewidth=1.5): + mid = (x1 + x2) / 2 + bottom_y = y_base - 0.05 # 竖线略微探出,到底部 + + # 左竖线 + plt.plot([x1, x1], [bottom_y, top_y], color='k', ls=linestyle, lw=linewidth) + # 顶横线(高度由调用者决定) + plt.plot([x1, x2], [top_y, top_y], color='k', ls=linestyle, lw=linewidth) + # 右竖线 + plt.plot([x2, x2], [top_y, bottom_y], color='k', ls=linestyle, lw=linewidth) + + # 文字放在横线上方 + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +# 以下函数完全不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 7)) # 高度再压低一点,完全够用 + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) +y0 = 2.0 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+2.0], 'k--', linewidth=1.5) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 阶梯式括号:内层最高,外层最低,横线完全错开 +draw_simple_bracket(-2, 0, y0, r'$r=2$', top_y=y0 + 0.35, linestyle=':', linewidth=1.5) # 最外·最低 +draw_simple_bracket(-1, 1, y0, r'$r=1$', top_y=y0 + 0.60, linestyle='--', linewidth=1.5) +draw_simple_bracket( 0, 2, y0, r'$r=0$', top_y=y0 + 0.85, linestyle='-', linewidth=2.2) # 最内·最高·最粗 + +# 下图 (b) +y1 = -2.8 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+2.0], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', top_y=y1 + 0.35, linestyle=':', linewidth=1.5) +draw_simple_bracket( 0, 2, y1, r'$r=1$', top_y=y1 + 0.60, linestyle='--', linewidth=1.5) +draw_simple_bracket( 1, 3, y1, r'$r=0$', top_y=y1 + 0.85, linestyle='-', linewidth=2.2) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_staggered_brackets_final.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05d/testprj.py b/example/figure/1d/05d/testprj.py new file mode 100644 index 00000000..08861444 --- /dev/null +++ b/example/figure/1d/05d/testprj.py @@ -0,0 +1,106 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# Final version: horizontal lines almost at same height + vertical lines all the way down + r labels exactly aligned +def draw_simple_bracket(x1, x2, y_base, text, top_y_offset, linestyle='-', linewidth=1.5): + top_y = y_base + top_y_offset # only 0.05 difference between layers + mid = (x1 + x2) / 2 + bottom_y = y_base - 0.05 # vertical lines go all the way to the axis line + + plt.plot([x1, x1], [bottom_y, top_y], 'k', ls=linestyle, lw=linewidth) + plt.plot([x1, x2], [top_y, top_y], 'k', ls=linestyle, lw=linewidth) + plt.plot([x2, x2], [top_y, bottom_y], 'k', ls=linestyle, lw=linewidth) + + # r=0, r=1, r=2 labels exactly on the same horizontal line + fixed_text_y = y_base + 1.05 + plt.text(mid, fixed_text_y, text, ha='center', va='bottom', fontsize=12) + +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== Main program ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 7.5)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# Top subplot (a) +y0 = 2.0 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+1.8], 'k--', linewidth=1.5) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# Brackets: almost same horizontal line height + verticals to bottom + r labels perfectly aligned +draw_simple_bracket(-2, 0, y0, r'$r=2$', top_y_offset=0.80, linestyle=':', linewidth=1.4) +draw_simple_bracket(-1, 1, y0, r'$r=1$', top_y_offset=0.85, linestyle='--', linewidth=1.6) +draw_simple_bracket(0, 2, y0, r'$r=0$', top_y_offset=0.90, linestyle='-', linewidth=2.1) + +# Bottom subplot (b) +y1 = -2.6 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+1.8], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', top_y_offset=0.80, linestyle=':', linewidth=1.4) +draw_simple_bracket(0, 2, y1, r'$r=1$', top_y_offset=0.85, linestyle='--', linewidth=1.6) +draw_simple_bracket(1, 3, y1, r'$r=0$', top_y_offset=0.90, linestyle='-', linewidth=2.1) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_final.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05e/testprj.py b/example/figure/1d/05e/testprj.py new file mode 100644 index 00000000..963b2283 --- /dev/null +++ b/example/figure/1d/05e/testprj.py @@ -0,0 +1,106 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 横线完全同高版本 +def draw_simple_bracket(x1, x2, y_base, text, linestyle='-', linewidth=1.5): + top_y = y_base + 0.85 # 三层完全同一高度 + mid = (x1 + x2) / 2 + bottom_y = y_base - 0.05 # 竖线到底 + + plt.plot([x1, x1], [bottom_y, top_y], 'k', ls=linestyle, lw=linewidth, solid_capstyle='butt') + plt.plot([x1, x2], [top_y, top_y], 'k', ls=linestyle, lw=linewidth) + plt.plot([x2, x2], [top_y, bottom_y], 'k', ls=linestyle, lw=linewidth, solid_capstyle='butt') + + # r标签仍旧完全同一高度 + plt.text(mid, y_base + 1.05, text, ha='center', va='bottom', fontsize=12) + +# 其余函数完全不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 7.5)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) +y0 = 2.0 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+1.8], 'k--', linewidth=1.5) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 三层横线完全同高 +draw_simple_bracket(-2, 0, y0, r'$r=2$', linestyle=':', linewidth=1.4) +draw_simple_bracket(-1, 1, y0, r'$r=1$', linestyle='--', linewidth=1.8) +draw_simple_bracket(0, 2, y0, r'$r=0$', linestyle='-', linewidth=2.4) # 粗实线最明显 + +# 下图 (b) +y1 = -2.6 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+1.8], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', linestyle=':', linewidth=1.4) +draw_simple_bracket(0, 2, y1, r'$r=1$', linestyle='--', linewidth=1.8) +draw_simple_bracket(1, 3, y1, r'$r=0$', linestyle='-', linewidth=2.4) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_same_horizontal_line.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05f/testprj.py b/example/figure/1d/05f/testprj.py new file mode 100644 index 00000000..b49db4ee --- /dev/null +++ b/example/figure/1d/05f/testprj.py @@ -0,0 +1,108 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 简单折线括号(∏形),横线完全统一高度 +def draw_simple_bracket(x1, x2, y_base, text, vert_bottom_y, linestyle='-', linewidth=1.5): + top_y = y_base + 0.8 # 所有括号顶横完全统一高度 + mid = (x1 + x2) / 2 + + # 左竖线 + plt.plot([x1, x1], [vert_bottom_y, top_y], 'k', ls=linestyle, lw=linewidth) + # 顶横线 + plt.plot([x1, x2], [top_y, top_y], 'k', ls=linestyle, lw=linewidth) + # 右竖线 + plt.plot([x2, x2], [top_y, vert_bottom_y], 'k', ls=linestyle, lw=linewidth) + + # 文字统一高度 + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +# 其他函数保持不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 8)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) +y0 = 2.2 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+2.0], 'k--', linewidth=1.5) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 线型顺序改为从左到右:粗实线 → 破折线 → 点线 +draw_simple_bracket(-2, 0, y0, r'$r=2$', vert_bottom_y=y0 + 0.10, linestyle='-', linewidth=2.2) # 最外层:粗实线 +draw_simple_bracket(-1, 1, y0, r'$r=1$', vert_bottom_y=y0 + 0.32, linestyle='--', linewidth=1.6) +draw_simple_bracket( 0, 2, y0, r'$r=0$', vert_bottom_y=y0 + 0.54, linestyle=':', linewidth=1.4) # 最内层:点线 + +# 下图 (b) +y1 = -3.0 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+2.0], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', vert_bottom_y=y1 + 0.10, linestyle='-', linewidth=2.2) # 最外层:粗实线 +draw_simple_bracket( 0, 2, y1, r'$r=1$', vert_bottom_y=y1 + 0.32, linestyle='--', linewidth=1.6) +draw_simple_bracket( 1, 3, y1, r'$r=0$', vert_bottom_y=y1 + 0.54, linestyle=':', linewidth=1.4) # 最内层:点线 + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_brackets_left_to_right_solid.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05g/testprj.py b/example/figure/1d/05g/testprj.py new file mode 100644 index 00000000..745358be --- /dev/null +++ b/example/figure/1d/05g/testprj.py @@ -0,0 +1,105 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 折线括号:横线统一高度 + 用颜色区分 +def draw_simple_bracket(x1, x2, y_base, text, vert_bottom_y, color='k', linestyle='-', linewidth=1.6): + top_y = y_base + 0.8 # 横线完全统一高度 + mid = (x1 + x2) / 2 + + plt.plot([x1, x1], [vert_bottom_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x1, x2], [top_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x2, x2], [top_y, vert_bottom_y], color=color, ls=linestyle, lw=linewidth) + + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +# 其余函数完全不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 8)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) +y0 = 2.2 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0-1.8, y0+2.0], 'k--', linewidth=1.5) +plot_rect(-2.2, y0-0.1, 4.4, 0.2, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# === 缺省配色(推荐)=== +draw_simple_bracket(-2, 0, y0, r'$r=2$', vert_bottom_y=y0 + 0.10, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket(-1, 1, y0, r'$r=1$', vert_bottom_y=y0 + 0.32, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 0, 2, y0, r'$r=0$', vert_bottom_y=y0 + 0.54, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +# 下图 (b) +y1 = -3.0 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1-1.8, y1+2.0], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.1, 4.4, 0.2, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +# 同上配色 +draw_simple_bracket(-1, 1, y1, r'$r=2$', vert_bottom_y=y1 + 0.10, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket( 0, 2, y1, r'$r=1$', vert_bottom_y=y1 + 0.32, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 1, 3, y1, r'$r=0$', vert_bottom_y=y1 + 0.54, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_color_gray_final.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05h/testprj.py b/example/figure/1d/05h/testprj.py new file mode 100644 index 00000000..39971108 --- /dev/null +++ b/example/figure/1d/05h/testprj.py @@ -0,0 +1,105 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 折线括号:横线统一高度 + 颜色区分(修复变量名错误) +def draw_simple_bracket(x1, x2, y_base, text, vert_bottom_y, color='k', linestyle='-', linewidth=1.6): + top_y = y_base + 0.8 # 横线完全统一 + mid = (x1 + x2) / 2 + + # 关键修复:全部使用 vert_bottom_y + plt.plot([x1, x1], [vert_bottom_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x1, x2], [top_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x2, x2], [top_y, vert_bottom_y], color=color, ls=linestyle, lw=linewidth) + + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +# 其余函数完全不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 7)) # 紧凑美观 + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a +y0 = 2.0 +plot_cfd_line(x_points, y0) +plt.plot([xv, xv], [y0 - 0.8, y0 + 0.8], 'k--', linewidth=1.5) # 短虚线,不交叉 +plot_rect(-2.2, y0-0.15, 4.4, 0.3, rectangle_color) # 稍高一点,更协调 +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 灰度配色(最推荐) +draw_simple_bracket(-2, 0, y0, r'$r=2$', vert_bottom_y=y0 + 0.05, color='black', linestyle=':', linewidth=1.6) # 从稍微高一点开始 +draw_simple_bracket(-1, 1, y0, r'$r=1$', vert_bottom_y=y0 + 0.27, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 0, 2, y0, r'$r=0$', vert_bottom_y=y0 + 0.49, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +# 下图 b +y1 = -1.8 # 间距已最小化但不拥挤 +plot_cfd_line(x_points, y1) +plt.plot([xv, xv], [y1 - 0.8, y1 + 0.8], 'k--', linewidth=1.5) +plot_rect(-1.2, y1-0.15, 4.4, 0.3, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', vert_bottom_y=y1 + 0.05, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket( 0, 2, y1, r'$r=1$', vert_bottom_y=y1 + 0.27, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 1, 3, y1, r'$r=0$', vert_bottom_y=y1 + 0.49, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_perfect_final.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05i/testprj.py b/example/figure/1d/05i/testprj.py new file mode 100644 index 00000000..9cd433ef --- /dev/null +++ b/example/figure/1d/05i/testprj.py @@ -0,0 +1,110 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 折线括号:横线统一高度 + 颜色区分 +def draw_simple_bracket(x1, x2, y_base, text, vert_bottom_y, color='k', linestyle='-', linewidth=1.6): + top_y = y_base + 0.8 # 横线完全统一高度 + mid = (x1 + x2) / 2 + + plt.plot([x1, x1], [vert_bottom_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x1, x2], [top_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x2, x2], [top_y, vert_bottom_y], color=color, ls=linestyle, lw=linewidth) + + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +# 其余函数完全不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name): + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +plt.figure(figsize=(12, 7)) # 紧凑布局 + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) +y0 = 2.0 +plot_cfd_line(x_points, y0) + +# 关键修改:垂直虚线向上穿过括号横线 0.25 单位(到 y0+1.05),但不碰到 r 标签 +plt.plot([xv, xv], [y0 - 0.8, y0 + 1.05], 'k--', linewidth=1.5) + +plot_rect(-2.2, y0-0.15, 4.4, 0.3, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction') + +# 灰度配色(最推荐) +draw_simple_bracket(-2, 0, y0, r'$r=2$', vert_bottom_y=y0 + 0.05, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket(-1, 1, y0, r'$r=1$', vert_bottom_y=y0 + 0.27, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 0, 2, y0, r'$r=0$', vert_bottom_y=y0 + 0.49, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +# 下图 (b) +y1 = -1.8 +plot_cfd_line(x_points, y1) + +# 同上处理 +plt.plot([xv, xv], [y1 - 0.8, y1 + 1.05], 'k--', linewidth=1.5) + +plot_rect(-1.2, y1-0.15, 4.4, 0.3, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', vert_bottom_y=y1 + 0.05, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket( 0, 2, y1, r'$r=1$', vert_bottom_y=y1 + 0.27, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 1, 3, y1, r'$r=0$', vert_bottom_y=y1 + 0.49, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_final_with_piercing_line.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/05j/testprj.py b/example/figure/1d/05j/testprj.py new file mode 100644 index 00000000..08eaddb6 --- /dev/null +++ b/example/figure/1d/05j/testprj.py @@ -0,0 +1,131 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.patches as patches + +# 折线括号:横线统一高度 + 颜色区分 +def draw_simple_bracket(x1, x2, y_base, text, vert_bottom_y, color='k', linestyle='-', linewidth=1.6): + top_y = y_base + 0.8 # 横线完全统一高度 + mid = (x1 + x2) / 2 + + plt.plot([x1, x1], [vert_bottom_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x1, x2], [top_y, top_y], color=color, ls=linestyle, lw=linewidth) + plt.plot([x2, x2], [top_y, vert_bottom_y], color=color, ls=linestyle, lw=linewidth) + + plt.text(mid, top_y + 0.15, text, ha='center', va='bottom', fontsize=12) + +# 其余函数完全不变 +def plot_mixed_line(xst, xed, y0): + ds = xed - xst + ds1 = ds / 4 + x1 = xst + ds1 + x2 = xed - ds1 + plt.plot([xst, x1], [y0, y0], 'k-', linewidth=1) + plt.plot([x1, x2], [y0, y0], 'k--', linewidth=1) + plt.plot([x2, xed], [y0, y0], 'k-', linewidth=1) + +def plot_cfd_line(x_points, y0): + edge_points_red = np.concatenate([x_points[:2], x_points[10:]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, + facecolor='red', edgecolor='black', linewidth=1) + + special_black_points = np.array([-4, 4], dtype=np.float64) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, + facecolor='black', edgecolor='black', linewidth=1) + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + plot_mixed_line(-4, -2, y0) + plot_mixed_line(2, 4, y0) + +def plot_rect(x, y, width, height, rectangle_color): + rect = patches.FancyBboxPatch((x, y), width, height, + boxstyle="round,pad=0.1,rounding_size=0.1", + edgecolor='none', facecolor=rectangle_color, zorder=0) + plt.gca().add_patch(rect) + +def plot_label(y0, xv, lr, txt_name, label_type='left'): + """label_type: 'left' 或 'right',决定下图的标签""" + # 通用标签(边界和远场点) + plt.text(-6, y0-0.5, '$-2$', fontsize=12, ha='center') + plt.text(-5, y0-0.5, '$-1$', fontsize=12, ha='center') + plt.text(4, y0-0.5, '$i=N+1$', fontsize=12, ha='center') + plt.text(5, y0-0.5, '$N+2$', fontsize=12, ha='center') + plt.text(6, y0-0.5, '$N+3$', fontsize=12, ha='center') + + # 根据重建方式选择标签 + if label_type == 'left': + # 上图:i 在中心(x=0) + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-2, y0-0.5, '$i-2$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') + elif label_type == 'right': + # 下图:i 在中心(x=0),但括号整体右移 + plt.text(-4, y0-0.5, '$i=1$', fontsize=12, ha='center') + plt.text(-1, y0-0.5, '$i-1$', fontsize=12, ha='center') # 括号起点 + plt.text(0, y0-0.5, '$i$', fontsize=12, ha='center') # i + plt.text(1, y0-0.5, '$i+1$', fontsize=12, ha='center') # 括号中点 + plt.text(2, y0-0.5, '$i+2$', fontsize=12, ha='center') # 括号终点 + plt.text(3, y0-0.5, '$i+3$', fontsize=12, ha='center') # 新增 + + # 边界位置标记 + plt.text(-4, y0+0.3, '$x=0$', fontsize=12, ha='center') + plt.text(4, y0+0.3, '$x=L$', fontsize=12, ha='center') + + # 重建公式标签 + lrname = r'$u_{i+\frac{1}{2}}^' + lr + r'$' + plt.text(xv, y0+0.5, lrname, fontsize=12, ha='center') + plt.text(0, y0-1.5, txt_name, fontsize=12, ha='center') + +# ====================== 主程序 ====================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +# 可调整的间距参数 +spacing = 3.0 # ← 两图之间的垂直间距(默认3.0,原来是3.8) +# 想更小就调成 2.5 或 2.0 + +plt.figure(figsize=(12, 6)) # 高度减小,适应紧凑布局 + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) +xv = 0.5 +rectangle_color = (150/255, 150/255, 200/255) + +# 上图 (a) - 左重建 +y0 = spacing / 2 # 上图在中心上方 +plot_cfd_line(x_points, y0) + +# 关键修改:垂直虚线向上穿过括号横线 0.25 单位(到 y0+1.05),但不碰到 r 标签 +plt.plot([xv, xv], [y0 - 0.8, y0 + 1.05], 'k--', linewidth=1.5) + +plot_rect(-2.2, y0-0.15, 4.4, 0.3, rectangle_color) +plot_label(y0, xv-0.3, 'L', '(a) Left-side reconstruction', label_type='left') + +# 灰度配色(最推荐) +draw_simple_bracket(-2, 0, y0, r'$r=2$', vert_bottom_y=y0 + 0.05, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket(-1, 1, y0, r'$r=1$', vert_bottom_y=y0 + 0.27, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 0, 2, y0, r'$r=0$', vert_bottom_y=y0 + 0.49, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +# 下图 (b) - 右重建 +y1 = -spacing / 2 # 下图在中心下方 +plot_cfd_line(x_points, y1) + +# 同上处理 +plt.plot([xv, xv], [y1 - 0.8, y1 + 1.05], 'k--', linewidth=1.5) + +plot_rect(-1.2, y1-0.15, 4.4, 0.3, rectangle_color) +plot_label(y1, xv+0.3, 'R', '(b) Right-side reconstruction', label_type='right') + +draw_simple_bracket(-1, 1, y1, r'$r=2$', vert_bottom_y=y1 + 0.05, color='black', linestyle=':', linewidth=1.6) +draw_simple_bracket( 0, 2, y1, r'$r=1$', vert_bottom_y=y1 + 0.27, color=(0.3,0.3,0.3), linestyle='--', linewidth=1.6) +draw_simple_bracket( 1, 3, y1, r'$r=0$', vert_bottom_y=y1 + 0.49, color=(0.6,0.6,0.6), linestyle='-', linewidth=1.6) + +plt.axis('equal') +plt.axis('off') +plt.savefig('cfd_final_with_piercing_line.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/Simple1DGrid/01/testprj.py b/example/figure/1d/Simple1DGrid/01/testprj.py new file mode 100644 index 00000000..18a2e6a6 --- /dev/null +++ b/example/figure/1d/Simple1DGrid/01/testprj.py @@ -0,0 +1,88 @@ +import numpy as np +import matplotlib.pyplot as plt # 可选,用于可视化 + +class Simple1DGrid: + def __init__(self, a, b, N_physical, ng=2): + """ + 初始化1D网格。 + - a, b: 物理域边界 [x=a, x=b] + - N_physical: 物理网格点数 (不含ghost) + - ng: ghost层数 (每侧) + """ + self.a = a + self.b = b + self.N = N_physical # 物理点数 + self.ng = ng + self.total_N = N_physical + 2 * ng # 总数组大小 + + # 网格点坐标 x (node-centered, 包括ghost) + dx = (b - a) / N_physical + self.x = np.linspace(a - ng * dx, b + ng * dx, self.total_N) + + # 网格中心坐标 xcc (cell-centered, 用于存储u等) + self.xcc = 0.5 * (self.x[:-1] + self.x[1:]) # 总长 total_N - 1 + + # 物理域切片 (视图,避免拷贝) + self.ist = ng # 逻辑起始 = ng (数组中物理从ng开始) + self.ied = ng + N_physical # 逻辑结束 = ng + N + self.x_physical = self.x[self.ist:self.ied] + self.xcc_physical = self.xcc[self.ist-1:self.ied-1] # xcc物理部分 (N-1? 视方法而定) + + # 示例物理量: u (速度场,初始化为sin波) + self.u = np.sin(2 * np.pi * self.xcc / (b - a)) # 全域初始化 (包括ghost,稍后填充) + + # 只暴露物理视图 + self.u_physical = self.u[self.ist-1:self.ied-1] # 假设cell-centered,调整为self.ist:self.ied if node-centered + + def fill_ghosts_dirichlet(self, u_left=0.0, u_right=0.0): + """ + 填充ghost: Dirichlet边界 (u(a)=u_left, u(b)=u_right) + """ + # 左侧ghost: 镜像或常值 (这里用常值) + self.u[:self.ng] = u_left # 简单常值填充 + # 右侧ghost + self.u[-self.ng:] = u_right + # 更新物理视图 (可选,但视图自动同步) + + def fill_ghosts_neumann(self, du_left=0.0, du_right=0.0): + """ + Neumann边界 (du/dx = const) + """ + dx = self.x[1] - self.x[0] + # 左侧: u[-1] = u[0] - du_left * dx (一阶后向) + self.u[:self.ng] = self.u[self.ng] - du_left * dx * np.arange(1, self.ng+1)[::-1] + # 右侧类似 + self.u[-self.ng:] = self.u[-self.ng-1] + du_right * dx * np.arange(1, self.ng+1) + + def plot_grid(self): + """可视化网格和u""" + fig, ax = plt.subplots() + ax.plot(self.x, np.zeros_like(self.x), 'ko-', label='All points (with ghosts)') + ax.plot(self.x_physical, np.zeros_like(self.x_physical), 'ro-', label='Physical domain') + ax.plot(self.xcc_physical, self.u_physical, 'b-', label='u at centers') + ax.axvline(self.a, color='g', ls='--', label='x=a') + ax.axvline(self.b, color='g', ls='--', label='x=b') + ax.set_xlabel('x') + ax.set_title('1D Grid with Ghosts') + ax.legend() + plt.show() + +# 使用示例 +if __name__ == "__main__": + grid = Simple1DGrid(a=0.0, b=1.0, N_physical=50, ng=2) + print(f"ist (offset): {grid.ist}, ied: {grid.ied}") + print(f"Total array size: {grid.total_N}") + print(f"Physical x range: {grid.x_physical[0]:.3f} to {grid.x_physical[-1]:.3f}") + + # 填充ghost (Dirichlet u=0) + grid.fill_ghosts_dirichlet(u_left=0.0, u_right=0.0) + + # CFD时间步示例: 简单Advection (du/dt + c du/dx =0),用upwind + c = 0.5 # 波速 + dt = 0.01 # 时间步 + u_new = grid.u.copy() + for i in range(grid.ist, grid.ied-1): # 只循环物理域 (注意ied-1 for cell-centered) + u_new[i] = grid.u[i] - c * dt * (grid.u[i] - grid.u[i-1]) / (grid.x[i] - grid.x[i-1]) + grid.u = u_new # 更新 + + grid.plot_grid() # 可视化 \ No newline at end of file diff --git a/example/figure/1d/animation/01/animation.py b/example/figure/1d/animation/01/animation.py new file mode 100644 index 00000000..53ae992e --- /dev/null +++ b/example/figure/1d/animation/01/animation.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) + +# 初始化数据 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') + +# 动画更新函数 +def animate(frame): + # 随着帧数增加,x 数据偏移,实现波形移动 + y = np.sin(x + frame / 10.0) + line.set_ydata(y) + return line, + +# 创建动画:100 帧,每帧间隔 20ms,支持 blit 优化(更快渲染) +ani = animation.FuncAnimation(fig, animate, frames=100, interval=20, blit=True) + +# 显示动画(在 Jupyter 中可用 plt.show(),否则保存为 GIF) +plt.show() + +# 可选:保存为 GIF 文件(需要 pillow 或 imagemagick) +# ani.save('sine_wave.gif', writer='pillow', fps=30) \ No newline at end of file diff --git a/example/figure/1d/animation/01a/animation.py b/example/figure/1d/animation/01a/animation.py new file mode 100644 index 00000000..44c6dc5f --- /dev/null +++ b/example/figure/1d/animation/01a/animation.py @@ -0,0 +1,33 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) + +# 初始化数据和时间标签 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') +time_text = ax.text(0.05, 0.95, '', transform=ax.transAxes, fontsize=12) + +# 动画更新函数:引入时间变化 +def animate(frame): + t = frame * 0.1 # 时间以秒为单位(每帧 0.1 秒) + y = np.sin(x + t) # 波形随时间偏移 + line.set_ydata(y) + time_text.set_text(f'时间: {t:.1f} s') # 显示当前时间 + return line, time_text + +# 创建动画:200 帧,每帧间隔 50ms(总时长约 10 秒) +ani = animation.FuncAnimation(fig, animate, frames=200, interval=50, blit=True) + +# 显示动画 +plt.show() + +# 保存为 GIF(需要 Pillow: pip install pillow) +ani.save('sine_wave.gif', writer='pillow', fps=20) + +# 保存为 MP4(需要 FFmpeg: 安装后配置环境变量) +ani.save('sine_wave.mp4', writer='ffmpeg', fps=20) \ No newline at end of file diff --git a/example/figure/1d/animation/01b/animation.py b/example/figure/1d/animation/01b/animation.py new file mode 100644 index 00000000..017a014d --- /dev/null +++ b/example/figure/1d/animation/01b/animation.py @@ -0,0 +1,36 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 设置中文字体(Windows 推荐) +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 支持中文 +plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) +ax.set_title('正弦波动画') # 测试中文标题 + +# 初始化数据和时间标签 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') +time_text = ax.text(0.05, 0.95, '', transform=ax.transAxes, fontsize=12) + +# 动画更新函数:引入时间变化 +def animate(frame): + t = frame * 0.1 # 时间以秒为单位 + y = np.sin(x + t) + line.set_ydata(y) + time_text.set_text(f'时间: {t:.1f} s') # 中文现在正常 + return line, time_text + +# 创建动画 +ani = animation.FuncAnimation(fig, animate, frames=200, interval=50, blit=True) + +# 先保存文件(取消注释需要的) +ani.save('sine_wave.gif', writer='pillow', fps=20) # 保存 GIF +ani.save('sine_wave.mp4', writer='ffmpeg', fps=20) # 保存 MP4(需 FFmpeg) + +# 再显示动画(窗口播放) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/animation/01c/animation.py b/example/figure/1d/animation/01c/animation.py new file mode 100644 index 00000000..76cf5dc2 --- /dev/null +++ b/example/figure/1d/animation/01c/animation.py @@ -0,0 +1,52 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 设置中文字体(Windows 推荐) +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 支持中文 +plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) +ax.set_title('正弦波动画') # 测试中文标题 + +# 初始化数据和时间标签 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') +time_text = ax.text(0.05, 0.95, '', transform=ax.transAxes, fontsize=12) + +# 动画更新函数:引入时间变化 +def animate(frame): + t = frame * 0.1 # 时间以秒为单位 + y = np.sin(x + t) + line.set_ydata(y) + time_text.set_text(f'时间: {t:.1f} s') # 中文现在正常 + return line, time_text + +# 创建动画 +ani = animation.FuncAnimation(fig, animate, frames=200, interval=50, blit=True) + +plt.show() + +# 进度回调函数:实时显示保存进度 +def progress_callback(i, n): + print(f'保存进度: {i}/{n} 帧 ({i/n*100:.1f}%)') + +# 先显示动画(非阻塞窗口) +plt.show(block=False) # 窗口弹出播放,但脚本继续运行 +print('动画窗口已打开,正在播放...') + +# 再保存文件(带进度和错误处理) +try: + # 选择保存格式:取消注释需要的 + ani.save('sine_wave.gif', writer='pillow', fps=20, progress_callback=progress_callback) # 保存 GIF + # ani.save('sine_wave.mp4', writer='ffmpeg', fps=20, progress_callback=progress_callback) # 保存 MP4(需 FFmpeg) + print('保存完毕!') +except Exception as e: + print(f'保存出错: {e}') + print('请检查 writer(如 Pillow/FFmpeg)是否安装,或帧数是否过大。') + +# 保持窗口打开(可选,防止立即关闭) +plt.pause(0.01) # 短暂暂停,让窗口持续 \ No newline at end of file diff --git a/example/figure/1d/animation/01d/animation.py b/example/figure/1d/animation/01d/animation.py new file mode 100644 index 00000000..570cc109 --- /dev/null +++ b/example/figure/1d/animation/01d/animation.py @@ -0,0 +1,35 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 设置中文字体(Windows 推荐) +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] # 支持中文 +plt.rcParams['axes.unicode_minus'] = False # 正常显示负号 + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) +ax.set_title('正弦波动画') # 测试中文标题 + +# 初始化数据和时间标签 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') +time_text = ax.text(0.05, 0.95, '', transform=ax.transAxes, fontsize=12) + +# 动画更新函数:引入时间变化 +def animate(frame): + t = frame * 0.1 # 时间以秒为单位 + y = np.sin(x + t) + line.set_ydata(y) + time_text.set_text(f'时间: {t:.1f} s') # 中文现在正常 + return line, time_text + +# 创建动画 +ani = animation.FuncAnimation(fig, animate, frames=200, interval=50, blit=True) + +plt.show() + +ani.save('sine_wave.gif', writer='pillow', fps=20) # 保存 GIF +ani.save('sine_wave.mp4', writer='ffmpeg', fps=20) # 保存 MP4(需 FFmpeg) + diff --git a/example/figure/1d/arrow/01/testprj.py b/example/figure/1d/arrow/01/testprj.py new file mode 100644 index 00000000..4fd5eec3 --- /dev/null +++ b/example/figure/1d/arrow/01/testprj.py @@ -0,0 +1,18 @@ +import matplotlib.pyplot as plt +import numpy as np + +# 创建图形 +fig, ax = plt.subplots(figsize=(6, 6)) + +# 绘制向上的箭头 +# plt.arrow(x起点, y起点, dx水平增量, dy垂直增量, ...) +ax.arrow(0.5, 0.2, 0, 0.6, + head_width=0.05, head_length=0.1, + fc='blue', ec='blue', linewidth=2) + +# 设置坐标轴范围 +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +ax.set_aspect('equal') # 等比例显示 +plt.title('向上的箭头 - plt.arrow()') +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01a/testprj.py b/example/figure/1d/arrow/01a/testprj.py new file mode 100644 index 00000000..fd9c7f01 --- /dev/null +++ b/example/figure/1d/arrow/01a/testprj.py @@ -0,0 +1,22 @@ +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(6, 6)) + +# 使用 annotate 绘制箭头 +ax.annotate('', xy=(0.5, 0.8), xytext=(0.5, 0.2), + arrowprops=dict(arrowstyle='->', + color='red', + lw=3, + shrinkA=0, shrinkB=0)) + +# 或者使用 fancy 箭头样式 +ax.annotate('向上箭头', xy=(0.5, 0.8), xytext=(0.5, 0.2), + arrowprops=dict(arrowstyle='fancy', + fc='green', ec='darkgreen', + connectionstyle="arc3,rad=0")) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +plt.title('向上的箭头 - plt.annotate()') +plt.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01b/testprj.py b/example/figure/1d/arrow/01b/testprj.py new file mode 100644 index 00000000..06d09b31 --- /dev/null +++ b/example/figure/1d/arrow/01b/testprj.py @@ -0,0 +1,26 @@ +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(10, 8)) + +# 不同样式的向上箭头示例 +arrow_styles = [ + ('简单箭头', (0.2, 0.2), '->', 'blue'), + ('燕尾箭头', (0.4, 0.2), '<->', 'red'), + ('填充箭头', (0.6, 0.2), '-|>', 'green'), + ('宽头箭头', (0.8, 0.2), 'wedge,tail_width=0.7', 'purple') +] + +for label, start_pos, style, color in arrow_styles: + x, y = start_pos + ax.annotate(label, xy=(x, y+0.6), xytext=(x, y), + arrowprops=dict(arrowstyle=style, + color=color, + lw=2, + shrinkA=5, shrinkB=5)) + ax.text(x-0.08, y-0.05, f'起点: ({x},{y})', fontsize=8) + +ax.set_xlim(0, 1) +ax.set_ylim(0, 1) +plt.title('不同样式的向上箭头') +plt.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01c/testprj.py b/example/figure/1d/arrow/01c/testprj.py new file mode 100644 index 00000000..6ccfff05 --- /dev/null +++ b/example/figure/1d/arrow/01c/testprj.py @@ -0,0 +1,38 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Create a figure +fig, ax = plt.subplots(figsize=(8, 6)) + +# Draw a line segment +x = [0, 3, 5, 2] +y = [0, 2, 1, 3] +ax.plot(x, y, 'gray', alpha=0.5, linewidth=2, label='Original line') + +# Add arrows on each segment of the line +for i in range(len(x)-1): + # Calculate the midpoint of the segment + mid_x = (x[i] + x[i+1]) / 2 + mid_y = (y[i] + y[i+1]) / 2 + + # Calculate the direction vector + dx = x[i+1] - x[i] + dy = y[i+1] - y[i] + + # Add arrow at the midpoint + ax.annotate('', xy=(mid_x + dx/4, mid_y + dy/4), + xytext=(mid_x - dx/4, mid_y - dy/4), + arrowprops=dict(arrowstyle='->', color=f'C{i}', + lw=2, mutation_scale=15)) + +# Add markers at the endpoints of the line segment +ax.scatter(x, y, c='red', s=100, zorder=5, label='Endpoints') + +# Set figure properties +ax.set_xlim(-1, 6) +ax.set_ylim(-1, 4) +ax.set_aspect('equal') +ax.grid(True, alpha=0.3) +ax.legend() +plt.title('Arrow Example on Line Segment') +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01d/testprj.py b/example/figure/1d/arrow/01d/testprj.py new file mode 100644 index 00000000..c5ed5c97 --- /dev/null +++ b/example/figure/1d/arrow/01d/testprj.py @@ -0,0 +1,97 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Create figure and axis +fig, ax = plt.subplots(figsize=(8, 6)) + +# Define line segment coordinates +x_start, y_start = 0, 0 # Starting point +x_end, y_end = 5, 3 # Ending point + +# Draw the original line segment +ax.plot([x_start, x_end], [y_start, y_end], + 'gray', alpha=0.5, linewidth=3, label='Original line segment') + +# Calculate direction vector +dx = x_end - x_start +dy = y_end - y_start + +# Calculate normalized direction vector +length = np.sqrt(dx**2 + dy**2) +dx_norm = dx / length +dy_norm = dy / length + +# Define positions where to place arrows along the line +# Using fractions of the line length +arrow_positions = [0.2, 0.5, 0.8] # At 20%, 50%, and 80% of the line + +# Add arrows at specified positions +for i, fraction in enumerate(arrow_positions): + # Calculate arrow position + arrow_x = x_start + fraction * dx + arrow_y = y_start + fraction * dy + + # Calculate arrow endpoints (start and end of arrow) + arrow_length = 0.8 # Length of arrow relative to line + + # Arrow start point (slightly behind the position) + arrow_start_x = arrow_x - 0.3 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.3 * arrow_length * dy_norm + + # Arrow end point (slightly ahead of the position) + arrow_end_x = arrow_x + 0.5 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.5 * arrow_length * dy_norm + + # Draw arrow using annotate + ax.annotate('', # Empty text (only arrow) + xy=(arrow_end_x, arrow_end_y), # Arrow head position + xytext=(arrow_start_x, arrow_start_y), # Arrow tail position + arrowprops=dict(arrowstyle='->', + color=f'C{i}', # Different color for each arrow + linewidth=2.5, + mutation_scale=20, # Controls arrow head size + shrinkA=0, # No shrink at start + shrinkB=0), # No shrink at end + zorder=5) # Ensure arrows are on top + + # Add a small dot at arrow position for clarity + ax.scatter(arrow_x, arrow_y, color=f'C{i}', s=50, + edgecolor='black', linewidth=1, zorder=6, + label=f'Arrow {i+1} position') + +# Add markers at the endpoints of the line +ax.scatter([x_start, x_end], [y_start, y_end], + c='red', s=100, zorder=5, + edgecolor='black', linewidth=2, + label='Endpoints') + +# Add coordinate labels for endpoints +ax.text(x_start, y_start - 0.3, f'({x_start}, {y_start})', + ha='center', va='top', fontsize=10) +ax.text(x_end, y_end + 0.3, f'({x_end}, {y_end})', + ha='center', va='bottom', fontsize=10) + +# Set axis properties +ax.set_xlim(-1, 6) +ax.set_ylim(-1, 4) +ax.set_aspect('equal') # Keep aspect ratio 1:1 +ax.grid(True, alpha=0.3, linestyle='--') + +# Add title and labels +ax.set_title('Multiple Arrows on a Line Segment', fontsize=14, fontweight='bold') +ax.set_xlabel('X-axis', fontsize=12) +ax.set_ylabel('Y-axis', fontsize=12) + +# Add legend +ax.legend(loc='upper left', fontsize=10) + +# Add text explanation +info_text = f"Line length: {length:.2f} units\n" +info_text += f"Direction vector: ({dx}, {dy})\n" +info_text += f"Normalized direction: ({dx_norm:.2f}, {dy_norm:.2f})" +ax.text(0.02, 0.98, info_text, transform=ax.transAxes, + fontsize=9, verticalalignment='top', + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue", alpha=0.8)) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/arrow/01e/testprj.py b/example/figure/1d/arrow/01e/testprj.py new file mode 100644 index 00000000..9605b520 --- /dev/null +++ b/example/figure/1d/arrow/01e/testprj.py @@ -0,0 +1,266 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, label=None, zorder=2): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + label : str or None (default=None) + Label for the arrow (for legend) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + + Returns: + -------- + arrow_annotation : matplotlib.text.Annotation + The arrow annotation object + """ + # Validate position parameter + if position < 0 or position > 1: + raise ValueError("Position must be between 0 and 1") + + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length (relative to line length) + arrow_length = 0.2 * length # 20% of line length + + # Calculate arrow start and end points + # Arrow points in the direction of the line + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.6 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.6 * arrow_length * dy_norm + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + # Add a small marker at the arrow position + marker = ax.scatter(arrow_x, arrow_y, color=color, s=30, + zorder=zorder+1, alpha=0.6) + + # Add label if provided + if label: + ax.text(arrow_x, arrow_y + 0.05*length, label, + color=color, fontsize=9, ha='center', va='bottom', + bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)) + + return arrow, marker + +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) + + # Example 1: Single line with arrows at different positions + ax1.set_title("Arrows at Different Positions", fontsize=14, fontweight='bold') + + # Draw the line + line_x = [0, 5] + line_y = [0, 3] + ax1.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + # Draw arrows at different positions + positions = [0.0, 0.25, 0.5, 0.75, 1.0] + colors = ['red', 'orange', 'green', 'blue', 'purple'] + labels = ['Start (0.0)', '0.25', 'Middle (0.5)', '0.75', 'End (1.0)'] + + for pos, color, label in zip(positions, colors, labels): + draw_arrow_on_line(ax1, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1], + position=pos, + arrow_style='->', + color=color, + linewidth=2, + head_size=15, + label=label, + zorder=5) + + # Mark endpoints + ax1.scatter(line_x, line_y, c='black', s=100, zorder=10, label='Endpoints') + + # Add coordinate labels + ax1.text(line_x[0], line_y[0]-0.4, f'({line_x[0]}, {line_y[0]})', + ha='center', fontsize=10) + ax1.text(line_x[1], line_y[1]+0.4, f'({line_x[1]}, {line_y[1]})', + ha='center', fontsize=10) + + ax1.set_xlim(-1, 6) + ax1.set_ylim(-1, 5) + ax1.set_aspect('equal') + ax1.grid(True, alpha=0.3) + ax1.legend(loc='upper left') + ax1.set_xlabel('X-axis') + ax1.set_ylabel('Y-axis') + + # Example 2: Multiple lines with arrows + ax2.set_title("Multiple Lines with Arrows", fontsize=14, fontweight='bold') + + # Define multiple lines + lines = [ + {'start': (0, 0), 'end': (4, 1), 'color': 'blue', 'label': 'Line 1'}, + {'start': (1, 0), 'end': (3, 3), 'color': 'green', 'label': 'Line 2'}, + {'start': (0, 2), 'end': (5, 0), 'color': 'red', 'label': 'Line 3'}, + ] + + # Draw each line with arrows + for i, line in enumerate(lines): + x1, y1 = line['start'] + x2, y2 = line['end'] + color = line['color'] + label = line['label'] + + # Draw the line + ax2.plot([x1, x2], [y1, y2], color=color, linewidth=3, + alpha=0.3, label=label) + + # Draw arrows at three positions + for pos in [0.2, 0.5, 0.8]: + draw_arrow_on_line(ax2, + x_start=x1, y_start=y1, + x_end=x2, y_end=y2, + position=pos, + arrow_style='-|>', + color=color, + linewidth=1.5, + head_size=12, + zorder=5) + + # Mark endpoints + ax2.scatter([x1, x2], [y1, y2], c=color, s=80, zorder=10, edgecolors='black') + + ax2.set_xlim(-0.5, 5.5) + ax2.set_ylim(-0.5, 3.5) + ax2.set_aspect('equal') + ax2.grid(True, alpha=0.3) + ax2.legend(loc='upper right') + ax2.set_xlabel('X-axis') + ax2.set_ylabel('Y-axis') + + plt.tight_layout() + plt.show() + +# Example 3: Interactive demonstration +def interactive_demo(): + """Interactive demonstration with user-specified position.""" + + fig, ax = plt.subplots(figsize=(10, 8)) + + # Define a line + x_start, y_start = 1, 1 + x_end, y_end = 8, 6 + + # Draw the base line + ax.plot([x_start, x_end], [y_start, y_end], + 'gray', linewidth=4, alpha=0.2, label='Base line') + + # Test different arrow styles at position 0.5 + arrow_styles = ['->', '-|>', '<-', '<->', 'fancy', 'simple'] + colors = plt.cm.Set2(np.linspace(0, 1, len(arrow_styles))) + + for i, (style, color) in enumerate(zip(arrow_styles, colors)): + # Offset each arrow slightly for visibility + offset = 0.1 * i + position = 0.3 + offset + + if position <= 1.0: # Ensure position is valid + arrow, marker = draw_arrow_on_line(ax, + x_start=x_start, y_start=y_start, + x_end=x_end, y_end=y_end, + position=position, + arrow_style=style, + color=color, + linewidth=2, + head_size=15, + label=f"style='{style}'", + zorder=5) + + # Mark endpoints + ax.scatter([x_start, x_end], [y_start, y_end], + c='black', s=150, zorder=10, label='Endpoints') + + # Add coordinate labels + ax.text(x_start, y_start-0.5, f'Start: ({x_start}, {y_start})', + ha='center', fontsize=11, fontweight='bold') + ax.text(x_end, y_end+0.5, f'End: ({x_end}, {y_end})', + ha='center', fontsize=11, fontweight='bold') + + # Calculate and display line information + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + info_text = f"Line Information:\n" + info_text += f"• Length: {length:.2f} units\n" + info_text += f"• Direction: ({dx:.1f}, {dy:.1f})\n" + info_text += f"• Angle: {np.degrees(np.arctan2(dy, dx)):.1f}°" + + ax.text(0.02, 0.98, info_text, transform=ax.transAxes, + fontsize=11, verticalalignment='top', + bbox=dict(boxstyle="round,pad=0.5", facecolor="lightyellow", alpha=0.9)) + + # Set plot properties + ax.set_xlim(0, 9) + ax.set_ylim(0, 8) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3, linestyle='--') + ax.set_title("Arrow Styles at Different Positions", fontsize=16, fontweight='bold') + ax.set_xlabel('X-axis', fontsize=12) + ax.set_ylabel('Y-axis', fontsize=12) + ax.legend(loc='lower right', fontsize=10) + + plt.tight_layout() + plt.show() + +# Run examples +if __name__ == "__main__": + print("Example 1: Arrows at different positions") + example_usage() + + print("\nExample 2: Different arrow styles") + interactive_demo() \ No newline at end of file diff --git a/example/figure/1d/arrow/01f/testprj.py b/example/figure/1d/arrow/01f/testprj.py new file mode 100644 index 00000000..b03a8a58 --- /dev/null +++ b/example/figure/1d/arrow/01f/testprj.py @@ -0,0 +1,120 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, label=None, zorder=2): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + label : str or None (default=None) + Label for the arrow (for legend) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + + Returns: + -------- + arrow_annotation : matplotlib.text.Annotation + The arrow annotation object + """ + # Validate position parameter + if position < 0 or position > 1: + raise ValueError("Position must be between 0 and 1") + + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length (relative to line length) + arrow_length = 0.2 * length # 20% of line length + + # Calculate arrow start and end points + # Arrow points in the direction of the line + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + # Add a small marker at the arrow position + marker = ax.scatter(arrow_x, arrow_y, color=color, s=30, + zorder=zorder+1, alpha=0.6) + + # Add label if provided + if label: + ax.text(arrow_x, arrow_y + 0.05*length, label, + color=color, fontsize=9, ha='center', va='bottom', + bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)) + + return arrow, marker + +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + plt.figure(figsize=(8, 8)) + + # Draw the line + line_x = [0, 1] + line_y = [0, 1] + plt.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + draw_arrow_on_line(plt, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1] + ) + + + plt.xlim(-0.5, 5.5) + plt.ylim(-0.5, 3.5) + plt.axis('equal') + plt.grid(True, alpha=0.3) + plt.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + example_usage() diff --git a/example/figure/1d/arrow/01f0/testprj.py b/example/figure/1d/arrow/01f0/testprj.py new file mode 100644 index 00000000..58fefef5 --- /dev/null +++ b/example/figure/1d/arrow/01f0/testprj.py @@ -0,0 +1,110 @@ +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2, show_connection=False): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + show_connection : bool (default=False) + Whether to show the connection line (arrow shaft) + """ + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length + arrow_length = 0.2 * length + + # Calculate arrow start and end points + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Create arrow properties + arrow_props = dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0) + + # If we don't want the connection line, set linestyle to 'none' + if not show_connection: + arrow_props['linestyle'] = 'none' + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=arrow_props, + zorder=zorder) + + return arrow + +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + plt.figure(figsize=(8, 8)) + + # Draw the line + line_x = [0, 1] + line_y = [0, 1] + plt.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + draw_arrow_on_line(plt, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1] + ) + + + plt.xlim(-0.5, 5.5) + plt.ylim(-0.5, 3.5) + plt.axis('equal') + plt.grid(True, alpha=0.3) + plt.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + example_usage() diff --git a/example/figure/1d/arrow/01f1/testprj.py b/example/figure/1d/arrow/01f1/testprj.py new file mode 100644 index 00000000..2c066b4a --- /dev/null +++ b/example/figure/1d/arrow/01f1/testprj.py @@ -0,0 +1,73 @@ +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow +# Example usage +def example_usage(): + """Example demonstrating the draw_arrow_on_line function.""" + + # Create figure + plt.figure(figsize=(8, 8)) + + # Draw the line + line_x = [0, 1] + line_y = [0, 1] + plt.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + draw_arrow_only(plt, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1] + ) + + + plt.xlim(-0.5, 5.5) + plt.ylim(-0.5, 3.5) + plt.axis('equal') + plt.grid(True, alpha=0.3) + plt.legend(loc='upper right') + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + example_usage() diff --git a/example/figure/1d/arrow/01g/testprj.py b/example/figure/1d/arrow/01g/testprj.py new file mode 100644 index 00000000..779623f9 --- /dev/null +++ b/example/figure/1d/arrow/01g/testprj.py @@ -0,0 +1,279 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, label=None, zorder=2, show_marker=False): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + label : str or None (default=None) + Label for the arrow (for legend) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + show_marker : bool (default=False) + Whether to show a marker at the arrow position + + Returns: + -------- + arrow_annotation : matplotlib.text.Annotation + The arrow annotation object + """ + # Validate position parameter + if position < 0 or position > 1: + raise ValueError("Position must be between 0 and 1") + + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length (relative to line length) + arrow_length = 0.2 * length # 20% of line length + + # Calculate arrow start and end points + # Arrow points in the direction of the line + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + # Add a small marker at the arrow position only if requested + marker = None + if show_marker: + marker = ax.scatter(arrow_x, arrow_y, color=color, s=30, + zorder=zorder+1, alpha=0.6) + + # Add label if provided + if label: + ax.text(arrow_x, arrow_y + 0.05*length, label, + color=color, fontsize=9, ha='center', va='bottom', + bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)) + + return arrow, marker + +# 简洁版本:完全不绘制标记点 +def draw_arrow_on_line_simple(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2): + """ + Simplified version - draw arrow without marker or label. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Arrow length + arrow_length = 0.2 * length + + # Arrow start and end + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Draw arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +# Example usage +def example_usage(): + """Example demonstrating different options.""" + + fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) + + # 定义线段 + x_start, y_start = 0, 0 + x_end, y_end = 1, 1 + + # 示例1: 不显示标记点(默认) + ax1.set_title("Without marker (default)", fontsize=12) + ax1.plot([x_start, x_end], [y_start, y_end], 'gray', linewidth=2, alpha=0.3) + + # 绘制多个箭头,不显示标记点 + positions = [0.2, 0.5, 0.8] + colors = ['red', 'green', 'blue'] + + for pos, color in zip(positions, colors): + draw_arrow_on_line(ax1, x_start, y_start, x_end, y_end, + position=pos, color=color, show_marker=False) + + ax1.set_xlim(-0.1, 1.1) + ax1.set_ylim(-0.1, 1.1) + ax1.set_aspect('equal') + ax1.grid(True, alpha=0.3) + + # 示例2: 显示标记点 + ax2.set_title("With markers", fontsize=12) + ax2.plot([x_start, x_end], [y_start, y_end], 'gray', linewidth=2, alpha=0.3) + + for pos, color in zip(positions, colors): + draw_arrow_on_line(ax2, x_start, y_start, x_end, y_end, + position=pos, color=color, show_marker=True) + + ax2.set_xlim(-0.1, 1.1) + ax2.set_ylim(-0.1, 1.1) + ax2.set_aspect('equal') + ax2.grid(True, alpha=0.3) + + # 示例3: 使用简洁版本 + ax3.set_title("Using simple version", fontsize=12) + ax3.plot([x_start, x_end], [y_start, y_end], 'gray', linewidth=2, alpha=0.3) + + # 使用简洁版本 + for pos, color in zip(positions, colors): + draw_arrow_on_line_simple(ax3, x_start, y_start, x_end, y_end, + position=pos, color=color) + + ax3.set_xlim(-0.1, 1.1) + ax3.set_ylim(-0.1, 1.1) + ax3.set_aspect('equal') + ax3.grid(True, alpha=0.3) + + plt.tight_layout() + plt.show() + +# 修改你的原始示例 +def your_example(): + """修改后的你的示例""" + fig, ax = plt.subplots(figsize=(8, 8)) + + # 定义线段 + line_x = [0, 1] + line_y = [0, 1] + + # 绘制线段 + ax.plot(line_x, line_y, 'gray', linewidth=3, alpha=0.3, label='Base line') + + # 绘制箭头(不显示标记点) + draw_arrow_on_line(ax, + x_start=line_x[0], y_start=line_y[0], + x_end=line_x[1], y_end=line_y[1], + position=0.5, # 中间位置 + color='blue', + show_marker=False # 不显示标记点 + ) + + # 设置图形属性 + ax.set_xlim(-0.5, 1.5) + ax.set_ylim(-0.5, 1.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + ax.set_title("Arrow without marker point") + + plt.tight_layout() + plt.show() + +# 更多箭头示例 +def multiple_arrows_example(): + """展示多个箭头且不显示标记点的例子""" + fig, ax = plt.subplots(figsize=(10, 8)) + + # 定义多个线段 + lines = [ + ((0, 0), (4, 1), 'Line 1'), + ((1, 0), (3, 3), 'Line 2'), + ((0, 2), (4, 0), 'Line 3'), + ] + + colors = ['blue', 'green', 'red'] + + for i, ((x1, y1), (x2, y2), label) in enumerate(lines): + color = colors[i] + + # 绘制线段 + ax.plot([x1, x2], [y1, y2], color=color, linewidth=2, alpha=0.3, label=label) + + # 在线段上绘制3个箭头(不显示标记点) + for position in [0.25, 0.5, 0.75]: + draw_arrow_on_line(ax, x1, y1, x2, y2, + position=position, + arrow_style='-|>', + color=color, + linewidth=1.5, + head_size=12, + show_marker=False) + + # 设置图形属性 + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.5, 3.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + ax.set_title("Multiple Arrows Without Marker Points") + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + print("示例1: 你的原始示例(修改版)") + your_example() + + print("\n示例2: 不同选项对比") + example_usage() + + print("\n示例3: 多个箭头示例") + multiple_arrows_example() \ No newline at end of file diff --git a/example/figure/1d/arrow/01h/testprj.py b/example/figure/1d/arrow/01h/testprj.py new file mode 100644 index 00000000..bc6feb33 --- /dev/null +++ b/example/figure/1d/arrow/01h/testprj.py @@ -0,0 +1,335 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2, show_connection=False): + """ + Draw an arrow on a line segment at a specified position. + + Parameters: + ----------- + ax : matplotlib.axes.Axes + The axes to draw on + x_start, y_start : float + Starting point coordinates + x_end, y_end : float + Ending point coordinates + position : float (default=0.5) + Position along the line where to place the arrow (0=start, 1=end) + arrow_style : str (default='->') + Arrow style (e.g., '->', '-|>', '<-', '<->', 'fancy') + color : str or tuple (default='blue') + Arrow color + linewidth : float (default=2) + Arrow line width + head_size : float (default=15) + Arrow head size (mutation_scale parameter) + zorder : int (default=2) + Drawing order (higher values are drawn on top) + show_connection : bool (default=False) + Whether to show the connection line (arrow shaft) + """ + # Calculate the coordinates for the arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction vector + dx = x_end - x_start + dy = y_end - y_start + + # Normalize direction vector + length = np.sqrt(dx**2 + dy**2) + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Define arrow length + arrow_length = 0.2 * length + + # Calculate arrow start and end points + arrow_start_x = arrow_x - 0.4 * arrow_length * dx_norm + arrow_start_y = arrow_y - 0.4 * arrow_length * dy_norm + arrow_end_x = arrow_x + 0.4 * arrow_length * dx_norm + arrow_end_y = arrow_y + 0.4 * arrow_length * dy_norm + + # Create arrow properties + arrow_props = dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + shrinkA=0, + shrinkB=0) + + # If we don't want the connection line, set linestyle to 'none' + if not show_connection: + arrow_props['linestyle'] = 'none' + + # Draw the arrow + arrow = ax.annotate('', + xy=(arrow_end_x, arrow_end_y), + xytext=(arrow_start_x, arrow_start_y), + arrowprops=arrow_props, + zorder=zorder) + + return arrow + +# 方法2:使用箭头的偏移参数(更直接的方法) +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, position=0.5, + arrow_style='->', color='blue', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +# 方法3:使用FancyArrowPatch直接控制 +def draw_arrow_head_only(ax, x_start, y_start, x_end, y_end, position=0.5, + color='blue', linewidth=2, head_size=0.3, zorder=2): + """ + Draw only the arrow head using FancyArrowPatch. + """ + from matplotlib.patches import FancyArrowPatch + + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Arrow length + arrow_length = 0.2 * length + + # Create arrow with specified head properties + arrow = FancyArrowPatch((arrow_x, arrow_y), + (arrow_x + arrow_length * dx_norm, + arrow_y + arrow_length * dy_norm), + arrowstyle='->', + color=color, + linewidth=0, # No line + mutation_scale=head_size * 20, # Scale factor + zorder=zorder) + + ax.add_patch(arrow) + return arrow + +# 示例:对比不同方法 +def compare_methods(): + """对比显示连接线和只显示箭头的方法""" + + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + axes = axes.flatten() + + # 定义线段 + x_start, y_start = 0, 0 + x_end, y_end = 1, 1 + + methods = [ + ("With connection line", True, draw_arrow_on_line), + ("Without connection line", False, draw_arrow_on_line), + ("draw_arrow_only method", None, draw_arrow_only), + ("draw_arrow_head_only method", None, draw_arrow_head_only), + ("Different arrow styles", None, None), + ("Multiple arrows", None, None), + ] + + for i, (title, show_conn, method_func) in enumerate(methods): + ax = axes[i] + ax.set_xlim(-0.1, 1.1) + ax.set_ylim(-0.1, 1.1) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.set_title(title, fontsize=12) + + # 绘制基准线段 + ax.plot([x_start, x_end], [y_start, y_end], + 'gray', linewidth=2, alpha=0.2) + + if i == 0 or i == 1: + # 方法1:使用linestyle控制 + draw_arrow_on_line(ax, x_start, y_start, x_end, y_end, + position=0.5, + color='blue', + show_connection=show_conn, + head_size=20) + + elif i == 2: + # 方法2:draw_arrow_only + draw_arrow_only(ax, x_start, y_start, x_end, y_end, + position=0.5, + color='red', + head_size=20) + + elif i == 3: + # 方法3:draw_arrow_head_only + draw_arrow_head_only(ax, x_start, y_start, x_end, y_end, + position=0.5, + color='green', + head_size=0.4) + + elif i == 4: + # 不同箭头样式 + arrow_styles = ['->', '-|>', '<-', '<->', 'fancy'] + colors = ['red', 'blue', 'green', 'purple', 'orange'] + + for j, (style, color) in enumerate(zip(arrow_styles, colors)): + position = 0.2 + j * 0.15 + draw_arrow_only(ax, x_start, y_start, x_end, y_end, + position=position, + arrow_style=style, + color=color, + head_size=15) + ax.text(position, 0.95, style, ha='center', fontsize=8) + + elif i == 5: + # 多个箭头 + positions = [0.2, 0.4, 0.6, 0.8] + colors = plt.cm.viridis(np.linspace(0, 1, len(positions))) + + for pos, color in zip(positions, colors): + draw_arrow_only(ax, x_start, y_start, x_end, y_end, + position=pos, + color=color, + head_size=15) + + plt.tight_layout() + plt.show() + +# 实用示例:在折线上绘制箭头 +def polyline_with_arrows(): + """在折线上绘制只显示箭头的例子""" + + fig, ax = plt.subplots(figsize=(10, 8)) + + # 定义折线点 + points = [(0, 0), (2, 3), (4, 1), (6, 4), (8, 2)] + x_coords = [p[0] for p in points] + y_coords = [p[1] for p in points] + + # 绘制折线 + ax.plot(x_coords, y_coords, 'gray', linewidth=3, alpha=0.3, + marker='o', markersize=8, label='Polyline') + + # 在每个线段上绘制箭头(只显示箭头头) + for i in range(len(points)-1): + x1, y1 = points[i] + x2, y2 = points[i+1] + + # 在线段的不同位置绘制箭头 + for position in [0.25, 0.5, 0.75]: + draw_arrow_only(ax, x1, y1, x2, y2, + position=position, + arrow_style='-|>', + color=f'C{i}', + linewidth=1.5, + head_size=12) + + # 设置图形属性 + ax.set_xlim(-0.5, 8.5) + ax.set_ylim(-0.5, 4.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.set_title("Arrow Heads on Polyline (No Connection Lines)") + ax.legend() + + plt.tight_layout() + plt.show() + +# 向量场示例 +def vector_field_example(): + """向量场风格的箭头示例""" + + fig, ax = plt.subplots(figsize=(10, 8)) + + # 创建网格 + x = np.linspace(0, 4, 5) + y = np.linspace(0, 4, 5) + X, Y = np.meshgrid(x, y) + + # 向量场函数(旋转场) + U = -Y + 2 # X分量 + V = X - 2 # Y分量 + + # 绘制向量场(只显示箭头头) + for i in range(len(x)): + for j in range(len(y)): + # 计算向量长度 + length = np.sqrt(U[j, i]**2 + V[j, i]**2) + + if length > 0: + # 归一化 + u_norm = U[j, i] / length + v_norm = V[j, i] / length + + # 绘制箭头(只显示箭头头) + draw_arrow_only(ax, + x_start=X[j, i] - 0.1 * u_norm, + y_start=Y[j, i] - 0.1 * v_norm, + x_end=X[j, i] + 0.1 * u_norm, + y_end=Y[j, i] + 0.1 * v_norm, + position=0.5, + arrow_style='-|>', + color='blue', + head_size=10 + 5 * length) # 长度越大箭头越大 + + # 设置图形属性 + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.5, 4.5) + ax.set_aspect('equal') + ax.grid(True, alpha=0.3) + ax.set_title("Vector Field with Arrow Heads Only") + + plt.tight_layout() + plt.show() + +if __name__ == "__main__": + print("对比不同方法:") + compare_methods() + + print("\n折线箭头示例:") + polyline_with_arrows() + + print("\n向量场示例:") + vector_field_example() \ No newline at end of file diff --git a/example/figure/1d/eno/01/cfd.png b/example/figure/1d/eno/01/cfd.png deleted file mode 100644 index 049e581c..00000000 Binary files a/example/figure/1d/eno/01/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/01a/cfd.png b/example/figure/1d/eno/01a/cfd.png deleted file mode 100644 index c0bb62c9..00000000 Binary files a/example/figure/1d/eno/01a/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/01b/cfd.png b/example/figure/1d/eno/01b/cfd.png deleted file mode 100644 index 9b8bcc93..00000000 Binary files a/example/figure/1d/eno/01b/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/01c/cfd.png b/example/figure/1d/eno/01c/cfd.png deleted file mode 100644 index 0e2088f7..00000000 Binary files a/example/figure/1d/eno/01c/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/01e/cfd.png b/example/figure/1d/eno/01e/cfd.png deleted file mode 100644 index 486ca803..00000000 Binary files a/example/figure/1d/eno/01e/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/01f/cfd.png b/example/figure/1d/eno/01f/cfd.png deleted file mode 100644 index 11ab81bd..00000000 Binary files a/example/figure/1d/eno/01f/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/02e/cfd.png b/example/figure/1d/eno/02e/cfd.png deleted file mode 100644 index 926d6cd4..00000000 Binary files a/example/figure/1d/eno/02e/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/02g/cfd.png b/example/figure/1d/eno/02g/cfd.png deleted file mode 100644 index 7e9f01ca..00000000 Binary files a/example/figure/1d/eno/02g/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/02h/cfd.png b/example/figure/1d/eno/02h/cfd.png deleted file mode 100644 index aaea00d6..00000000 Binary files a/example/figure/1d/eno/02h/cfd.png and /dev/null differ diff --git a/example/figure/1d/eno/03/testprj.py b/example/figure/1d/eno/03/testprj.py new file mode 100644 index 00000000..a19e1838 --- /dev/null +++ b/example/figure/1d/eno/03/testprj.py @@ -0,0 +1,207 @@ +import numpy as np +import matplotlib.pyplot as plt + +def plot_all_cell_center( xcc, yref ): + plt.scatter(xcc, np.full_like(xcc, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center( xcc, yref ): + nx = xcc.size + ii = nx // 2 + im = ii - 1 + ip = ii + 1 + xcc_new = [] + for i in range(0, nx): + if i > 0 and i < im: + continue + if i > ip and i < nx-1: + continue + xcc_new.append( xcc[i] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center_rs( xcc, yref, r, s ): + nx = xcc.size + ii = nx // 2 + xcc_new = [] + for m in range(-r, s+1): + xcc_new.append( xcc[ii+m] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=100, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_mesh( x, yref ): + dx = x[1] - x[0] + dy = 0.1 * dx + nx = x.size + for i in range(0, nx): + xm = x[i] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + nxc = x.size - 1 + + for i in range(0, nxc): + plt.plot([x[i], x[i+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_mesh_rs( x, yref, r, s): + dx = x[1] - x[0] + dy = 0.1 * dx + + nxc = xcc.size + ii = nxc // 2 + + idc = [] + + for m in range(-r, s+1): + #print(f'm={m}, r={r}, s={s}') + idc.append( ii+m ) + #print(f"idc={idc}") + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + for i in range(0, nvertex): + xm = x[ idv[i] ] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + for i in range(0, ncell): + plt.plot([x[idc[i]], x[idc[i]+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_label(x, xcc, yref): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + yt = yref + dyt + ybc = yref - 0.5* dyb + plt.text(x[0], yb, r'$x_{i-\frac{5}{2}}$', fontsize=12, ha='center') + plt.text(x[1], yb, r'$x_{i-\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[2], yb, r'$x_{i-\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[3], yb, r'$x_{i+\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[4], yb, r'$x_{i+\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[5], yb, r'$x_{i+\frac{5}{2}}$', fontsize=12, ha='center') + + nx = xcc.size + i = nx // 2 + print("i=",i) + im = i - 1 + im1 = i - 2 + ip = i + 1 + ip1 = i + 2 + + plt.text(xcc[im1], ybc, r'$i-2$', fontsize=12, ha='center') + plt.text(xcc[im], ybc, r'$i-1$', fontsize=12, ha='center') + plt.text(xcc[i], ybc, r'$i$', fontsize=12, ha='center') + plt.text(xcc[ip], ybc, r'$i+1$', fontsize=12, ha='center') + plt.text(xcc[ip1], ybc, r'$i+2$', fontsize=12, ha='center') + return + +def plot_label_rs(x, xcc, yref, r, s): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + yt = yref + dyt + ybc = yref - 0.5* dyb + ytt = yref + 0.5* dyt + namelist = [] + namelist.append(r'$x_{i-\frac{5}{2}}$') + namelist.append(r'$x_{i-\frac{3}{2}}$') + namelist.append(r'$x_{i-\frac{1}{2}}$') + namelist.append(r'$x_{i+\frac{1}{2}}$') + namelist.append(r'$x_{i+\frac{3}{2}}$') + namelist.append(r'$x_{i+\frac{5}{2}}$') + + nx = xcc.size + ii = nx // 2 + + idc = [] + for m in range(-r, s+1): + idc.append( ii+m ) + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + + for i in range(0, nvertex): + xm = x[ idv[i] ] + name = namelist[ idv[i] ] + #plt.text(xm, yb, name, fontsize=12, ha='center') + plt.text(xm, ytt, name, fontsize=12, ha='center') + + for m in range(-r, s+1): + ss = '-' + if m > 0 : + ss = '+' + str = r'$i' + ss + f'{abs(m)}' + r'$' + if m == 0 : + plt.text(xcc[ii+m], ybc, r'$i$', fontsize=12, ha='center') + else: + plt.text(xcc[ii+m], ybc, str, fontsize=12, ha='center') + + str = r'$' + f'({r=},{s=})' + r'$' + ishift = (-r+s)//2 + plt.text(xcc[ii+ishift], yb, str, fontsize=12, ha='center') + + return + +def getrs(k,rv,sv): + kk = k-1 + for m in range(0, k): + s = m + r = kk - s + rv.append( r ) + sv.append( s ) + return + +# 设置字体为 Times New Roman +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +# 设置图形大小和样式 +plt.figure(figsize=(12, 6)) + +nx = 5 +L = 1.0 +x_l = 0.0 +dx = L / nx + +x = np.zeros(nx+1, dtype=np.float64) +xcc = np.zeros(nx, dtype=np.float64) + +for i in range(0, nx+1): + x[i] = x_l + dx*(i) + +for i in range(0, nx): + xcc[i] = 0.5*(x[i]+x[i+1]) + +print("x=",x) +print("xcc=",xcc) + +k=3 +rv = [] +sv = [] +getrs(k,rv,sv) +print(f'{rv=},{sv=}') + +dyref = 0.2 + +size = len(rv) +print(f'{size=}') +for i in range(0, size): + yref = 0.0 - i * dyref + r=rv[i] + s=sv[i] + plot_cell_center_rs( xcc, yref, r, s) + plot_mesh_rs( x, yref, r, s) + plot_label_rs(x, xcc, yref, r, s) + + +plt.axis('equal') +plt.axis('off') + +plt.savefig('cfd.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03a/testprj.py b/example/figure/1d/eno/03a/testprj.py new file mode 100644 index 00000000..7d2138c0 --- /dev/null +++ b/example/figure/1d/eno/03a/testprj.py @@ -0,0 +1,218 @@ +import numpy as np +import matplotlib.pyplot as plt + +def plot_all_cell_center( xcc, yref ): + plt.scatter(xcc, np.full_like(xcc, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center( xcc, yref ): + nx = xcc.size + ii = nx // 2 + im = ii - 1 + ip = ii + 1 + xcc_new = [] + for i in range(0, nx): + if i > 0 and i < im: + continue + if i > ip and i < nx-1: + continue + xcc_new.append( xcc[i] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=20, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_cell_center_rs( xcc, yref, r, s ): + nx = xcc.size + ii = 2 + xcc_new = [] + for m in range(-r, s+1): + print(f"m={m},r={r},s={s},ii={ii},ii+m={ii+m}") + xcc_new.append( xcc[ii+m] ) + plt.scatter(xcc_new, np.full_like(xcc_new, yref), s=100, facecolor='black', edgecolor='black', linewidth=1) + return + +def plot_mesh( x, yref ): + dx = x[1] - x[0] + dy = 0.1 * dx + nx = x.size + for i in range(0, nx): + xm = x[i] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + nxc = x.size - 1 + + for i in range(0, nxc): + plt.plot([x[i], x[i+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_mesh_rs( x, yref, r, s): + dx = x[1] - x[0] + dy = 0.1 * dx + + nxc = xcc.size + ii = 2 + + idc = [] + + for m in range(-r, s+1): + #print(f'm={m}, r={r}, s={s}') + print(f"m={m},r={r},s={s},ii={ii},ii+m={ii+m}") + idc.append( ii+m ) + #print(f"idc={idc}") + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + for i in range(0, nvertex): + xm = x[ idv[i] ] + plt.plot([xm, xm], [yref-dy, yref+dy], 'k-') # 绘制垂直线 + + for i in range(0, ncell): + plt.plot([x[idc[i]], x[idc[i]+1]], [yref, yref], 'b-', linewidth=1) + return + +def plot_label(x, xcc, yref): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + 0.1 + yt = yref + dyt + ybc = yref - 0.5* dyb + plt.text(x[0], yb, r'$x_{i-\frac{5}{2}}$', fontsize=12, ha='center') + plt.text(x[1], yb, r'$x_{i-\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[2], yb, r'$x_{i-\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[3], yb, r'$x_{i+\frac{1}{2}}$', fontsize=12, ha='center') + plt.text(x[4], yb, r'$x_{i+\frac{3}{2}}$', fontsize=12, ha='center') + plt.text(x[5], yb, r'$x_{i+\frac{5}{2}}$', fontsize=12, ha='center') + plt.text(x[6], yb, r'$x_{i+\frac{7}{2}}$', fontsize=12, ha='center') + + nx = xcc.size + i = nx // 2 + print("i=",i) + im = i - 1 + im1 = i - 2 + ip = i + 1 + ip1 = i + 2 + ip2 = i + 3 + + plt.text(xcc[im1], ybc, r'$i-2$', fontsize=12, ha='center') + plt.text(xcc[im], ybc, r'$i-1$', fontsize=12, ha='center') + plt.text(xcc[i], ybc, r'$i$', fontsize=12, ha='center') + plt.text(xcc[ip], ybc, r'$i+1$', fontsize=12, ha='center') + plt.text(xcc[ip1], ybc, r'$i+2$', fontsize=12, ha='center') + plt.text(xcc[ip2], ybc, r'$i+3$', fontsize=12, ha='center') + return + +def plot_label_rs(x, xcc, yref, r, s, namelist): + dx = x[1] - x[0] + dyb = 0.5 * dx + dyt = dyb * 0.6 + yb = yref - dyb + yt = yref + dyt + ybc = yref - 0.5* dyb + ytt = yref + 0.5* dyt + + nx = xcc.size + ii = 2 + + idc = [] + for m in range(-r, s+1): + print(f"m={m},r={r},s={s},ii={ii},ii+m={ii+m}") + idc.append( ii+m ) + idv = idc.copy() + idv.append(idc[-1]+1) + + ncell = len( idc ) + nvertex = len( idv ) + + for i in range(0, nvertex): + xm = x[ idv[i] ] + name = namelist[ idv[i] ] + #plt.text(xm, yb, name, fontsize=12, ha='center') + plt.text(xm, ytt, name, fontsize=12, ha='center') + + for m in range(-r, s+1): + ss = '-' + if m > 0 : + ss = '+' + str = r'$i' + ss + f'{abs(m)}' + r'$' + if m == 0 : + plt.text(xcc[ii+m], ybc, r'$i$', fontsize=12, ha='center') + else: + plt.text(xcc[ii+m], ybc, str, fontsize=12, ha='center') + + str = r'$' + f'({r=},{s=})' + r'$' + ishift = (-r+s)//2 + plt.text(xcc[ii+ishift], yb, str, fontsize=12, ha='center') + + return + +def getrs(k,rv,sv): + kk = k-1 + for m in range(0, k+1): + s = m + r = kk - s + rv.append( r ) + sv.append( s ) + return + +# 设置字体为 Times New Roman +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) + +# 设置图形大小和样式 +plt.figure(figsize=(12, 6)) + +nx = 5 + 1 +L = 1.0 +x_l = 0.0 +dx = L / nx + +x = np.zeros(nx+1, dtype=np.float64) +xcc = np.zeros(nx, dtype=np.float64) + +for i in range(0, nx+1): + x[i] = x_l + dx*(i) + +for i in range(0, nx): + xcc[i] = 0.5*(x[i]+x[i+1]) + +print("x=",x) +print("xcc=",xcc) + +k=3 +rv = [] +sv = [] +getrs(k,rv,sv) +print(f'{rv=},{sv=}') + +namelist = [] +namelist.append(r'$x_{i-\frac{5}{2}}$') +namelist.append(r'$x_{i-\frac{3}{2}}$') +namelist.append(r'$x_{i-\frac{1}{2}}$') +namelist.append(r'$x_{i+\frac{1}{2}}$') +namelist.append(r'$x_{i+\frac{3}{2}}$') +namelist.append(r'$x_{i+\frac{5}{2}}$') +namelist.append(r'$x_{i+\frac{7}{2}}$') + +dyref = 0.2 +size = len(rv) +print(f'{size=}') + + +for i in range(0, size): + yref = 0.0 - i * dyref + r=rv[i] + s=sv[i] + plot_cell_center_rs( xcc, yref, r, s) + print(f"plot_mesh_rs ,r={r},s={s}") + plot_mesh_rs( x, yref, r, s) + print(f"plot_label_rs ,r={r},s={s}") + plot_label_rs(x, xcc, yref, r, s, namelist) + + +plt.axis('equal') +plt.axis('off') + +plt.savefig('cfd.png', bbox_inches='tight', dpi=300) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03b/testprj.py b/example/figure/1d/eno/03b/testprj.py new file mode 100644 index 00000000..5dd2fa38 --- /dev/null +++ b/example/figure/1d/eno/03b/testprj.py @@ -0,0 +1,100 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只需调这几个) ========================== +visual_cell_width = 2.0 # 调大一点让整体占满(2.0 在 12 英寸宽下几乎完美左右贴边) +center_x = 6.0 # 6.0 基本居中(因为左右延伸不对称 2.5 vs 3.5) +dyref = 1.5 # 行间垂直间距 +k = 3 # 保持 3 → 产生 r = 2,1,0,-1 四行 +# ========================================================================= + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) # 自动支持 r 负(Python range 能处理) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=120, facecolor='black', edgecolor='black') + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.12 * visual_cell_width + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + # 垂直黑线 + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.5) + + # 水平蓝线 + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.4) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dyb = 0.5 * visual_cell_width + yb = yref - dyb - 0.1 # (r,s) 稍微低一点 + ybc = yref - 0.5 * dyb + ytt = yref + 0.52 * dyb # vertex 标签在最上方 + + # === vertex 标签(只画当前行实际存在的) === + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + offset = v + frac = int(round(abs(offset) * 2)) # 1,3,5,7 + sign = '+' if offset > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + offset * visual_cell_width + plt.text(xv, ytt, label, fontsize=12, ha='center', va='bottom') + + # === cell 标签 === + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, ybc, r'$i$', fontsize=14, ha='center', color='red') + elif m > 0: + plt.text(xc, ybc, rf'$i+{m}$', fontsize=12, ha='center') + else: + plt.text(xc, ybc, rf'$i-{ -m }$', fontsize=12, ha='center') + + # (r,s) 标注 + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, yb, rf'$(r={r},\; s={s})$', fontsize=13, ha='center', color='darkblue') + +def getrs(k, rv, sv): + kk = k - 1 + for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(12, 6.5)) # 稍微高一点容纳 4 行 + +rv, sv = [], [] +getrs(k, rv, sv) +print(f'生成的 (r,s) 顺序: {list(zip(rv, sv))}') # [ (2,0), (1,1), (0,2), (-1,3) ] + +for i in range(len(rv)): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.xlim(-1, 13) # 基本贴边(2.0 时 -5→+7 相对单位) +plt.ylim(-len(rv)*dyref - 1, 1.5) +plt.tight_layout() +plt.savefig('cfd_stencil_bias.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03c/testprj.py b/example/figure/1d/eno/03c/testprj.py new file mode 100644 index 00000000..28f2160f --- /dev/null +++ b/example/figure/1d/eno/03c/testprj.py @@ -0,0 +1,97 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只需调这几个) ========================== +visual_cell_width = 2.0 # 2.0 时左右几乎完美贴边(12英寸宽) +center_x = 5.0 # 精确居中(左2.5 + 右3.5 = 6个单元 → center=2.5×2.0=5.0) +dyref = 2.35 # 行间距,彻底消除垂直交叉(可继续调大更疏朗) +k = 3 # 保持3 → 产生你想要的4种偏置 +# ========================================================================= + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140, facecolor='black', edgecolor='black', linewidth=1) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.12 * visual_cell_width + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + # 垂直黑线 + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.6) + + # 水平蓝线(cell) + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.6) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dyb = 0.5 * visual_cell_width # 1.0 + y_vertex = yref + 0.68 * dyb # vertex 标签(最上方) + y_cell = yref - 0.68 * dyb # cell 标签(中间偏下) + y_rs = yref - 1.05 * dyb # (r,s) 标签(最下方,保证不交叉) + + # === vertex 标签(只画当前行实际存在的)=== + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=13, ha='center', va='bottom') + + # === cell 标签 === + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=15, ha='center', color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=13, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=13, ha='center') + + # === (r,s) 标注(放在最下方,永远不会和下一行 vertex 交叉)=== + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=14, ha='center', color='darkblue', weight='bold') + +def getrs(k, rv, sv): + kk = k - 1 + for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(12, 7.8)) # 稍微高一点,4行更舒适 + +rv, sv = [], [] +getrs(k, rv, sv) +print(f'生成的 (r,s) 顺序: {list(zip(rv, sv))}') # 你会看到 [(2, 0), (1, 1), (0, 2), (-1, 3)] + +for i in range(len(rv)): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.tight_layout() +plt.savefig('cfd_stencil_final.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03d/testprj.py b/example/figure/1d/eno/03d/testprj.py new file mode 100644 index 00000000..e3abb5e4 --- /dev/null +++ b/example/figure/1d/eno/03d/testprj.py @@ -0,0 +1,95 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数 ========================== +visual_cell_width = 2.0 +center_x = 5.0 # 精确居中(左2.5 + 右3.5 = 6个单元宽) +dyref = 2.35 # 行间距(保证上下完全不交叉) +k = 3 +# ========================================================================= + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140, facecolor='black', edgecolor='black', linewidth=1) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.12 * visual_cell_width + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.6) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.6) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dyb = 0.5 * visual_cell_width # = 1.0 + # 关键调整:vertex 标签大幅贴近网格线 + y_vertex = yref + 0.28 * dyb # ← 原来0.68,现在0.28,明显更近 + y_cell = yref - 0.68 * dyb + y_rs = yref - 1.05 * dyb # (r,s) 仍保持在最下方,安全距离 + + # === vertex 标签(只画当前行实际存在的)=== + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=13.5, ha='center', va='bottom', color='black') + + # === cell 标签 === + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=15, ha='center', color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=13, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=13, ha='center') + + # === (r,s) === + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=14, ha='center', color='darkblue', weight='bold') + +def getrs(k, rv, sv): + kk = k - 1 + for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(12, 7.8)) + +rv, sv = [], [] +getrs(k, rv, sv) + +for i in range(len(rv)): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.tight_layout() +plt.savefig('cfd_stencil_final_tighter.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03e/testprj.py b/example/figure/1d/eno/03e/testprj.py new file mode 100644 index 00000000..5e570484 --- /dev/null +++ b/example/figure/1d/eno/03e/testprj.py @@ -0,0 +1,116 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # x方向宽度(英寸),改大 → 更宽 +fig_height = 8.0 # y方向高度(英寸),改小 → 更扁 +# 长宽比随意调,例如: +# fig_width = 20.0, fig_height = 7.0 → 约 2.85:1 +# fig_width = 18.0, fig_height = 6.0 → 3:1 +# fig_width = 14.0, fig_height = 9.0 → 约 1.55:1 + +k = 3 # 你的偏置阶数 +# ========================================================================= + +# === 缩放因子(以 16×8 为基准)=== +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +# === 水平尺寸(永远占满宽度)=== +visual_cell_width = fig_width / 6.0 # 最大跨度正好 6 个 cell +center_x = 2.5 * visual_cell_width # 精确居中(左2.5 + 右3.5) + +# === 垂直尺寸(完全独立于水平)=== +base_dyref = 1.85 # 基准高度 8 英寸、4 行时的行间距 +rv, sv = [], [] +kk = k - 1 +for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) +num_rows = len(rv) + +dyref = base_dyref * scale_y * (4.0 / max(num_rows, 1)) # 行数自动适配 +vertical_half = dyref * 0.54 # ≈1.0(基准时),所有垂直偏移都基于它 + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + dy = 0.25 * vertical_half # 短竖线长度随垂直缩放 + + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + y_vertex = yref + 0.25 * vertical_half + y_cell = yref - 0.68 * vertical_half + y_rs = yref - 1.18 * vertical_half + + # vertex 标签 + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + + + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # cell 标签 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + + # (r,s) + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=15*scale_y, ha='center', + color='darkblue', weight='bold') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +for i in range(num_rows): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +plt.axis('off') +plt.tight_layout(pad=0.3) +plt.savefig('cfd_stencil_perfect_ratio.png', bbox_inches='tight', dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/eno/03f/testprj.py b/example/figure/1d/eno/03f/testprj.py new file mode 100644 index 00000000..4d4ad9ae --- /dev/null +++ b/example/figure/1d/eno/03f/testprj.py @@ -0,0 +1,120 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # x方向宽度(英寸) +fig_height = 8.0 # y方向高度(英寸),随意改实现任意长宽比 +# 示例:20x8、18x6、24x9 等都完美自适应不交叉 + +k = 3 +# ========================================================================= + +# === 缩放因子(以 16×8 为基准)=== +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +# === 水平尺寸(图形略微放大 → 边距自然极小)=== +visual_cell_width = fig_width / 6.9 # 6.9 → 边距更接近 Word “窄”边距(≈0.4~0.6cm) +center_x = 3.0 * visual_cell_width # 完美居中(左2.5 + 右3.5 的平均) + +# === 垂直尺寸 === +base_dyref = 1.85 +rv, sv = [], [] +kk = k - 1 +for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) +num_rows = len(rv) + +dyref = base_dyref * scale_y * (4.0 / max(num_rows, 1)) +vertical_unit = dyref * 0.54 + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.25 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + y_vertex = yref + 0.25 * vertical_unit + y_cell = yref - 0.68 * vertical_unit + y_rs = yref - 1.05 * vertical_unit + + # vertex 标签 + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # cell 标签 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + + # (r,s) + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=15*scale_y, ha='center', + color='darkblue', weight='bold') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +for i in range(num_rows): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +# === 极小边距(真正像 Word “窄”边距)=== +margin_x = 0.12 * visual_cell_width # 左右 ≈0.3~0.5cm +margin_y = 0.25 * vertical_unit # 上下极小(保证不裁剪最后一行的 (r,s)) + +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x +min_y = -(num_rows-1)*dyref - 1.3*vertical_unit - margin_y +max_y = 0 + 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') + +plt.savefig('cfd_stencil_word_narrow.png', bbox_inches='tight', pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01/testprj.py b/example/figure/1d/finite_difference/01/testprj.py new file mode 100644 index 00000000..98f0600f --- /dev/null +++ b/example/figure/1d/finite_difference/01/testprj.py @@ -0,0 +1,500 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,0, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01a/testprj.py b/example/figure/1d/finite_difference/01a/testprj.py new file mode 100644 index 00000000..f569dd3e --- /dev/null +++ b/example/figure/1d/finite_difference/01a/testprj.py @@ -0,0 +1,614 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,0, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01b/testprj.py b/example/figure/1d/finite_difference/01b/testprj.py new file mode 100644 index 00000000..87cefef2 --- /dev/null +++ b/example/figure/1d/finite_difference/01b/testprj.py @@ -0,0 +1,600 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + #if self.cellbased == 0: + # nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrap(self, coor_list, ghost_mesh, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = ghost_mesh.xcc[itgt], ghost_mesh.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + self.draw_periodic_connections_wrap(left_list, self.ghost_mesh_left) + self.draw_periodic_connections_wrap(right_list, self.ghost_mesh_right,-1) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,0, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01c/testprj.py b/example/figure/1d/finite_difference/01c/testprj.py new file mode 100644 index 00000000..2ed1761d --- /dev/null +++ b/example/figure/1d/finite_difference/01c/testprj.py @@ -0,0 +1,646 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + #if self.cellbased == 0: + # nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrapOld(self, coor_list, ghost_mesh, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = ghost_mesh.xcc[itgt], ghost_mesh.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def draw_periodic_connections_wrap(self, coor_list, xsrc, ysrc, xtgt, ytgt, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = xsrc[isrc], ysrc[isrc] + tgt_x, tgt_y = xtgt[itgt], ytgt[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + if self.cellbased == 1: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + else: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + self.draw_periodic_connections_wrap( + left_list, + self.xcc, + self.ycc, + self.ghost_mesh_left.xcc, + self.ghost_mesh_left.ycc + ) + self.draw_periodic_connections_wrap( + right_list, + self.xcc, + self.ycc, + self.ghost_mesh_right.xcc, + self.ghost_mesh_right.ycc, + -1 + ) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01d/testprj.py b/example/figure/1d/finite_difference/01d/testprj.py new file mode 100644 index 00000000..995ff708 --- /dev/null +++ b/example/figure/1d/finite_difference/01d/testprj.py @@ -0,0 +1,636 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + #if self.cellbased == 0: + # nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrap(self, coor_list, xsrc, ysrc, xtgt, ytgt, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = xsrc[isrc], ysrc[isrc] + tgt_x, tgt_y = xtgt[itgt], ytgt[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + if self.cellbased == 1: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.xcc, + self.ycc, + self.ghost_mesh_left.xcc, + self.ghost_mesh_left.ycc + ) + self.draw_periodic_connections_wrap( + right_list, + self.xcc, + self.ycc, + self.ghost_mesh_right.xcc, + self.ghost_mesh_right.ycc, + -1 + ) + else: + inodemax = self.nnodes - 1 + for i in range(self.nghosts): + left_list.append((inodemax-i, i+1, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i+1, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.x, + self.y, + self.ghost_mesh_left.x, + self.ghost_mesh_left.y + ) + self.draw_periodic_connections_wrap( + right_list, + self.x, + self.y, + self.ghost_mesh_right.x, + self.ghost_mesh_right.y, + -1 + ) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/finite_difference/01e/testprj.py b/example/figure/1d/finite_difference/01e/testprj.py new file mode 100644 index 00000000..2613bd5a --- /dev/null +++ b/example/figure/1d/finite_difference/01e/testprj.py @@ -0,0 +1,636 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + + def plot_vertical_lines_at_cell_center(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(self.ncells): + xm = self.xcc[i] + ym = self.ycc[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + #self.cellbased = 1 + self.cellbased = 0 + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot_node_label(self): + ytext_shift = 0.8*abs(self.dx) + for i in range(self.nnodes): + if self.lr == "L": + node_label = f"${-i+1+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + node_label = f"$N$" + else: + node_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.x[i], self.y[i]-ytext_shift, node_label, fontsize=12, ha='center') + + def plot_cell_based_figure(self): + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_node_based_figure(self): + self.plot_node_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.x, self.y, s=50, facecolor='red', edgecolor='black', linewidth=1) + + self.plot_vertical_lines_at_cell_center() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + #self.cellbased = 1 + self.cellbased = 0 + nghostcells = self.nghosts + if self.cellbased == 0: + nghostcells = self.nghosts - 1 + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=nghostcells, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_node_label(self): + dytext = 0.8*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.nnodes): + if i < self.nlabels: + node_label = f"${i+1+self.ishift}$" + elif i > self.nnodes - 1 - self.nlabels: + inew = i - (self.nnodes - 1) + self.ishift + 1 + node_label = self.get_dollar_label("N",inew) + else: + node_label="" + # Add text label at cell center + plt.text(self.x[i], self.y[i]-dytext, node_label, fontsize=12, ha='center') + icenter = self.nnodes // 2 + plt.text(self.x[icenter], self.y[icenter]-dytext, f"$i$", fontsize=12, ha='center') + node_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + #xm = self.xcc[self.ncells-1] + self.dx + #ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={node_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_node(self): + # Plot main mesh cell-center points (black fill with black edge) + x_new = [] + y_new = [] + for i in range(self.nnodes): + if self.nodemark[i] == 1: + x_new.append( self.x[i] ) + y_new.append( self.y[i] ) + plt.scatter(x_new, y_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot_node_mesh(self): + self.icenter = self.nnodes // 2 + self.nlabels = self.nghosts + self.nodemark = np.zeros(self.nnodes, dtype=int) + for i in range(self.nnodes): + if i < self.nlabels: + self.nodemark[i] = 1 + elif i > self.nnodes - 1 - self.nlabels: + self.nodemark[i] = 1 + self.nodemark[self.icenter] = 1 + + self.plot_node() + self.plot_node_label() + + # Plot horizontal line connecting main mesh nodes + for ic in range(self.ncells): + ls_left = "k-" if self.nodemark[ic] == 1 else "k--" + plt.plot([self.x[ic], self.xcc[ic]], [self.y[ic], self.ycc[ic]], ls_left, linewidth=1) + + ls_right = "k-" if self.nodemark[ic+1] == 1 else "k--" + plt.plot([self.xcc[ic], self.x[ic+1]], [self.ycc[ic], self.y[ic+1]], ls_right, linewidth=1) + + def plot_cell_based_figure(self): + self.plot_cell_mesh() + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_node_based_figure(self): + self.plot_node_mesh() + self.plot_vertical_lines_at_cell_center() + self.plot_boundary_vertical_interface_lines() + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot(self): + if self.cellbased == 1: + self.plot_cell_based_figure() + else: + self.plot_node_based_figure() + + def draw_periodic_connections_wrap(self, coor_list, xsrc, ysrc, xtgt, ytgt, coeff=1): + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + for i in range(len(coor_list)): + isrc, itgt, color = coor_list[i] + src_x, src_y = xsrc[isrc], ysrc[isrc] + tgt_x, tgt_y = xtgt[itgt], ytgt[itgt] + + dy = i * vlen + end_y_src = src_y + coeff *( vlen + offset_y + dy ) + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + if self.cellbased == 1: + icellmax = self.ncells - 1 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.xcc, + self.ycc, + self.ghost_mesh_left.xcc, + self.ghost_mesh_left.ycc + ) + self.draw_periodic_connections_wrap( + right_list, + self.xcc, + self.ycc, + self.ghost_mesh_right.xcc, + self.ghost_mesh_right.ycc, + -1 + ) + else: + inodemax = self.nnodes - 1 + for i in range(self.nghosts): + left_list.append((inodemax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + self.draw_periodic_connections_wrap( + left_list, + self.x, + self.y, + self.ghost_mesh_left.x, + self.ghost_mesh_left.y + ) + self.draw_periodic_connections_wrap( + right_list, + self.x, + self.y, + self.ghost_mesh_right.x, + self.ghost_mesh_right.y, + -1 + ) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/mesh/01/00_testanimation.py b/example/figure/1d/mesh/01/00_testanimation.py new file mode 100644 index 00000000..53ae992e --- /dev/null +++ b/example/figure/1d/mesh/01/00_testanimation.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) + +# 初始化数据 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') + +# 动画更新函数 +def animate(frame): + # 随着帧数增加,x 数据偏移,实现波形移动 + y = np.sin(x + frame / 10.0) + line.set_ydata(y) + return line, + +# 创建动画:100 帧,每帧间隔 20ms,支持 blit 优化(更快渲染) +ani = animation.FuncAnimation(fig, animate, frames=100, interval=20, blit=True) + +# 显示动画(在 Jupyter 中可用 plt.show(),否则保存为 GIF) +plt.show() + +# 可选:保存为 GIF 文件(需要 pillow 或 imagemagick) +# ani.save('sine_wave.gif', writer='pillow', fps=30) \ No newline at end of file diff --git a/example/figure/1d/mesh/01/01_cfd_grid_storage.py b/example/figure/1d/mesh/01/01_cfd_grid_storage.py new file mode 100644 index 00000000..9f686ecc --- /dev/null +++ b/example/figure/1d/mesh/01/01_cfd_grid_storage.py @@ -0,0 +1,214 @@ +""" +文件名: 01_cfd_grid_storage.py +功能: 绘制一维CFD基础网格与变量存储示意图 +包含: 顶点中心存储、单元中心存储、边界条件处理、有限差分格式示意 +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14, + 'figure.dpi': 100 + }) + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + setup_plot_style() + fig = plt.figure(figsize=(15, 10)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("Vertex-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=120, color='red', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记顶点 + for i, x in enumerate(x_vertices): + if i == 0: + label = f'Boundary\n$u_0$' + color = "orange" + elif i == len(x_vertices) - 1: + label = f'Boundary\n$u_{i}$' + color = "orange" + else: + label = f'Storage\n$u_{i}$' + color = "yellow" + + ax1.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor=color, alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + rect = ax1.add_patch(Rectangle((x_vertices[i], -0.08), dx, 0.16, + alpha=0.2, color='blue', + edgecolor='blue', linewidth=1.5)) + ax1.text(center, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue", alpha=0.8)) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.35, 0.35) + ax1.set_xlabel('Position x') + ax1.set_ylabel('Variable Storage') + ax1.text(0.5, 0.95, 'Variables stored at vertices', transform=ax1.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("Cell-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=120, color='green', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记变量 + for i, x in enumerate(x_centers): + label = f'Storage\n$u_{i}$' + ax2.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + rect = ax2.add_patch(Rectangle((x, -0.08), dx, 0.16, + alpha=0.2, color='orange', + edgecolor='orange', linewidth=1.5)) + ax2.text(x + dx/2, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="peachpuff", alpha=0.8)) + + # 标记边界 + ax2.axvline(x_vertices[0], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.axvline(x_vertices[-1], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.text(x_vertices[0], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + ax2.text(x_vertices[-1], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.35, 0.35) + ax2.set_xlabel('Position x') + ax2.set_ylabel('Variable Storage') + ax2.text(0.5, 0.95, 'Variables stored at cell centers', transform=ax2.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("Boundary Conditions and Ghost Cells", fontsize=14, fontweight='bold', pad=20) + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=80, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=120, color='blue', + edgecolors='black', linewidth=1.5, zorder=5) + + # 标记边界 + ax3.axvline(0, color='red', linestyle='-', linewidth=3, alpha=0.8) + ax3.axvline(n_cells*dx, color='red', linestyle='-', linewidth=3, alpha=0.8) + + # 填充虚拟点区域 + ax3.axvspan(-dx, 0, alpha=0.15, color='red', hatch='//') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.15, color='red', hatch='//') + + # 标记点类型 + ax3.text(-dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + ax3.text(n_cells*dx + dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + + # 标记计算区域 + ax3.axvspan(0, n_cells*dx, alpha=0.1, color='green') + ax3.text(n_cells*dx/2, -0.2, 'Computational Domain', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8)) + + # 添加箭头示意边界条件 + ax3.annotate('BC: u=0', xy=(0, 0), xytext=(-1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.annotate('BC: ∂u/∂x=0', xy=(n_cells*dx, 0), + xytext=(n_cells*dx + 1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.set_xlim(-1.8*dx, (n_cells+1.8)*dx) + ax3.set_ylim(-0.3, 0.4) + ax3.set_xlabel('Position x') + ax3.set_ylabel('Domain') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("Finite Difference Schemes", fontsize=14, fontweight='bold', pad=20) + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=3, alpha=0.5, label='Exact Solution') + ax4.scatter(x[i], u[i], s=200, color='red', zorder=5, + edgecolors='black', linewidth=1.5, label='Current point $u_i$') + ax4.scatter(x[i-1], u[i-1], s=150, color='green', zorder=4, + edgecolors='black', linewidth=1, label='Upstream $u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=150, color='orange', zorder=4, + edgecolors='black', linewidth=1, label='Downstream $u_{i+1}$') + + # 绘制差分示意线 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5, linewidth=1.5) + + # 标注差分格式 + ax4.annotate('Central Difference\n(2nd order)', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.4), + arrowprops=dict(arrowstyle="->", color='blue', lw=2), + ha='center', fontsize=11, color='blue', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.annotate('Upwind Scheme\n(1st order)', xy=(x[i], u[i]), xytext=(x[i]-2.5, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red', lw=2), + ha='center', fontsize=11, color='red', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.set_xlabel('Position x') + ax4.set_ylabel('Variable u') + ax4.legend(loc='upper right', fontsize=10) + ax4.grid(True, alpha=0.3) + + # 添加整体标题 + plt.suptitle('1D CFD Grid and Variable Storage Illustration', fontsize=16, fontweight='bold', y=1.02) + + plt.tight_layout() + plt.savefig('01_cfd_grid_storage.png', dpi=300, bbox_inches='tight', facecolor='white') + plt.show() + +if __name__ == "__main__": + plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/02_convection_schemes.py b/example/figure/1d/mesh/01/02_convection_schemes.py new file mode 100644 index 00000000..a2b2c413 --- /dev/null +++ b/example/figure/1d/mesh/01/02_convection_schemes.py @@ -0,0 +1,126 @@ +""" +文件名: 02_convection_schemes.py +功能: 绘制一维对流方程不同数值格式对比 +包含: 迎风格式、中心差分格式、QUICK格式的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_convection_schemes(): + """绘制不同对流格式示意图""" + setup_plot_style() + + # 创建数据 + x = np.linspace(0, 2*np.pi, 100) + u_exact = np.sin(x) + + # 添加数值噪声模拟数值解 + np.random.seed(42) + u_upwind = u_exact + 0.1*np.random.randn(len(x)) # 迎风格式(有耗散) + u_central = u_exact + 0.05*np.random.randn(len(x)) # 中心差分(有振荡) + u_quick = u_exact + 0.02*np.random.randn(len(x)) # QUICK格式 + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 迎风格式 + ax = axes[0, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_upwind, 'r--', linewidth=2, marker='o', markersize=4, + markevery=5, label='Upwind Scheme') + ax.fill_between(x, u_exact-0.15, u_exact+0.15, alpha=0.1, color='gray') + ax.set_title('Upwind Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nDissipation', xy=(3, 0), xytext=(4, -0.8), + arrowprops=dict(arrowstyle="->", color='red'), + fontsize=10, color='red') + + # 2. 中心差分格式 + ax = axes[0, 1] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_central, 'b--', linewidth=2, marker='s', markersize=4, + markevery=5, label='Central Difference') + ax.set_title('Central Difference Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nOscillation', xy=(1.5, 0.5), xytext=(0.5, 0.8), + arrowprops=dict(arrowstyle="->", color='blue'), + fontsize=10, color='blue') + + # 3. QUICK格式 + ax = axes[1, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_quick, 'g--', linewidth=2, marker='^', markersize=4, + markevery=5, label='QUICK Scheme') + ax.set_title('QUICK Scheme (3rd Order)', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 格式示意 + ax = axes[1, 1] + ax.set_title('Grid Point Dependencies', fontsize=12, fontweight='bold') + + # 绘制网格点 + points_x = np.array([0, 1, 2, 3, 4]) + points_y = np.zeros_like(points_x) + + ax.scatter(points_x, points_y, s=200, color='gray') + + # 标记点 + labels = ['$u_{i-2}$', '$u_{i-1}$', '$u_i$', '$u_{i+1}$', '$u_{i+2}$'] + for i, (x_pos, label) in enumerate(zip(points_x, labels)): + ax.text(x_pos, 0.1, label, ha='center', fontsize=12, fontweight='bold') + + # 迎风格式依赖 + ax.annotate('', xy=(2, 0), xytext=(1, 0), + arrowprops=dict(arrowstyle='<-', color='red', lw=2)) + ax.text(1.5, -0.15, 'Upwind', ha='center', color='red', fontweight='bold') + + # 中心差分依赖 + ax.annotate('', xy=(2, 0), xytext=(1, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.annotate('', xy=(2, 0), xytext=(3, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.text(2, -0.2, 'Central', ha='center', color='blue', fontweight='bold') + + # QUICK格式依赖 + ax.annotate('', xy=(2, 0), xytext=(0, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(1, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(3, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.text(2, 0.2, 'QUICK', ha='center', color='green', fontweight='bold') + + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.3, 0.3) + ax.set_xlabel('Grid Point Index') + ax.grid(True, alpha=0.3) + ax.set_yticks([]) + + plt.suptitle('Comparison of Convection Schemes for 1D CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('02_convection_schemes.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_convection_schemes() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/03_interpolation_methods.py b/example/figure/1d/mesh/01/03_interpolation_methods.py new file mode 100644 index 00000000..44eaf808 --- /dev/null +++ b/example/figure/1d/mesh/01/03_interpolation_methods.py @@ -0,0 +1,119 @@ +""" +文件名: 03_interpolation_methods.py +功能: 绘制不同插值方法对比 +包含: 线性插值、二次插值、三次样条插值的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt +from scipy import interpolate + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_interpolation_methods(): + """绘制不同插值方法对比""" + setup_plot_style() + + # 创建粗网格和细网格 + x_coarse = np.linspace(0, 10, 6) + u_coarse = np.sin(x_coarse * 0.8) + + x_fine = np.linspace(0, 10, 100) + u_exact = np.sin(x_fine * 0.8) + + # 不同插值方法 + # 线性插值 + f_linear = interpolate.interp1d(x_coarse, u_coarse, kind='linear') + u_linear = f_linear(x_fine) + + # 二次插值 + f_quadratic = interpolate.interp1d(x_coarse, u_coarse, kind='quadratic') + u_quadratic = f_quadratic(x_fine) + + # 三次样条插值 + f_cubic = interpolate.CubicSpline(x_coarse, u_coarse) + u_cubic = f_cubic(x_fine) + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 线性插值 + ax = axes[0, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_linear, 'r--', linewidth=2, label='Linear Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Linear Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 2. 二次插值 + ax = axes[0, 1] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_quadratic, 'g--', linewidth=2, label='Quadratic Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Quadratic Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 3. 三次样条插值 + ax = axes[1, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_cubic, 'b--', linewidth=2, label='Cubic Spline') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Cubic Spline Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 误差比较 + ax = axes[1, 1] + errors = { + 'Linear': np.abs(u_linear - u_exact), + 'Quadratic': np.abs(u_quadratic - u_exact), + 'Cubic Spline': np.abs(u_cubic - u_exact) + } + + x_pos = np.arange(len(errors)) + mean_errors = [np.mean(err) for err in errors.values()] + max_errors = [np.max(err) for err in errors.values()] + + width = 0.35 + ax.bar(x_pos - width/2, mean_errors, width, label='Mean Error', color='skyblue') + ax.bar(x_pos + width/2, max_errors, width, label='Max Error', color='salmon') + + ax.set_xlabel('Interpolation Method') + ax.set_ylabel('Error') + ax.set_title('Interpolation Error Comparison', fontsize=12, fontweight='bold') + ax.set_xticks(x_pos) + ax.set_xticklabels(list(errors.keys())) + ax.legend() + ax.grid(True, alpha=0.3, axis='y') + + # 添加数值标签 + for i, (mean_err, max_err) in enumerate(zip(mean_errors, max_errors)): + ax.text(i - width/2, mean_err + 0.001, f'{mean_err:.3f}', + ha='center', va='bottom', fontsize=9) + ax.text(i + width/2, max_err + 0.001, f'{max_err:.3f}', + ha='center', va='bottom', fontsize=9) + + plt.suptitle('Comparison of Interpolation Methods for CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('03_interpolation_methods.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_interpolation_methods() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/04_cfd_animation.py b/example/figure/1d/mesh/01/04_cfd_animation.py new file mode 100644 index 00000000..566d7d3b --- /dev/null +++ b/example/figure/1d/mesh/01/04_cfd_animation.py @@ -0,0 +1,372 @@ +""" +文件名: 04_cfd_animation.py +功能: 创建CFD计算过程动画 +修复: 分离保存动画和显示动画的过程,避免AttributeError +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation, PillowWriter, FFMpegWriter +import warnings +import sys +import os + +warnings.filterwarnings('ignore') + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14, + 'figure.dpi': 100, + 'savefig.dpi': 150, + 'animation.embed_limit': 100 # 增加嵌入限制 + }) + +def simulate_convection_equation(): + """模拟对流方程的解""" + # 设置网格和参数 + L = 10.0 # 计算域长度 + nx = 100 # 网格点数 + dx = L / nx + x = np.linspace(0, L, nx) + + # 时间参数 + nt = 80 # 减少帧数以加快速度 + dt = 0.05 + + # 初始化变量 + u_exact = np.zeros((nt, nx)) + u_numerical = np.zeros((nt, nx)) + + # 初始条件:高斯波包 + u0 = np.exp(-(x - L/4)**2 / 0.5) * np.sin(3 * x) + + # 生成精确解(对流方程) + c = 0.5 # 对流速度 + for n in range(nt): + # 精确解:简单对流 + shift = c * n * dt + u_exact[n, :] = np.exp(-(x - shift - L/4)**2 / 0.5) * np.sin(3 * (x - shift)) + + # 数值解:使用一阶迎风格式 + if n == 0: + u_numerical[n, :] = u0 + else: + for i in range(1, nx): + # 一阶迎风格式 + u_numerical[n, i] = u_numerical[n-1, i] - c*dt/dx * ( + u_numerical[n-1, i] - u_numerical[n-1, i-1]) + # 边界条件:左侧固定为零 + u_numerical[n, 0] = 0.0 + + return x, u_exact, u_numerical, nt, dt + +def save_animation_only(): + """仅保存动画,不显示""" + print("正在创建并保存动画...") + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画对象 + ani = FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True) + + # 尝试保存动画 + saved = False + + # 首先尝试保存为mp4 + try: + print("尝试保存为MP4格式...") + writer = FFMpegWriter(fps=10, metadata=dict(artist='CFD Visualization'), bitrate=1800) + ani.save('04_cfd_animation.mp4', writer=writer) + print("✓ 动画已成功保存为 '04_cfd_animation.mp4'") + saved = True + except Exception as e: + print(f"MP4保存失败: {e}") + + # 如果mp4失败,尝试保存为gif + if not saved: + try: + print("尝试保存为GIF格式...") + writer = PillowWriter(fps=10) + ani.save('04_cfd_animation.gif', writer=writer) + print("✓ 动画已保存为 '04_cfd_animation.gif'") + saved = True + except Exception as e: + print(f"GIF保存失败: {e}") + + # 关闭图形以释放资源 + plt.close(fig) + + if saved: + print("\n动画保存完成!文件已生成。") + print("您可以使用视频播放器查看保存的动画文件。") + else: + print("\n无法保存动画,将创建静态图像...") + create_static_frames() + + return saved + +def create_static_frames(): + """创建静态的关键帧图像""" + print("正在创建静态关键帧图像...") + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 选择几个关键帧 + key_frames = [0, nt//4, nt//2, 3*nt//4, nt-1] + frame_names = ['Initial', 'Quarter', 'Half', 'Three Quarters', 'Final'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for idx, (frame, name) in enumerate(zip(key_frames, frame_names)): + if idx >= len(axes): + break + + ax = axes[idx] + ax.plot(x, u_exact[frame, :], 'b-', linewidth=2, label='Exact', alpha=0.7) + ax.plot(x, u_numerical[frame, :], 'r--', linewidth=2, label='Numerical') + ax.scatter(x[::10], u_numerical[frame, ::10], s=30, color='red', alpha=0.5) + + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('u') + ax.set_title(f'{name} (t={frame*dt:.2f}s)') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + # 第6个子图:误差随时间变化 + ax = axes[5] + errors = np.abs(u_numerical - u_exact) + mean_errors = np.mean(errors, axis=1) + max_errors = np.max(errors, axis=1) + + time = np.arange(nt) * dt + ax.plot(time, mean_errors, 'b-', label='Mean Error') + ax.plot(time, max_errors, 'r-', label='Max Error') + ax.fill_between(time, 0, mean_errors, alpha=0.3, color='blue') + ax.fill_between(time, 0, max_errors, alpha=0.1, color='red') + + ax.set_xlabel('Time') + ax.set_ylabel('Error') + ax.set_title('Numerical Error Evolution') + ax.grid(True, alpha=0.3) + ax.legend() + + plt.suptitle('1D Convection Equation: Key Frames and Error Analysis', + fontsize=16, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static_frames.png', dpi=300, bbox_inches='tight') + print("✓ 静态关键帧已保存为 '04_cfd_static_frames.png'") + plt.close(fig) + + return True + +def show_animation_only(): + """仅显示动画,不保存""" + print("正在创建动画以供显示...") + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建新的图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation (Live)', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s | Frame: {frame}/{nt}') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画 + ani = FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True, interval=100, repeat=False) + + print("动画准备就绪,显示窗口...") + print("提示:关闭窗口以继续程序。") + + plt.tight_layout() + plt.show() + + return ani + +def check_ffmpeg_available(): + """检查ffmpeg是否可用""" + try: + import subprocess + result = subprocess.run(['ffmpeg', '-version'], + capture_output=True, text=True, shell=True) + return result.returncode == 0 + except: + return False + +def main(): + """主函数""" + print("=" * 60) + print("1D CFD 动画演示程序") + print("=" * 60) + + # 检查ffmpeg + has_ffmpeg = check_ffmpeg_available() + print(f"FFmpeg可用: {'✓' if has_ffmpeg else '✗'}") + + print("\n选项:") + print("1. 保存动画为视频文件(不显示)") + print("2. 显示动画(不保存)") + print("3. 创建静态关键帧图像") + + try: + choice = int(input("\n请选择 (1-3, 默认=1): ") or "1") + except: + choice = 1 + + setup_plot_style() + + if choice == 1: + print("\n" + "=" * 50) + print("选项1: 保存动画为视频文件") + print("=" * 50) + save_animation_only() + + elif choice == 2: + print("\n" + "=" * 50) + print("选项2: 显示动画") + print("=" * 50) + print("注意:动画将在新窗口中显示") + print("关闭窗口后程序将继续运行") + show_animation_only() + + elif choice == 3: + print("\n" + "=" * 50) + print("选项3: 创建静态关键帧图像") + print("=" * 50) + create_static_frames() + + else: + print("\n无效选择,使用默认选项...") + save_animation_only() + + print("\n" + "=" * 60) + print("程序执行完成!") + print("生成的文件:") + + # 列出生成的文件 + files_to_check = [ + ('04_cfd_animation.mp4', '动画视频文件'), + ('04_cfd_animation.gif', '动画GIF文件'), + ('04_cfd_static_frames.png', '静态关键帧图像') + ] + + for filename, description in files_to_check: + if os.path.exists(filename): + file_size = os.path.getsize(filename) / 1024 # KB + print(f" ✓ {filename} - {description} ({file_size:.1f} KB)") + else: + print(f" ✗ {filename} - 未生成") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\n程序被用户中断。") + except Exception as e: + print(f"\n程序执行出错: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/example/figure/1d/mesh/01/04_cfd_animationBAK.py b/example/figure/1d/mesh/01/04_cfd_animationBAK.py new file mode 100644 index 00000000..f3c274c9 --- /dev/null +++ b/example/figure/1d/mesh/01/04_cfd_animationBAK.py @@ -0,0 +1,299 @@ +""" +文件名: 04_cfd_animation.py +功能: 创建CFD计算过程动画 +包含: 波动方程的数值解演化过程 +修复: 解决了动画保存后plt.show()的AttributeError问题 +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import warnings +warnings.filterwarnings('ignore') + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 12, + 'axes.titlesize': 14, + 'axes.labelsize': 12, + 'figure.titlesize': 16 + }) + +def simulate_convection_equation(): + """模拟对流方程的解""" + # 设置网格和参数 + L = 10.0 # 计算域长度 + nx = 100 # 网格点数 + dx = L / nx + x = np.linspace(0, L, nx) + + # 时间参数 + nt = 100 # 减少帧数以加快速度 + dt = 0.05 + + # 初始化变量 + u_exact = np.zeros((nt, nx)) + u_numerical = np.zeros((nt, nx)) + + # 初始条件:高斯波包 + u0 = np.exp(-(x - L/4)**2 / 0.5) * np.sin(3 * x) + + # 生成精确解(对流方程) + c = 0.5 # 对流速度 + for n in range(nt): + # 精确解:简单对流 + shift = c * n * dt + u_exact[n, :] = np.exp(-(x - shift - L/4)**2 / 0.5) * np.sin(3 * (x - shift)) + + # 数值解:使用一阶迎风格式 + if n == 0: + u_numerical[n, :] = u0 + else: + for i in range(1, nx): + # 一阶迎风格式 + u_numerical[n, i] = u_numerical[n-1, i] - c*dt/dx * ( + u_numerical[n-1, i] - u_numerical[n-1, i-1]) + # 边界条件:左侧固定为零 + u_numerical[n, 0] = 0.0 + + return x, u_exact, u_numerical, nt, dt + +def create_cfd_animation(save_video=True, show_plot=True): + """创建CFD计算过程动画""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 添加说明文本 + info_text = fig.text(0.5, 0.02, + 'Convection velocity: c = 0.5 | CFL number: ~0.5', + ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + # 每隔5个点显示一个标记点 + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s | Frame: {frame}/{nt}') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True, interval=100, repeat=False) + + # 保存动画(可选) + if save_video: + try: + print("正在保存动画...") + # 尝试使用不同的写入器 + try: + ani.save('04_cfd_animation.mp4', writer='ffmpeg', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.mp4'") + except: + # 如果ffmpeg不可用,尝试保存为gif + ani.save('04_cfd_animation.gif', writer='pillow', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.gif' (使用pillow写入器)") + except Exception as e: + print(f"保存动画失败: {e}") + print("请确保已安装必要的视频编码器(如ffmpeg)或pillow库") + + # 显示动画(可选) + if show_plot: + try: + plt.tight_layout() + plt.show() + except Exception as e: + print(f"显示动画时出错: {e}") + print("这可能是因为图形窗口已关闭,但这是正常的。") + + return ani + +def create_static_frames(): + """创建静态的关键帧图像,作为替代方案""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 选择几个关键帧 + key_frames = [0, nt//4, nt//2, 3*nt//4, nt-1] + frame_names = ['Initial', 'Quarter', 'Half', 'Three Quarters', 'Final'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for idx, (frame, name) in enumerate(zip(key_frames, frame_names)): + if idx >= len(axes): + break + + ax = axes[idx] + ax.plot(x, u_exact[frame, :], 'b-', linewidth=2, label='Exact', alpha=0.7) + ax.plot(x, u_numerical[frame, :], 'r--', linewidth=2, label='Numerical') + ax.scatter(x[::10], u_numerical[frame, ::10], s=30, color='red', alpha=0.5) + + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('u') + ax.set_title(f'{name} (t={frame*dt:.2f}s)') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + # 第6个子图:误差随时间变化 + ax = axes[5] + errors = np.abs(u_numerical - u_exact) + mean_errors = np.mean(errors, axis=1) + max_errors = np.max(errors, axis=1) + + time = np.arange(nt) * dt + ax.plot(time, mean_errors, 'b-', label='Mean Error') + ax.plot(time, max_errors, 'r-', label='Max Error') + ax.fill_between(time, 0, mean_errors, alpha=0.3, color='blue') + ax.fill_between(time, 0, max_errors, alpha=0.1, color='red') + + ax.set_xlabel('Time') + ax.set_ylabel('Error') + ax.set_title('Numerical Error Evolution') + ax.grid(True, alpha=0.3) + ax.legend() + + plt.suptitle('1D Convection Equation: Key Frames and Error Analysis', + fontsize=16, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static_frames.png', dpi=300, bbox_inches='tight') + print("静态关键帧已保存为 '04_cfd_static_frames.png'") + plt.show() + +def create_simple_animation(): + """创建一个更简单的动画,避免复杂问题""" + setup_plot_style() + + # 简单示例:波的传播 + x = np.linspace(0, 10, 200) + t = np.linspace(0, 4*np.pi, 100) + + fig, ax = plt.subplots(figsize=(10, 6)) + + # 创建初始线 + line, = ax.plot([], [], 'b-', linewidth=2) + + # 设置图形 + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Amplitude u') + ax.set_title('Wave Propagation in 1D Domain') + ax.grid(True, alpha=0.3) + + # 初始化函数 + def init(): + line.set_data([], []) + return line, + + # 更新函数 + def update(i): + y = np.sin(2*np.pi*(x/5 - t[i]/5)) * np.exp(-0.05*x) + line.set_data(x, y) + ax.set_title(f'Wave Propagation (t = {t[i]:.2f})') + return line, + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=len(t), + init_func=init, blit=True, interval=50) + + # 保存为gif + try: + ani.save('04_simple_wave.gif', writer='pillow', fps=15) + print("简单动画已保存为 '04_simple_wave.gif'") + except Exception as e: + print(f"保存简单动画失败: {e}") + + plt.show() + return ani + +if __name__ == "__main__": + print("=" * 60) + print("1D CFD 动画演示程序") + print("=" * 60) + print("\n选项:") + print("1. 创建完整动画(可能有问题)") + print("2. 创建静态关键帧图像") + print("3. 创建简单波动动画") + + try: + choice = int(input("\n请选择 (1-3, 默认=2): ") or "2") + except: + choice = 2 + + if choice == 1: + print("\n正在创建完整动画...") + print("注意:保存视频需要ffmpeg或pillow库") + print("如果失败,将自动回退到静态图像") + + # 先尝试创建静态图像 + create_static_frames() + + # 然后尝试动画 + try: + ani = create_cfd_animation(save_video=True, show_plot=True) + except Exception as e: + print(f"\n动画创建失败: {e}") + print("已创建静态图像作为替代。") + + elif choice == 2: + print("\n正在创建静态关键帧图像...") + create_static_frames() + + elif choice == 3: + print("\n正在创建简单波动动画...") + create_simple_animation() + + else: + print("\n无效选择,创建静态关键帧图像...") + create_static_frames() + + print("\n程序执行完成!") \ No newline at end of file diff --git a/example/figure/1d/mesh/01/04_cfd_animationOld.py b/example/figure/1d/mesh/01/04_cfd_animationOld.py new file mode 100644 index 00000000..f3c274c9 --- /dev/null +++ b/example/figure/1d/mesh/01/04_cfd_animationOld.py @@ -0,0 +1,299 @@ +""" +文件名: 04_cfd_animation.py +功能: 创建CFD计算过程动画 +包含: 波动方程的数值解演化过程 +修复: 解决了动画保存后plt.show()的AttributeError问题 +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import warnings +warnings.filterwarnings('ignore') + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 12, + 'axes.titlesize': 14, + 'axes.labelsize': 12, + 'figure.titlesize': 16 + }) + +def simulate_convection_equation(): + """模拟对流方程的解""" + # 设置网格和参数 + L = 10.0 # 计算域长度 + nx = 100 # 网格点数 + dx = L / nx + x = np.linspace(0, L, nx) + + # 时间参数 + nt = 100 # 减少帧数以加快速度 + dt = 0.05 + + # 初始化变量 + u_exact = np.zeros((nt, nx)) + u_numerical = np.zeros((nt, nx)) + + # 初始条件:高斯波包 + u0 = np.exp(-(x - L/4)**2 / 0.5) * np.sin(3 * x) + + # 生成精确解(对流方程) + c = 0.5 # 对流速度 + for n in range(nt): + # 精确解:简单对流 + shift = c * n * dt + u_exact[n, :] = np.exp(-(x - shift - L/4)**2 / 0.5) * np.sin(3 * (x - shift)) + + # 数值解:使用一阶迎风格式 + if n == 0: + u_numerical[n, :] = u0 + else: + for i in range(1, nx): + # 一阶迎风格式 + u_numerical[n, i] = u_numerical[n-1, i] - c*dt/dx * ( + u_numerical[n-1, i] - u_numerical[n-1, i-1]) + # 边界条件:左侧固定为零 + u_numerical[n, 0] = 0.0 + + return x, u_exact, u_numerical, nt, dt + +def create_cfd_animation(save_video=True, show_plot=True): + """创建CFD计算过程动画""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 创建图形 + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + + # 初始化线条 + line_exact, = ax1.plot([], [], 'b-', linewidth=2, label='Exact Solution') + line_num, = ax2.plot([], [], 'r-', linewidth=2, label='Numerical Solution') + line_num_dots, = ax2.plot([], [], 'ro', markersize=4, alpha=0.5, markevery=5) + + # 设置坐标轴 + for ax in [ax1, ax2]: + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('Variable u') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + ax1.set_title('Exact Solution of Convection Equation') + ax2.set_title('Numerical Solution (First-order Upwind Scheme)') + + fig.suptitle('1D Convection Equation Simulation', fontsize=16, fontweight='bold', y=0.98) + + # 添加时间文本 + time_text = fig.text(0.5, 0.95, '', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) + + # 添加说明文本 + info_text = fig.text(0.5, 0.02, + 'Convection velocity: c = 0.5 | CFL number: ~0.5', + ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7)) + + # 初始化函数 + def init(): + line_exact.set_data([], []) + line_num.set_data([], []) + line_num_dots.set_data([], []) + time_text.set_text('') + return line_exact, line_num, line_num_dots, time_text + + # 动画更新函数 + def update(frame): + # 更新精确解 + line_exact.set_data(x, u_exact[frame, :]) + + # 更新数值解 + line_num.set_data(x, u_numerical[frame, :]) + # 每隔5个点显示一个标记点 + line_num_dots.set_data(x[::5], u_numerical[frame, ::5]) + + # 更新时间文本 + time_text.set_text(f'Time: {frame*dt:.2f} s | Frame: {frame}/{nt}') + + return line_exact, line_num, line_num_dots, time_text + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=nt, + init_func=init, blit=True, interval=100, repeat=False) + + # 保存动画(可选) + if save_video: + try: + print("正在保存动画...") + # 尝试使用不同的写入器 + try: + ani.save('04_cfd_animation.mp4', writer='ffmpeg', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.mp4'") + except: + # 如果ffmpeg不可用,尝试保存为gif + ani.save('04_cfd_animation.gif', writer='pillow', fps=10, dpi=150) + print("动画已保存为 '04_cfd_animation.gif' (使用pillow写入器)") + except Exception as e: + print(f"保存动画失败: {e}") + print("请确保已安装必要的视频编码器(如ffmpeg)或pillow库") + + # 显示动画(可选) + if show_plot: + try: + plt.tight_layout() + plt.show() + except Exception as e: + print(f"显示动画时出错: {e}") + print("这可能是因为图形窗口已关闭,但这是正常的。") + + return ani + +def create_static_frames(): + """创建静态的关键帧图像,作为替代方案""" + setup_plot_style() + + # 获取模拟数据 + x, u_exact, u_numerical, nt, dt = simulate_convection_equation() + + # 选择几个关键帧 + key_frames = [0, nt//4, nt//2, 3*nt//4, nt-1] + frame_names = ['Initial', 'Quarter', 'Half', 'Three Quarters', 'Final'] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for idx, (frame, name) in enumerate(zip(key_frames, frame_names)): + if idx >= len(axes): + break + + ax = axes[idx] + ax.plot(x, u_exact[frame, :], 'b-', linewidth=2, label='Exact', alpha=0.7) + ax.plot(x, u_numerical[frame, :], 'r--', linewidth=2, label='Numerical') + ax.scatter(x[::10], u_numerical[frame, ::10], s=30, color='red', alpha=0.5) + + ax.set_xlim(0, 10) + ax.set_ylim(-1.2, 1.2) + ax.set_xlabel('Position x') + ax.set_ylabel('u') + ax.set_title(f'{name} (t={frame*dt:.2f}s)') + ax.grid(True, alpha=0.3) + ax.legend(loc='upper right') + + # 第6个子图:误差随时间变化 + ax = axes[5] + errors = np.abs(u_numerical - u_exact) + mean_errors = np.mean(errors, axis=1) + max_errors = np.max(errors, axis=1) + + time = np.arange(nt) * dt + ax.plot(time, mean_errors, 'b-', label='Mean Error') + ax.plot(time, max_errors, 'r-', label='Max Error') + ax.fill_between(time, 0, mean_errors, alpha=0.3, color='blue') + ax.fill_between(time, 0, max_errors, alpha=0.1, color='red') + + ax.set_xlabel('Time') + ax.set_ylabel('Error') + ax.set_title('Numerical Error Evolution') + ax.grid(True, alpha=0.3) + ax.legend() + + plt.suptitle('1D Convection Equation: Key Frames and Error Analysis', + fontsize=16, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static_frames.png', dpi=300, bbox_inches='tight') + print("静态关键帧已保存为 '04_cfd_static_frames.png'") + plt.show() + +def create_simple_animation(): + """创建一个更简单的动画,避免复杂问题""" + setup_plot_style() + + # 简单示例:波的传播 + x = np.linspace(0, 10, 200) + t = np.linspace(0, 4*np.pi, 100) + + fig, ax = plt.subplots(figsize=(10, 6)) + + # 创建初始线 + line, = ax.plot([], [], 'b-', linewidth=2) + + # 设置图形 + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Amplitude u') + ax.set_title('Wave Propagation in 1D Domain') + ax.grid(True, alpha=0.3) + + # 初始化函数 + def init(): + line.set_data([], []) + return line, + + # 更新函数 + def update(i): + y = np.sin(2*np.pi*(x/5 - t[i]/5)) * np.exp(-0.05*x) + line.set_data(x, y) + ax.set_title(f'Wave Propagation (t = {t[i]:.2f})') + return line, + + # 创建动画 + ani = animation.FuncAnimation(fig, update, frames=len(t), + init_func=init, blit=True, interval=50) + + # 保存为gif + try: + ani.save('04_simple_wave.gif', writer='pillow', fps=15) + print("简单动画已保存为 '04_simple_wave.gif'") + except Exception as e: + print(f"保存简单动画失败: {e}") + + plt.show() + return ani + +if __name__ == "__main__": + print("=" * 60) + print("1D CFD 动画演示程序") + print("=" * 60) + print("\n选项:") + print("1. 创建完整动画(可能有问题)") + print("2. 创建静态关键帧图像") + print("3. 创建简单波动动画") + + try: + choice = int(input("\n请选择 (1-3, 默认=2): ") or "2") + except: + choice = 2 + + if choice == 1: + print("\n正在创建完整动画...") + print("注意:保存视频需要ffmpeg或pillow库") + print("如果失败,将自动回退到静态图像") + + # 先尝试创建静态图像 + create_static_frames() + + # 然后尝试动画 + try: + ani = create_cfd_animation(save_video=True, show_plot=True) + except Exception as e: + print(f"\n动画创建失败: {e}") + print("已创建静态图像作为替代。") + + elif choice == 2: + print("\n正在创建静态关键帧图像...") + create_static_frames() + + elif choice == 3: + print("\n正在创建简单波动动画...") + create_simple_animation() + + else: + print("\n无效选择,创建静态关键帧图像...") + create_static_frames() + + print("\n程序执行完成!") \ No newline at end of file diff --git a/example/figure/1d/mesh/01/05_interactive_cfd_plot.py b/example/figure/1d/mesh/01/05_interactive_cfd_plot.py new file mode 100644 index 00000000..6e034b89 --- /dev/null +++ b/example/figure/1d/mesh/01/05_interactive_cfd_plot.py @@ -0,0 +1,574 @@ +""" +文件名: 05_interactive_cfd_plot.py +功能: 创建交互式CFD可视化 +包含: 使用Plotly创建可交互的CFD图像 +注意: 需要安装plotly库: pip install plotly +""" + +import numpy as np +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import plotly.io as pio + +def create_interactive_cfd_plot(): + """创建交互式CFD可视化""" + + print("正在创建交互式CFD可视化...") + print("这将在浏览器中打开一个交互式图表。") + + # 创建数据 + x = np.linspace(0, 10, 100) + + # 精确解和数值解 + u_exact = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 添加不同数值格式的"数值解" + np.random.seed(42) + u_upwind = u_exact + 0.08*np.random.randn(len(x)) + u_central = u_exact + 0.05*np.sin(5*x)*0.3 # 模拟振荡 + u_quick = u_exact + 0.02*np.random.randn(len(x)) + + # 创建子图 + fig = make_subplots( + rows=2, cols=2, + subplot_titles=('Upwind Scheme (1st Order)', + 'Central Difference Scheme (2nd Order)', + 'QUICK Scheme (3rd Order)', + 'Grid Point Stencil Dependencies'), + vertical_spacing=0.12, + horizontal_spacing=0.1 + ) + + # 1. 迎风格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=True, + legendgroup="exact" + ), + row=1, col=1 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_upwind, + mode='lines', + name='Upwind Scheme', + line=dict(color='red', width=2, dash='dash'), + showlegend=True, + legendgroup="upwind" + ), + row=1, col=1 + ) + + # 2. 中心差分格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=False, + legendgroup="exact" + ), + row=1, col=2 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_central, + mode='lines', + name='Central Difference', + line=dict(color='blue', width=2, dash='dash'), + showlegend=True, + legendgroup="central" + ), + row=1, col=2 + ) + + # 3. QUICK格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=False, + legendgroup="exact" + ), + row=2, col=1 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_quick, + mode='lines', + name='QUICK Scheme', + line=dict(color='green', width=2, dash='dash'), + showlegend=True, + legendgroup="quick" + ), + row=2, col=1 + ) + + # 4. 网格点模板依赖关系 + # 创建网格点 + grid_points = np.array([0, 1, 2, 3, 4]) + point_names = ['u_{i-2}', 'u_{i-1}', 'u_i', 'u_{i+1}', 'u_{i+2}'] + + # 添加网格点 + fig.add_trace( + go.Scatter( + x=grid_points, + y=[0, 0, 0, 0, 0], + mode='markers+text', + name='Grid Points', + marker=dict(size=15, color='gray'), + text=point_names, + textposition="top center", + showlegend=False + ), + row=2, col=2 + ) + + # 添加箭头表示依赖关系 + # 迎风格式箭头(从i-1到i) + fig.add_annotation( + x=1, y=0, + ax=2, ay=0, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="red", + row=2, col=2 + ) + + # 中心差分箭头(从i-1和i+1到i) + fig.add_annotation( + x=1, y=-0.05, + ax=2, ay=-0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="blue", + row=2, col=2 + ) + + fig.add_annotation( + x=3, y=-0.05, + ax=2, ay=-0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="blue", + row=2, col=2 + ) + + # QUICK格式箭头(从i-2, i-1, i+1到i) + fig.add_annotation( + x=0, y=0.05, + ax=2, ay=0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="green", + row=2, col=2 + ) + + fig.add_annotation( + x=1, y=0.05, + ax=2, ay=0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="green", + row=2, col=2 + ) + + fig.add_annotation( + x=3, y=0.05, + ax=2, ay=0.05, + xref="x4", yref="y4", + axref="x4", ayref="y4", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + arrowcolor="green", + row=2, col=2 + ) + + # 添加文本标签 + fig.add_annotation( + x=1.5, y=-0.15, + text="Upwind", + showarrow=False, + font=dict(color="red", size=12), + row=2, col=2 + ) + + fig.add_annotation( + x=2, y=-0.25, + text="Central", + showarrow=False, + font=dict(color="blue", size=12), + row=2, col=2 + ) + + fig.add_annotation( + x=2, y=0.2, + text="QUICK", + showarrow=False, + font=dict(color="green", size=12), + row=2, col=2 + ) + + # 更新布局 + fig.update_layout( + title_text="Interactive CFD Visualization: Comparison of Convection Schemes", + title_font_size=20, + title_x=0.5, + height=900, + width=1200, + showlegend=True, + legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=1.02 + ) + ) + + # 更新轴标签 + fig.update_xaxes(title_text="Position x", row=1, col=1) + fig.update_yaxes(title_text="Velocity u", row=1, col=1) + + fig.update_xaxes(title_text="Position x", row=1, col=2) + fig.update_yaxes(title_text="Velocity u", row=1, col=2) + + fig.update_xaxes(title_text="Position x", row=2, col=1) + fig.update_yaxes(title_text="Velocity u", row=2, col=1) + + fig.update_xaxes(title_text="Grid Point Index", row=2, col=2, range=[-0.5, 4.5]) + fig.update_yaxes(title_text="", row=2, col=2, range=[-0.3, 0.3], showticklabels=False) + + # 保存为HTML文件 + html_filename = "05_interactive_cfd.html" + pio.write_html(fig, html_filename, auto_open=True) + + print(f"✓ 交互式图表已保存为 '{html_filename}'") + print("正在浏览器中打开...") + + return fig + +def create_interactive_grid_visualization(): + """创建交互式网格可视化""" + + print("\n创建交互式网格可视化...") + + # 创建网格数据 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 创建子图 + fig = make_subplots( + rows=1, cols=2, + subplot_titles=('Vertex-centered Storage', 'Cell-centered Storage'), + horizontal_spacing=0.15 + ) + + # 1. 顶点中心存储 + # 添加顶点 + fig.add_trace( + go.Scatter( + x=x_vertices, + y=[0]*len(x_vertices), + mode='markers+text', + name='Vertices', + marker=dict(size=15, color='red', symbol='circle'), + text=[f'u{i}' for i in range(len(x_vertices))], + textposition="top center", + showlegend=False + ), + row=1, col=1 + ) + + # 添加控制体矩形(使用形状) + for i in range(n_cells): + fig.add_shape( + type="rect", + x0=x_vertices[i], + y0=-0.1, + x1=x_vertices[i+1], + y1=0.1, + line=dict(color="blue", width=2), + fillcolor="rgba(0, 0, 255, 0.1)", + row=1, col=1 + ) + + # 添加控制体标签 + fig.add_annotation( + x=(x_vertices[i] + x_vertices[i+1])/2, + y=0.15, + text=f"Cell {i}", + showarrow=False, + font=dict(size=10), + row=1, col=1 + ) + + # 2. 单元中心存储 + # 添加单元中心 + fig.add_trace( + go.Scatter( + x=x_centers, + y=[0]*len(x_centers), + mode='markers+text', + name='Cell Centers', + marker=dict(size=15, color='green', symbol='circle'), + text=[f'u{i}' for i in range(len(x_centers))], + textposition="top center", + showlegend=False + ), + row=1, col=2 + ) + + # 添加控制体矩形 + for i in range(n_cells): + fig.add_shape( + type="rect", + x0=x_vertices[i], + y0=-0.1, + x1=x_vertices[i+1], + y1=0.1, + line=dict(color="orange", width=2), + fillcolor="rgba(255, 165, 0, 0.1)", + row=1, col=2 + ) + + # 添加控制体标签 + fig.add_annotation( + x=(x_vertices[i] + x_vertices[i+1])/2, + y=0.15, + text=f"Cell {i}", + showarrow=False, + font=dict(size=10), + row=1, col=2 + ) + + # 标记边界 + fig.add_annotation( + x=x_vertices[0], + y=0.25, + text="Boundary", + showarrow=True, + arrowhead=2, + font=dict(color="red", size=12), + row=1, col=1 + ) + + fig.add_annotation( + x=x_vertices[-1], + y=0.25, + text="Boundary", + showarrow=True, + arrowhead=2, + font=dict(color="red", size=12), + row=1, col=1 + ) + + # 更新布局 + fig.update_layout( + title_text="Interactive CFD Grid Visualization", + title_font_size=20, + title_x=0.5, + height=500, + width=1000, + showlegend=False + ) + + # 更新轴 + for col in [1, 2]: + fig.update_xaxes(title_text="Position x", row=1, col=col, range=[-0.5, n_cells*dx+0.5]) + fig.update_yaxes(title_text="", row=1, col=col, range=[-0.3, 0.3], showticklabels=False) + + # 保存为HTML + html_filename = "05_interactive_grid.html" + pio.write_html(fig, html_filename, auto_open=True) + + print(f"✓ 网格可视化已保存为 '{html_filename}'") + + return fig + +def create_interactive_boundary_conditions(): + """创建交互式边界条件可视化""" + + print("\n创建交互式边界条件可视化...") + + n_cells = 5 + dx = 1.0 + + # 创建图形 + fig = go.Figure() + + # 真实计算点 + x_real = np.linspace(0, n_cells*dx, n_cells + 1) + + # 虚拟点 + x_ghost_left = [-dx] + x_ghost_right = [n_cells*dx + dx] + + # 添加点 + fig.add_trace(go.Scatter( + x=x_real, + y=[0]*len(x_real), + mode='markers+text', + name='Real Points', + marker=dict(size=15, color='blue', symbol='circle'), + text=[f'u{i}' for i in range(len(x_real))], + textposition="top center" + )) + + fig.add_trace(go.Scatter( + x=x_ghost_left + x_ghost_right, + y=[0, 0], + mode='markers+text', + name='Ghost Cells', + marker=dict(size=15, color='red', symbol='square'), + text=['Ghost L', 'Ghost R'], + textposition="top center" + )) + + # 添加区域 + fig.add_vrect( + x0=-dx, x1=0, + fillcolor="red", opacity=0.1, + line_width=0, + annotation_text="Ghost Cell Region", + annotation_position="top left" + ) + + fig.add_vrect( + x0=n_cells*dx, x1=n_cells*dx+dx, + fillcolor="red", opacity=0.1, + line_width=0, + annotation_text="Ghost Cell Region" + ) + + fig.add_vrect( + x0=0, x1=n_cells*dx, + fillcolor="green", opacity=0.1, + line_width=0, + annotation_text="Computational Domain", + annotation_position="top" + ) + + # 添加边界线 + fig.add_vline(x=0, line_width=3, line_dash="dash", line_color="red") + fig.add_vline(x=n_cells*dx, line_width=3, line_dash="dash", line_color="red") + + # 添加边界条件说明 + fig.add_annotation( + x=-dx/2, y=0.2, + text="Dirichlet BC:
u = u_boundary", + showarrow=False, + font=dict(size=11), + align="center" + ) + + fig.add_annotation( + x=n_cells*dx + dx/2, y=0.2, + text="Neumann BC:
∂u/∂x = 0", + showarrow=False, + font=dict(size=11), + align="center" + ) + + # 更新布局 + fig.update_layout( + title="Boundary Conditions and Ghost Cells", + xaxis_title="Position x", + yaxis_title="", + height=500, + width=800, + showlegend=True, + yaxis=dict(showticklabels=False, range=[-0.3, 0.4]) + ) + + # 保存为HTML + html_filename = "05_interactive_boundary.html" + pio.write_html(fig, html_filename, auto_open=True) + + print(f"✓ 边界条件可视化已保存为 '{html_filename}'") + + return fig + +def main(): + """主函数""" + print("=" * 60) + print("Interactive CFD Visualization with Plotly") + print("=" * 60) + print("\n选项:") + print("1. 对流格式比较") + print("2. 网格存储方式") + print("3. 边界条件处理") + print("4. 全部创建") + + try: + choice = input("\n请选择 (1-4, 默认=1): ").strip() + if choice == "": + choice = "1" + choice = int(choice) + except: + choice = 1 + + if choice == 1: + fig = create_interactive_cfd_plot() + elif choice == 2: + fig = create_interactive_grid_visualization() + elif choice == 3: + fig = create_interactive_boundary_conditions() + elif choice == 4: + print("\n创建所有交互式可视化...") + fig1 = create_interactive_cfd_plot() + fig2 = create_interactive_grid_visualization() + fig3 = create_interactive_boundary_conditions() + print("\n✓ 所有可视化已创建完成!") + else: + fig = create_interactive_cfd_plot() + + print("\n" + "=" * 60) + print("说明:") + print("- 交互式图表已保存为HTML文件") + print("- 可以在任何浏览器中打开查看") + print("- 支持缩放、平移、悬停查看数据点") + print("=" * 60) + +if __name__ == "__main__": + # 检查是否安装了plotly + try: + import plotly + main() + except ImportError: + print("错误: 需要安装plotly库") + print("请运行: pip install plotly") + print("或: pip install plotly numpy") \ No newline at end of file diff --git a/example/figure/1d/mesh/01/testprj.py b/example/figure/1d/mesh/01/testprj.py new file mode 100644 index 00000000..43af9550 --- /dev/null +++ b/example/figure/1d/mesh/01/testprj.py @@ -0,0 +1,140 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +import matplotlib.gridspec as gridspec + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + fig = plt.figure(figsize=(14, 8)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("顶点中心存储 (Vertex-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=100, color='red', zorder=5, label='变量存储点') + + # 标记顶点 + for i, x in enumerate(x_vertices): + ax1.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="yellow", alpha=0.7)) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + ax1.add_patch(Rectangle((x_vertices[i], -0.05), dx, 0.1, + alpha=0.2, color='blue', label='控制体' if i==0 else None)) + ax1.text(center, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.3, 0.3) + ax1.set_xlabel('x') + ax1.legend(loc='upper right') + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("单元中心存储 (Cell-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=100, color='green', zorder=5, label='变量存储点') + + # 标记变量 + for i, x in enumerate(x_centers): + ax2.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7)) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + ax2.add_patch(Rectangle((x, -0.05), dx, 0.1, + alpha=0.2, color='orange', label='控制体' if i==0 else None)) + ax2.text(x + dx/2, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.3, 0.3) + ax2.set_xlabel('x') + ax2.legend(loc='upper right') + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("边界条件与虚拟点", fontsize=12, fontweight='bold') + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=50, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=100, color='blue', label='计算点') + + # 标记区域 + ax3.axvline(0, color='red', linestyle='--', linewidth=2, label='左边界') + ax3.axvline(n_cells*dx, color='red', linestyle='--', linewidth=2, label='右边界') + ax3.axvspan(-dx, 0, alpha=0.1, color='red', label='虚拟点区域') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.1, color='red') + + # 标记点类型 + ax3.text(-dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + ax3.text(n_cells*dx + dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + + ax3.set_xlim(-1.5*dx, (n_cells+1.5)*dx) + ax3.set_ylim(-0.2, 0.3) + ax3.set_xlabel('x') + ax3.legend(loc='upper right') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("有限差分格式示意", fontsize=12, fontweight='bold') + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=2, label='真实解') + ax4.scatter(x[i], u[i], s=150, color='red', zorder=5, label='计算点 $u_i$') + ax4.scatter(x[i-1], u[i-1], s=100, color='green', zorder=4, label='$u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=100, color='orange', zorder=4, label='$u_{i+1}$') + + # 绘制差分示意 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5) + ax4.annotate('中心差分', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.3), + arrowprops=dict(arrowstyle="->", color='black'), + ha='center', fontsize=10) + + ax4.annotate('迎风格式\n使用上游点', xy=(x[i], u[i]), xytext=(x[i]-2, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red'), + ha='center', fontsize=9) + + ax4.set_xlabel('x') + ax4.set_ylabel('u') + ax4.legend(loc='upper right') + ax4.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('cfd_grid_illustration.png', dpi=300, bbox_inches='tight') + plt.show() + +# 运行绘图 +plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/00_testanimation.py b/example/figure/1d/mesh/01a/00_testanimation.py new file mode 100644 index 00000000..53ae992e --- /dev/null +++ b/example/figure/1d/mesh/01a/00_testanimation.py @@ -0,0 +1,28 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +# 创建图形和轴 +fig, ax = plt.subplots() +ax.set_xlim(0, 2*np.pi) +ax.set_ylim(-1, 1) + +# 初始化数据 +x = np.linspace(0, 2*np.pi, 1000) +line, = ax.plot(x, np.sin(x), color='blue') + +# 动画更新函数 +def animate(frame): + # 随着帧数增加,x 数据偏移,实现波形移动 + y = np.sin(x + frame / 10.0) + line.set_ydata(y) + return line, + +# 创建动画:100 帧,每帧间隔 20ms,支持 blit 优化(更快渲染) +ani = animation.FuncAnimation(fig, animate, frames=100, interval=20, blit=True) + +# 显示动画(在 Jupyter 中可用 plt.show(),否则保存为 GIF) +plt.show() + +# 可选:保存为 GIF 文件(需要 pillow 或 imagemagick) +# ani.save('sine_wave.gif', writer='pillow', fps=30) \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/01_cfd_grid_storage.py b/example/figure/1d/mesh/01a/01_cfd_grid_storage.py new file mode 100644 index 00000000..9f686ecc --- /dev/null +++ b/example/figure/1d/mesh/01a/01_cfd_grid_storage.py @@ -0,0 +1,214 @@ +""" +文件名: 01_cfd_grid_storage.py +功能: 绘制一维CFD基础网格与变量存储示意图 +包含: 顶点中心存储、单元中心存储、边界条件处理、有限差分格式示意 +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14, + 'figure.dpi': 100 + }) + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + setup_plot_style() + fig = plt.figure(figsize=(15, 10)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("Vertex-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=120, color='red', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记顶点 + for i, x in enumerate(x_vertices): + if i == 0: + label = f'Boundary\n$u_0$' + color = "orange" + elif i == len(x_vertices) - 1: + label = f'Boundary\n$u_{i}$' + color = "orange" + else: + label = f'Storage\n$u_{i}$' + color = "yellow" + + ax1.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor=color, alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + rect = ax1.add_patch(Rectangle((x_vertices[i], -0.08), dx, 0.16, + alpha=0.2, color='blue', + edgecolor='blue', linewidth=1.5)) + ax1.text(center, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue", alpha=0.8)) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.35, 0.35) + ax1.set_xlabel('Position x') + ax1.set_ylabel('Variable Storage') + ax1.text(0.5, 0.95, 'Variables stored at vertices', transform=ax1.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("Cell-centered Storage", fontsize=14, fontweight='bold', pad=20) + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=120, color='green', zorder=5, edgecolors='black', linewidth=1.5) + + # 标记变量 + for i, x in enumerate(x_centers): + label = f'Storage\n$u_{i}$' + ax2.text(x, -0.2, label, ha='center', fontsize=10, va='top', + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8, edgecolor='black')) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + rect = ax2.add_patch(Rectangle((x, -0.08), dx, 0.16, + alpha=0.2, color='orange', + edgecolor='orange', linewidth=1.5)) + ax2.text(x + dx/2, 0.15, f'Cell {i}', ha='center', fontsize=11, + bbox=dict(boxstyle="round,pad=0.3", facecolor="peachpuff", alpha=0.8)) + + # 标记边界 + ax2.axvline(x_vertices[0], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.axvline(x_vertices[-1], color='red', linestyle='--', linewidth=2, alpha=0.7) + ax2.text(x_vertices[0], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + ax2.text(x_vertices[-1], 0.25, 'Boundary', ha='center', color='red', fontsize=11, fontweight='bold') + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.35, 0.35) + ax2.set_xlabel('Position x') + ax2.set_ylabel('Variable Storage') + ax2.text(0.5, 0.95, 'Variables stored at cell centers', transform=ax2.transAxes, + ha='center', fontsize=11, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow")) + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("Boundary Conditions and Ghost Cells", fontsize=14, fontweight='bold', pad=20) + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=80, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=120, color='blue', + edgecolors='black', linewidth=1.5, zorder=5) + + # 标记边界 + ax3.axvline(0, color='red', linestyle='-', linewidth=3, alpha=0.8) + ax3.axvline(n_cells*dx, color='red', linestyle='-', linewidth=3, alpha=0.8) + + # 填充虚拟点区域 + ax3.axvspan(-dx, 0, alpha=0.15, color='red', hatch='//') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.15, color='red', hatch='//') + + # 标记点类型 + ax3.text(-dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + ax3.text(n_cells*dx + dx/2, 0.15, 'Ghost Cell', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.4", facecolor="pink", alpha=0.9, edgecolor='red')) + + # 标记计算区域 + ax3.axvspan(0, n_cells*dx, alpha=0.1, color='green') + ax3.text(n_cells*dx/2, -0.2, 'Computational Domain', ha='center', fontsize=12, + bbox=dict(boxstyle="round,pad=0.4", facecolor="lightgreen", alpha=0.8)) + + # 添加箭头示意边界条件 + ax3.annotate('BC: u=0', xy=(0, 0), xytext=(-1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.annotate('BC: ∂u/∂x=0', xy=(n_cells*dx, 0), + xytext=(n_cells*dx + 1.2*dx, 0.25), + arrowprops=dict(arrowstyle="->", color='darkred', lw=2), + ha='center', fontsize=10, color='darkred', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax3.set_xlim(-1.8*dx, (n_cells+1.8)*dx) + ax3.set_ylim(-0.3, 0.4) + ax3.set_xlabel('Position x') + ax3.set_ylabel('Domain') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("Finite Difference Schemes", fontsize=14, fontweight='bold', pad=20) + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=3, alpha=0.5, label='Exact Solution') + ax4.scatter(x[i], u[i], s=200, color='red', zorder=5, + edgecolors='black', linewidth=1.5, label='Current point $u_i$') + ax4.scatter(x[i-1], u[i-1], s=150, color='green', zorder=4, + edgecolors='black', linewidth=1, label='Upstream $u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=150, color='orange', zorder=4, + edgecolors='black', linewidth=1, label='Downstream $u_{i+1}$') + + # 绘制差分示意线 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5, linewidth=1.5) + + # 标注差分格式 + ax4.annotate('Central Difference\n(2nd order)', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.4), + arrowprops=dict(arrowstyle="->", color='blue', lw=2), + ha='center', fontsize=11, color='blue', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.annotate('Upwind Scheme\n(1st order)', xy=(x[i], u[i]), xytext=(x[i]-2.5, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red', lw=2), + ha='center', fontsize=11, color='red', + bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9)) + + ax4.set_xlabel('Position x') + ax4.set_ylabel('Variable u') + ax4.legend(loc='upper right', fontsize=10) + ax4.grid(True, alpha=0.3) + + # 添加整体标题 + plt.suptitle('1D CFD Grid and Variable Storage Illustration', fontsize=16, fontweight='bold', y=1.02) + + plt.tight_layout() + plt.savefig('01_cfd_grid_storage.png', dpi=300, bbox_inches='tight', facecolor='white') + plt.show() + +if __name__ == "__main__": + plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/02_convection_schemes.py b/example/figure/1d/mesh/01a/02_convection_schemes.py new file mode 100644 index 00000000..a2b2c413 --- /dev/null +++ b/example/figure/1d/mesh/01a/02_convection_schemes.py @@ -0,0 +1,126 @@ +""" +文件名: 02_convection_schemes.py +功能: 绘制一维对流方程不同数值格式对比 +包含: 迎风格式、中心差分格式、QUICK格式的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_convection_schemes(): + """绘制不同对流格式示意图""" + setup_plot_style() + + # 创建数据 + x = np.linspace(0, 2*np.pi, 100) + u_exact = np.sin(x) + + # 添加数值噪声模拟数值解 + np.random.seed(42) + u_upwind = u_exact + 0.1*np.random.randn(len(x)) # 迎风格式(有耗散) + u_central = u_exact + 0.05*np.random.randn(len(x)) # 中心差分(有振荡) + u_quick = u_exact + 0.02*np.random.randn(len(x)) # QUICK格式 + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 迎风格式 + ax = axes[0, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_upwind, 'r--', linewidth=2, marker='o', markersize=4, + markevery=5, label='Upwind Scheme') + ax.fill_between(x, u_exact-0.15, u_exact+0.15, alpha=0.1, color='gray') + ax.set_title('Upwind Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nDissipation', xy=(3, 0), xytext=(4, -0.8), + arrowprops=dict(arrowstyle="->", color='red'), + fontsize=10, color='red') + + # 2. 中心差分格式 + ax = axes[0, 1] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_central, 'b--', linewidth=2, marker='s', markersize=4, + markevery=5, label='Central Difference') + ax.set_title('Central Difference Scheme', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + ax.annotate('Numerical\nOscillation', xy=(1.5, 0.5), xytext=(0.5, 0.8), + arrowprops=dict(arrowstyle="->", color='blue'), + fontsize=10, color='blue') + + # 3. QUICK格式 + ax = axes[1, 0] + ax.plot(x, u_exact, 'k-', linewidth=3, alpha=0.7, label='Exact Solution') + ax.plot(x, u_quick, 'g--', linewidth=2, marker='^', markersize=4, + markevery=5, label='QUICK Scheme') + ax.set_title('QUICK Scheme (3rd Order)', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 格式示意 + ax = axes[1, 1] + ax.set_title('Grid Point Dependencies', fontsize=12, fontweight='bold') + + # 绘制网格点 + points_x = np.array([0, 1, 2, 3, 4]) + points_y = np.zeros_like(points_x) + + ax.scatter(points_x, points_y, s=200, color='gray') + + # 标记点 + labels = ['$u_{i-2}$', '$u_{i-1}$', '$u_i$', '$u_{i+1}$', '$u_{i+2}$'] + for i, (x_pos, label) in enumerate(zip(points_x, labels)): + ax.text(x_pos, 0.1, label, ha='center', fontsize=12, fontweight='bold') + + # 迎风格式依赖 + ax.annotate('', xy=(2, 0), xytext=(1, 0), + arrowprops=dict(arrowstyle='<-', color='red', lw=2)) + ax.text(1.5, -0.15, 'Upwind', ha='center', color='red', fontweight='bold') + + # 中心差分依赖 + ax.annotate('', xy=(2, 0), xytext=(1, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.annotate('', xy=(2, 0), xytext=(3, -0.05), + arrowprops=dict(arrowstyle='<-', color='blue', lw=2)) + ax.text(2, -0.2, 'Central', ha='center', color='blue', fontweight='bold') + + # QUICK格式依赖 + ax.annotate('', xy=(2, 0), xytext=(0, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(1, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.annotate('', xy=(2, 0), xytext=(3, 0.05), + arrowprops=dict(arrowstyle='<-', color='green', lw=2, alpha=0.7)) + ax.text(2, 0.2, 'QUICK', ha='center', color='green', fontweight='bold') + + ax.set_xlim(-0.5, 4.5) + ax.set_ylim(-0.3, 0.3) + ax.set_xlabel('Grid Point Index') + ax.grid(True, alpha=0.3) + ax.set_yticks([]) + + plt.suptitle('Comparison of Convection Schemes for 1D CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('02_convection_schemes.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_convection_schemes() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/03_interpolation_methods.py b/example/figure/1d/mesh/01a/03_interpolation_methods.py new file mode 100644 index 00000000..44eaf808 --- /dev/null +++ b/example/figure/1d/mesh/01a/03_interpolation_methods.py @@ -0,0 +1,119 @@ +""" +文件名: 03_interpolation_methods.py +功能: 绘制不同插值方法对比 +包含: 线性插值、二次插值、三次样条插值的比较 +""" + +import numpy as np +import matplotlib.pyplot as plt +from scipy import interpolate + +def setup_plot_style(): + """设置绘图样式""" + plt.rcParams.update({ + 'font.size': 11, + 'axes.titlesize': 12, + 'axes.labelsize': 11, + 'xtick.labelsize': 10, + 'ytick.labelsize': 10, + 'legend.fontsize': 9, + 'figure.titlesize': 14 + }) + +def plot_interpolation_methods(): + """绘制不同插值方法对比""" + setup_plot_style() + + # 创建粗网格和细网格 + x_coarse = np.linspace(0, 10, 6) + u_coarse = np.sin(x_coarse * 0.8) + + x_fine = np.linspace(0, 10, 100) + u_exact = np.sin(x_fine * 0.8) + + # 不同插值方法 + # 线性插值 + f_linear = interpolate.interp1d(x_coarse, u_coarse, kind='linear') + u_linear = f_linear(x_fine) + + # 二次插值 + f_quadratic = interpolate.interp1d(x_coarse, u_coarse, kind='quadratic') + u_quadratic = f_quadratic(x_fine) + + # 三次样条插值 + f_cubic = interpolate.CubicSpline(x_coarse, u_coarse) + u_cubic = f_cubic(x_fine) + + fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + + # 1. 线性插值 + ax = axes[0, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_linear, 'r--', linewidth=2, label='Linear Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Linear Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 2. 二次插值 + ax = axes[0, 1] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_quadratic, 'g--', linewidth=2, label='Quadratic Interpolation') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Quadratic Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 3. 三次样条插值 + ax = axes[1, 0] + ax.plot(x_fine, u_exact, 'k-', alpha=0.3, linewidth=3, label='Exact Solution') + ax.plot(x_fine, u_cubic, 'b--', linewidth=2, label='Cubic Spline') + ax.scatter(x_coarse, u_coarse, s=100, color='blue', zorder=5, label='Known Points') + ax.set_title('Cubic Spline Interpolation', fontsize=12, fontweight='bold') + ax.set_xlabel('x') + ax.set_ylabel('u') + ax.legend() + ax.grid(True, alpha=0.3) + + # 4. 误差比较 + ax = axes[1, 1] + errors = { + 'Linear': np.abs(u_linear - u_exact), + 'Quadratic': np.abs(u_quadratic - u_exact), + 'Cubic Spline': np.abs(u_cubic - u_exact) + } + + x_pos = np.arange(len(errors)) + mean_errors = [np.mean(err) for err in errors.values()] + max_errors = [np.max(err) for err in errors.values()] + + width = 0.35 + ax.bar(x_pos - width/2, mean_errors, width, label='Mean Error', color='skyblue') + ax.bar(x_pos + width/2, max_errors, width, label='Max Error', color='salmon') + + ax.set_xlabel('Interpolation Method') + ax.set_ylabel('Error') + ax.set_title('Interpolation Error Comparison', fontsize=12, fontweight='bold') + ax.set_xticks(x_pos) + ax.set_xticklabels(list(errors.keys())) + ax.legend() + ax.grid(True, alpha=0.3, axis='y') + + # 添加数值标签 + for i, (mean_err, max_err) in enumerate(zip(mean_errors, max_errors)): + ax.text(i - width/2, mean_err + 0.001, f'{mean_err:.3f}', + ha='center', va='bottom', fontsize=9) + ax.text(i + width/2, max_err + 0.001, f'{max_err:.3f}', + ha='center', va='bottom', fontsize=9) + + plt.suptitle('Comparison of Interpolation Methods for CFD', fontsize=14, fontweight='bold', y=1.02) + plt.tight_layout() + plt.savefig('03_interpolation_methods.png', dpi=300, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + plot_interpolation_methods() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/04_cfd_animation.py b/example/figure/1d/mesh/01a/04_cfd_animation.py new file mode 100644 index 00000000..7315bdbc --- /dev/null +++ b/example/figure/1d/mesh/01a/04_cfd_animation.py @@ -0,0 +1,178 @@ +""" +文件名: 04_cfd_animation_fixed.py +功能: 完全按照简单模式修复的CFD动画 +参考: 你的成功示例代码 +""" + +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +def create_simple_animation(): + """创建简单的CFD动画(只显示)""" + print("正在创建CFD动画...") + + # 创建图形和轴 + fig, ax = plt.subplots(figsize=(10, 6)) + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Velocity u') + ax.grid(True, alpha=0.3) + + # 初始化数据 + x = np.linspace(0, 10, 200) + line_exact, = ax.plot(x, np.zeros_like(x), 'b-', linewidth=2, label='Exact Solution') + line_num, = ax.plot(x, np.zeros_like(x), 'r--', linewidth=2, label='Numerical Solution') + ax.legend() + + # 添加标题 + ax.set_title('1D Convection Equation Simulation') + + # 动画更新函数 + def animate(frame): + t = frame * 0.05 # 时间步长 + + # 精确解:波动传播 + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) * np.exp(-0.1*x) + + # 数值解:添加一些数值误差(模拟迎风格式的耗散) + u_num = u_exact * (1 - 0.05*t) + 0.1*np.sin(5*x)*np.sin(t) + + line_exact.set_ydata(u_exact) + line_num.set_ydata(u_num) + ax.set_title(f'1D Convection Equation - Time: {t:.2f}s') + + return line_exact, line_num, + + # 创建动画:100帧,每帧间隔50ms + ani = animation.FuncAnimation(fig, animate, frames=100, interval=50, blit=True) + + print("动画准备就绪,显示窗口...") + print("提示:关闭窗口以退出程序。") + + plt.tight_layout() + plt.show() + + return ani + +def create_and_save_animation(): + """创建并保存动画(先保存再显示分开处理)""" + print("正在创建并保存CFD动画...") + + # ========== 第一部分:只保存动画 ========== + print("\n1. 保存动画到文件...") + + fig1, ax1 = plt.subplots(figsize=(10, 6)) + ax1.set_xlim(0, 10) + ax1.set_ylim(-1.5, 1.5) + + x = np.linspace(0, 10, 200) + line1, = ax1.plot(x, np.zeros_like(x), 'b-', lw=2) + line2, = ax1.plot(x, np.zeros_like(x), 'r--', lw=2) + + def update_save(frame): + t = frame * 0.05 + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) + u_num = u_exact * (1 - 0.05*t) + + line1.set_ydata(u_exact) + line2.set_ydata(u_num) + ax1.set_title(f'Time: {t:.2f}s') + + return line1, line2, + + ani_save = animation.FuncAnimation(fig1, update_save, frames=50, blit=True) + + # 保存动画 + try: + ani_save.save('04_cfd_save_only.gif', writer='pillow', fps=15, dpi=100) + print("✓ 动画已保存为 '04_cfd_save_only.gif'") + except Exception as e: + print(f"保存失败: {e}") + + # 重要:关闭保存用的图形 + plt.close(fig1) + + # ========== 第二部分:只显示动画 ========== + print("\n2. 显示动画...") + create_simple_animation() + +def create_static_images(): + """创建静态图像""" + print("正在创建静态图像...") + + x = np.linspace(0, 10, 200) + times = [0, 0.5, 1.0, 1.5, 2.0] + + fig, axes = plt.subplots(2, 3, figsize=(15, 8)) + axes = axes.flatten() + + for i, (ax, t) in enumerate(zip(axes[:5], times)): + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) + u_num = u_exact * (1 - 0.05*t) + + ax.plot(x, u_exact, 'b-', lw=2, label='Exact') + ax.plot(x, u_num, 'r--', lw=2, label='Numerical') + ax.set_xlim(0, 10) + ax.set_ylim(-1.5, 1.5) + ax.set_xlabel('Position x') + ax.set_ylabel('Velocity u') + ax.set_title(f'Time = {t:.1f}s') + ax.legend() + ax.grid(True, alpha=0.3) + + # 最后一个子图显示误差 + ax = axes[5] + t_vals = np.linspace(0, 2, 50) + errors = [] + + for t in t_vals: + u_exact = np.sin(2 * np.pi * (x/5 - t/2)) + u_num = u_exact * (1 - 0.05*t) + error = np.max(np.abs(u_exact - u_num)) + errors.append(error) + + ax.plot(t_vals, errors, 'k-', lw=2) + ax.fill_between(t_vals, 0, errors, alpha=0.3) + ax.set_xlabel('Time (s)') + ax.set_ylabel('Max Error') + ax.set_title('Numerical Error over Time') + ax.grid(True, alpha=0.3) + + plt.suptitle('1D Convection Equation - Static Frames', fontsize=16, y=1.02) + plt.tight_layout() + plt.savefig('04_cfd_static.png', dpi=300, bbox_inches='tight') + plt.show() + + print("✓ 静态图像已保存为 '04_cfd_static.png'") + +def main_menu(): + """主菜单""" + print("=" * 60) + print("CFD 动画演示程序") + print("=" * 60) + print("\n选项:") + print("1. 显示动画(简单,不会出错)") + print("2. 保存并显示动画(分开处理)") + print("3. 创建静态图像") + + try: + choice = input("\n请选择 (1-3, 默认=1): ").strip() + if choice == "": + choice = "1" + choice = int(choice) + except: + choice = 1 + + if choice == 1: + create_simple_animation() + elif choice == 2: + create_and_save_animation() + elif choice == 3: + create_static_images() + else: + create_simple_animation() + +if __name__ == "__main__": + main_menu() \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/05_interactive_cfd_plot.py b/example/figure/1d/mesh/01a/05_interactive_cfd_plot.py new file mode 100644 index 00000000..eadffe9b --- /dev/null +++ b/example/figure/1d/mesh/01a/05_interactive_cfd_plot.py @@ -0,0 +1,83 @@ +""" +文件名: 05_interactive_cfd_plot.py +功能: 创建交互式CFD可视化 +包含: 使用Plotly创建可交互的CFD图像 +注意: 需要安装plotly库: pip install plotly +""" + +import numpy as np +import plotly.graph_objects as go +from plotly.subplots import make_subplots + +def create_interactive_plot(): + """创建交互式CFD可视化""" + + # 创建数据 + x = np.linspace(0, 10, 100) + + # 精确解和数值解 + u_exact = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 添加不同数值格式的"数值解" + np.random.seed(42) + u_upwind = u_exact + 0.08*np.random.randn(len(x)) + u_central = u_exact + 0.05*np.sin(5*x)*0.3 # 模拟振荡 + u_quick = u_exact + 0.02*np.random.randn(len(x)) + + # 创建子图 + fig = make_subplots( + rows=2, cols=2, + subplot_titles=('Upwind Scheme', + 'Central Difference Scheme', + 'QUICK Scheme', + 'Grid Point Stencil'), + vertical_spacing=0.12, + horizontal_spacing=0.1 + ) + + # 1. 迎风格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=True + ), + row=1, col=1 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_upwind, + mode='lines+markers', + name='Upwind', + line=dict(color='red', width=2, dash='dash'), + marker=dict(size=4, symbol='circle'), + showlegend=True + ), + row=1, col=1 + ) + + # 2. 中心差分格式 + fig.add_trace( + go.Scatter( + x=x, y=u_exact, + mode='lines', + name='Exact Solution', + line=dict(color='black', width=3, dash='solid'), + showlegend=False + ), + row=1, col=2 + ) + + fig.add_trace( + go.Scatter( + x=x, y=u_central, + mode='lines+markers', + name='Central', + line=dict(color='blue', width=2, dash='dash'), + marker=dict(size=4, symbol='square'), + showlegend=True + ), + row=1 \ No newline at end of file diff --git a/example/figure/1d/mesh/01a/testprj.py b/example/figure/1d/mesh/01a/testprj.py new file mode 100644 index 00000000..43af9550 --- /dev/null +++ b/example/figure/1d/mesh/01a/testprj.py @@ -0,0 +1,140 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +import matplotlib.gridspec as gridspec + +def plot_cfd_grid_storage(): + """绘制一维CFD网格和变量存储方式对比""" + fig = plt.figure(figsize=(14, 8)) + + # 创建网格 + n_cells = 5 + dx = 1.0 + x_vertices = np.linspace(0, n_cells*dx, n_cells + 1) + x_centers = (x_vertices[:-1] + x_vertices[1:]) / 2 + + # 1. 顶点中心存储 + ax1 = plt.subplot(2, 2, 1) + ax1.set_title("顶点中心存储 (Vertex-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax1.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制顶点 + ax1.scatter(x_vertices, np.zeros_like(x_vertices), + s=100, color='red', zorder=5, label='变量存储点') + + # 标记顶点 + for i, x in enumerate(x_vertices): + ax1.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="yellow", alpha=0.7)) + + # 绘制控制体 + for i in range(len(x_vertices)-1): + center = (x_vertices[i] + x_vertices[i+1]) / 2 + ax1.add_patch(Rectangle((x_vertices[i], -0.05), dx, 0.1, + alpha=0.2, color='blue', label='控制体' if i==0 else None)) + ax1.text(center, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax1.set_xlim(-0.5, n_cells*dx + 0.5) + ax1.set_ylim(-0.3, 0.3) + ax1.set_xlabel('x') + ax1.legend(loc='upper right') + ax1.grid(True, alpha=0.3) + + # 2. 单元中心存储 + ax2 = plt.subplot(2, 2, 2) + ax2.set_title("单元中心存储 (Cell-centered)", fontsize=12, fontweight='bold') + + # 绘制网格线 + for x in x_vertices: + ax2.axvline(x, color='gray', linestyle='-', alpha=0.5, linewidth=0.8) + + # 绘制单元中心 + ax2.scatter(x_centers, np.zeros_like(x_centers), + s=100, color='green', zorder=5, label='变量存储点') + + # 标记变量 + for i, x in enumerate(x_centers): + ax2.text(x, -0.15, f'$u_{i}$', ha='center', fontsize=10, + bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7)) + + # 绘制控制体 + for i, x in enumerate(x_vertices[:-1]): + ax2.add_patch(Rectangle((x, -0.05), dx, 0.1, + alpha=0.2, color='orange', label='控制体' if i==0 else None)) + ax2.text(x + dx/2, 0.1, f'Cell {i}', ha='center', fontsize=9) + + ax2.set_xlim(-0.5, n_cells*dx + 0.5) + ax2.set_ylim(-0.3, 0.3) + ax2.set_xlabel('x') + ax2.legend(loc='upper right') + ax2.grid(True, alpha=0.3) + + # 3. 边界条件处理示意 + ax3 = plt.subplot(2, 2, 3) + ax3.set_title("边界条件与虚拟点", fontsize=12, fontweight='bold') + + # 扩展网格显示虚拟点 + x_extended = np.linspace(-dx, (n_cells+1)*dx, n_cells + 4) + x_real = x_extended[1:-2] # 真实计算区域 + + # 绘制所有点 + ax3.scatter(x_extended, np.zeros_like(x_extended), s=50, color='gray', alpha=0.5) + ax3.scatter(x_real, np.zeros_like(x_real), s=100, color='blue', label='计算点') + + # 标记区域 + ax3.axvline(0, color='red', linestyle='--', linewidth=2, label='左边界') + ax3.axvline(n_cells*dx, color='red', linestyle='--', linewidth=2, label='右边界') + ax3.axvspan(-dx, 0, alpha=0.1, color='red', label='虚拟点区域') + ax3.axvspan(n_cells*dx, (n_cells+1)*dx, alpha=0.1, color='red') + + # 标记点类型 + ax3.text(-dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + ax3.text(n_cells*dx + dx/2, 0.1, '虚拟点', ha='center', fontsize=9, + bbox=dict(boxstyle="round,pad=0.3", facecolor="pink", alpha=0.7)) + + ax3.set_xlim(-1.5*dx, (n_cells+1.5)*dx) + ax3.set_ylim(-0.2, 0.3) + ax3.set_xlabel('x') + ax3.legend(loc='upper right') + ax3.grid(True, alpha=0.3) + + # 4. 数值格式示意 + ax4 = plt.subplot(2, 2, 4) + ax4.set_title("有限差分格式示意", fontsize=12, fontweight='bold') + + # 创建示例数据 + x = np.linspace(0, 10, 50) + u = np.sin(x * 0.8) * np.exp(-0.1*x) + + # 选取几个点 + i = 25 + ax4.plot(x, u, 'b-', linewidth=2, label='真实解') + ax4.scatter(x[i], u[i], s=150, color='red', zorder=5, label='计算点 $u_i$') + ax4.scatter(x[i-1], u[i-1], s=100, color='green', zorder=4, label='$u_{i-1}$') + ax4.scatter(x[i+1], u[i+1], s=100, color='orange', zorder=4, label='$u_{i+1}$') + + # 绘制差分示意 + ax4.plot([x[i-1], x[i+1]], [u[i-1], u[i+1]], 'k--', alpha=0.5) + ax4.annotate('中心差分', xy=(x[i], u[i]), xytext=(x[i], u[i]+0.3), + arrowprops=dict(arrowstyle="->", color='black'), + ha='center', fontsize=10) + + ax4.annotate('迎风格式\n使用上游点', xy=(x[i], u[i]), xytext=(x[i]-2, u[i]-0.3), + arrowprops=dict(arrowstyle="->", color='red'), + ha='center', fontsize=9) + + ax4.set_xlabel('x') + ax4.set_ylabel('u') + ax4.legend(loc='upper right') + ax4.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('cfd_grid_illustration.png', dpi=300, bbox_inches='tight') + plt.show() + +# 运行绘图 +plot_cfd_grid_storage() \ No newline at end of file diff --git a/example/figure/1d/mesh/02/testprj.py b/example/figure/1d/mesh/02/testprj.py new file mode 100644 index 00000000..49c24860 --- /dev/null +++ b/example/figure/1d/mesh/02/testprj.py @@ -0,0 +1,21 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Example 4: How to truly center a line +plt.figure(figsize=(6, 6)) + +# Method 1: Manually set symmetric coordinate ranges +plt.plot([-1, 1], [0, 0], 'r-', linewidth=3, label='Horizontal line y=0') +plt.plot([0, 0], [-1, 1], 'b-', linewidth=3, label='Vertical line x=0') + +# Key: Set symmetric axis limits +plt.xlim(-2, 2) +plt.ylim(-2, 2) + +plt.axhline(y=0, color='gray', linestyle='--', alpha=0.5) # x-axis +plt.axvline(x=0, color='gray', linestyle='--', alpha=0.5) # y-axis +plt.grid(True) +plt.title('Centered by setting symmetric ranges') +plt.legend() +plt.gca().set_aspect('equal') # Equal aspect ratio +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/02a/testprj.py b/example/figure/1d/mesh/02a/testprj.py new file mode 100644 index 00000000..004b4b8f --- /dev/null +++ b/example/figure/1d/mesh/02a/testprj.py @@ -0,0 +1,32 @@ +import matplotlib.pyplot as plt +import numpy as np + +# Example 5: Canvas coordinates vs data coordinates +fig = plt.figure(figsize=(8, 6)) + +# Center in canvas coordinates (not data coordinates!) +ax = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # Graphics area centered in canvas + +# Now draw lines in the graphics area +x_center = 5 +y_center = 5 +length = 2 + +# Horizontal line +plt.plot([x_center - length/2, x_center + length/2], + [y_center, y_center], 'r-', linewidth=3, label='Horizontal line') + +# Vertical line +plt.plot([x_center, x_center], + [y_center - length/2, y_center + length/2], 'b-', linewidth=3, label='Vertical line') + +# Set symmetric ranges to center line in graphics area +plt.xlim(x_center - length, x_center + length) +plt.ylim(y_center - length, y_center + length) + +plt.grid(True) +plt.axhline(y=y_center, color='gray', linestyle='--', alpha=0.5) +plt.axvline(x=x_center, color='gray', linestyle='--', alpha=0.5) +plt.title('Line centered in graphics area') +plt.legend() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03/testprj.py b/example/figure/1d/mesh/03/testprj.py new file mode 100644 index 00000000..2bfeb53e --- /dev/null +++ b/example/figure/1d/mesh/03/testprj.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(6, 6)) + +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + +# Method 1: Manually set symmetric coordinate ranges +plt.plot([-1, 1], [0, 0], 'r-', linewidth=3, label='Horizontal line y=0') +plt.plot([0, 0], [-1, 1], 'b-', linewidth=3, label='Vertical line x=0') + +# Key: Set symmetric axis limits +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +plt.title('Centered by setting symmetric ranges') +plt.legend() +plt.axis('equal') +#plt.axis('off') +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03a/testprj.py b/example/figure/1d/mesh/03a/testprj.py new file mode 100644 index 00000000..836afa48 --- /dev/null +++ b/example/figure/1d/mesh/03a/testprj.py @@ -0,0 +1,27 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(12, 4)) # 关键:将画布设为长方形 (宽12, 高4) + +# 创建数据点 +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + +# 画十字线 +plt.plot([-6, 6], [0, 0], 'r-', linewidth=2, label='Horizontal line y=0') # 水平线 +plt.plot([0, 0], [-2, 2], 'b-', linewidth=2, label='Vertical line x=0') # 垂直线 + +# 设置坐标范围 +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +plt.title('Long strip grid: x-range [-6,6], y-range [-2,2]') +plt.legend() + +# 添加网格线 +plt.grid(True, linestyle='--', alpha=0.7) + +# 显示坐标轴刻度 +plt.xticks(np.arange(-6, 7, 1)) +plt.yticks(np.arange(-2, 3, 0.5)) + +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03b/testprj.py b/example/figure/1d/mesh/03b/testprj.py new file mode 100644 index 00000000..9628e970 --- /dev/null +++ b/example/figure/1d/mesh/03b/testprj.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(8, 4)) # 长方形画布 + +# 画十字线 +plt.plot([-6, 6], [0, 0], 'r-', linewidth=2) # 水平线 +plt.plot([0, 0], [-2, 2], 'b-', linewidth=2) # 垂直线 + +# 设置坐标范围 +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +# 关键:使用默认的宽高比,不强制等比例 +ax = plt.gca() +ax.set_aspect('auto') # 这是默认设置,可以省略 + +plt.title('Auto aspect ratio (default)') +plt.grid(True) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03c/testprj.py b/example/figure/1d/mesh/03c/testprj.py new file mode 100644 index 00000000..8a39969c --- /dev/null +++ b/example/figure/1d/mesh/03c/testprj.py @@ -0,0 +1,34 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(10, 4)) + +# 画十字线 +plt.plot([-6, 6], [0, 0], 'r-', linewidth=2) +plt.plot([0, 0], [-2, 2], 'b-', linewidth=2) + +plt.xlim(-6, 6) +plt.ylim(-2, 2) + +# 计算并设置精确的宽高比 +ax = plt.gca() + +# 数据范围比例 +data_ratio = (6 - (-6)) / (2 - (-2)) # x_range / y_range = 12/4 = 3 + +# 获取图形区域的宽高(归一化坐标) +pos = ax.get_position() +fig_width, fig_height = pos.width, pos.height + +# 计算显示比例 +display_ratio = fig_width / fig_height +print(f"display_ratio={display_ratio}") +print(f"data_ratio={data_ratio}") + +# 设置宽高比:data_ratio / (display_ratio) +#ax.set_aspect(data_ratio / display_ratio) +ax.set_aspect(display_ratio / data_ratio) + +plt.title(f'Manual aspect ratio: {display_ratio/data_ratio:.2f}') +plt.grid(True) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03d/testprj.py b/example/figure/1d/mesh/03d/testprj.py new file mode 100644 index 00000000..b75328f0 --- /dev/null +++ b/example/figure/1d/mesh/03d/testprj.py @@ -0,0 +1,44 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, axes = plt.subplots(1, 2, figsize=(14, 5)) + +# 左图:长条形网格 +ax1 = axes[0] +# 画主要网格线 +for x in np.arange(-6, 7, 1): + ax1.axvline(x=x, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) +for y in np.arange(-2, 3, 0.5): + ax1.axhline(y=y, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) + +# 画粗的坐标轴 +ax1.axhline(y=0, color='black', linewidth=2) +ax1.axvline(x=0, color='black', linewidth=2) + +ax1.set_xlim(-6, 6) +ax1.set_ylim(-2, 2) +ax1.set_title('1D-like grid (long strip)') +ax1.set_xlabel('x-axis') +ax1.set_ylabel('y-axis') +ax1.set_aspect('auto') # 不强制等比例 + +# 右图:比较等比例的情况 +ax2 = axes[1] +# 同样画网格线 +for x in np.arange(-6, 7, 1): + ax2.axvline(x=x, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) +for y in np.arange(-2, 3, 0.5): + ax2.axhline(y=y, color='gray', linestyle='-', alpha=0.3, linewidth=0.5) + +ax2.axhline(y=0, color='black', linewidth=2) +ax2.axvline(x=0, color='black', linewidth=2) + +ax2.set_xlim(-6, 6) +ax2.set_ylim(-2, 2) +ax2.set_title('With axis("equal") - becomes square') +ax2.set_xlabel('x-axis') +ax2.set_ylabel('y-axis') +ax2.set_aspect('equal') # 强制等比例 + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/03e/testprj.py b/example/figure/1d/mesh/03e/testprj.py new file mode 100644 index 00000000..c3682f64 --- /dev/null +++ b/example/figure/1d/mesh/03e/testprj.py @@ -0,0 +1,30 @@ +import matplotlib.pyplot as plt +import numpy as np + +plt.figure(figsize=(9, 3)) # 宽:高 = 3:1,对应数据范围比例12:4=3:1 + +# 画你的数据点(如果需要) +x_points = np.array([-6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6]) +y_points = np.zeros_like(x_points) # 假设y坐标都是0 + +plt.scatter(x_points, y_points, color='red', s=50, zorder=5, label='Data points') + +# 画坐标轴 +plt.axhline(y=0, color='blue', linewidth=2, label='x-axis') +plt.axvline(x=0, color='green', linewidth=2, label='y-axis') + +# 设置范围 +plt.xlim(-6.5, 6.5) # 稍微扩大一点 +plt.ylim(-2, 2) + +plt.title('1D Grid Visualization') +plt.xlabel('X coordinate (wide range)') +plt.ylabel('Y coordinate (narrow range)') +plt.legend() +plt.grid(True, alpha=0.3) + +# 关键:不要设置'equal',保持默认的'auto' +# plt.axis('equal') # ← 注释掉或删除这一行! + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04/testprj.py b/example/figure/1d/mesh/04/testprj.py new file mode 100644 index 00000000..30da0a46 --- /dev/null +++ b/example/figure/1d/mesh/04/testprj.py @@ -0,0 +1,36 @@ +import matplotlib.pyplot as plt +import numpy as np + +# 创建一个简单的边界面 +fig, ax = plt.subplots(figsize=(8, 6)) + +# 主竖线 +x_main = 0 +ax.axvline(x=x_main, color='black', linewidth=3, label='Main vertical line') + +# 在主竖线旁边添加斜线 +x_offset = 0.2 # 斜线离主线的距离 +num_slashes = 20 # 斜线数量 +y_positions = np.linspace(-4, 4, num_slashes) + +for i, y in enumerate(y_positions): + # 短斜线的起点和终点 + length = 0.5 + angle = 45 # 斜线角度 + + # 计算斜线端点 + x_start = x_main + y_start = y + x_end = x_start + length * np.cos(np.radians(angle)) + y_end = y_start + length * np.sin(np.radians(angle)) + + ax.plot([x_start, x_end], [y_start, y_end], + color='red', linewidth=1.5, alpha=0.7) + +ax.set_xlim(-2, 2) +ax.set_ylim(-5, 5) +ax.set_aspect('equal') +ax.set_title('Basic vertical line with diagonal slashes') +ax.legend() +ax.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04a/testprj.py b/example/figure/1d/mesh/04a/testprj.py new file mode 100644 index 00000000..6ef8e781 --- /dev/null +++ b/example/figure/1d/mesh/04a/testprj.py @@ -0,0 +1,45 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(figsize=(10, 6)) + +# 主边界竖线 +main_line_x = -3 +ax.axvline(x=main_line_x, color='darkblue', linewidth=4, alpha=0.8) + +# 创建装饰性斜线模式 +num_patterns = 15 +y_range = np.linspace(-4, 4, num_patterns) + +# 不同样式的斜线 +patterns = [ + {'color': 'red', 'length': 0.8, 'angle': 30, 'style': '-'}, + {'color': 'green', 'length': 0.6, 'angle': 45, 'style': '--'}, + {'color': 'orange', 'length': 0.4, 'angle': 60, 'style': '-.'}, +] + +for i, y in enumerate(y_range): + pattern = patterns[i % len(patterns)] # 循环使用不同样式 + + x_start = main_line_x + y_start = y + angle_rad = np.radians(pattern['angle']) + + x_end = x_start + pattern['length'] * np.cos(angle_rad) + y_end = y_start + pattern['length'] * np.sin(angle_rad) + + ax.plot([x_start, x_end], [y_start, y_end], + color=pattern['color'], + linewidth=2, + linestyle=pattern['style'], + alpha=0.7) + +# 添加一些点装饰 +for y in np.linspace(-3.5, 3.5, 8): + ax.scatter(main_line_x + 0.1, y, color='purple', s=50, alpha=0.6) + +ax.set_xlim(-4, 2) +ax.set_ylim(-5, 5) +ax.set_title('Decorative boundary pattern') +ax.grid(True, alpha=0.2) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04b/testprj.py b/example/figure/1d/mesh/04b/testprj.py new file mode 100644 index 00000000..0ad2840a --- /dev/null +++ b/example/figure/1d/mesh/04b/testprj.py @@ -0,0 +1,48 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(figsize=(10, 8)) + +# 两条主竖线 +left_line = -2 +right_line = 2 +ax.axvline(x=left_line, color='black', linewidth=3, label='Left boundary') +ax.axvline(x=right_line, color='black', linewidth=3, label='Right boundary') + +# 在两条竖线之间填充内容 +x_fill = np.linspace(left_line, right_line, 100) +y_fill = np.sin(x_fill * 2) * 2 +ax.fill_between(x_fill, -2, y_fill, alpha=0.2, color='skyblue', label='Area') + +# 在边界上添加对称的斜线 +num_slashes = 25 +y_positions = np.linspace(-3, 3, num_slashes) + +for y in y_positions: + # 左边界斜线(向左) + length = 0.6 + angle = 135 # 指向左下方 + + # 左边界 + x_start = left_line + y_start = y + x_end = x_start + length * np.cos(np.radians(angle)) + y_end = y_start + length * np.sin(np.radians(angle)) + ax.plot([x_start, x_end], [y_start, y_end], 'r-', linewidth=1.5, alpha=0.6) + + # 右边界斜线(向右) + angle = 45 # 指向右下方 + + x_start = right_line + y_start = y + x_end = x_start + length * np.cos(np.radians(angle)) + y_end = y_start + length * np.sin(np.radians(angle)) + ax.plot([x_start, x_end], [y_start, y_end], 'r-', linewidth=1.5, alpha=0.6) + +ax.set_xlim(-3, 3) +ax.set_ylim(-4, 4) +ax.set_aspect('equal') +ax.set_title('Boundary lines with symmetrical slashes') +ax.legend() +ax.grid(True, alpha=0.3) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04c/testprj.py b/example/figure/1d/mesh/04c/testprj.py new file mode 100644 index 00000000..77353fe9 --- /dev/null +++ b/example/figure/1d/mesh/04c/testprj.py @@ -0,0 +1,67 @@ +import matplotlib.pyplot as plt +import numpy as np + +def draw_boundary_pattern(ax, x_position, y_range=(-5, 5), + num_slashes=30, slash_length=0.5, + slash_angle=45, color='blue', + line_width=2, main_line_width=3): + """ + 绘制一个边界面图案 + + 参数: + ax: matplotlib坐标轴对象 + x_position: 主竖线的x坐标 + y_range: 斜线的y坐标范围 (min, max) + num_slashes: 斜线数量 + slash_length: 斜线长度 + slash_angle: 斜线角度(度) + color: 颜色 + """ + + # 画主竖线 + ax.axvline(x=x_position, color=color, linewidth=main_line_width, alpha=0.8) + + # 生成斜线 + y_positions = np.linspace(y_range[0], y_range[1], num_slashes) + angle_rad = np.radians(slash_angle) + + for i, y in enumerate(y_positions): + # 交替改变斜线方向 + direction = 1 if i % 2 == 0 else -1 + current_angle = slash_angle * direction + + # 计算斜线端点 + x_start = x_position + y_start = y + x_end = x_start + slash_length * np.cos(np.radians(current_angle)) + y_end = y_start + slash_length * np.sin(np.radians(current_angle)) + + # 绘制斜线 + ax.plot([x_start, x_end], [y_start, y_end], + color=color, linewidth=line_width, alpha=0.6) + + # 在斜线末端添加小圆点 + ax.scatter(x_end, y_end, color=color, s=20, alpha=0.8, zorder=5) + +# 使用自定义函数 +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) +axes = axes.flatten() + +# 不同的边界样式 +configs = [ + {'x_position': -2, 'color': 'red', 'slash_angle': 30, 'num_slashes': 15}, + {'x_position': 0, 'color': 'green', 'slash_angle': 60, 'num_slashes': 20}, + {'x_position': 2, 'color': 'blue', 'slash_angle': 45, 'num_slashes': 25}, + {'x_position': -1, 'color': 'purple', 'slash_angle': 135, 'num_slashes': 18}, +] + +for ax, config in zip(axes, configs): + draw_boundary_pattern(ax, **config, y_range=(-4, 4)) + ax.set_xlim(-3, 3) + ax.set_ylim(-5, 5) + ax.set_aspect('equal') + ax.set_title(f"Boundary at x={config['x_position']}") + ax.grid(True, alpha=0.2) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04d/testprj.py b/example/figure/1d/mesh/04d/testprj.py new file mode 100644 index 00000000..27ded332 --- /dev/null +++ b/example/figure/1d/mesh/04d/testprj.py @@ -0,0 +1,68 @@ +import matplotlib.pyplot as plt +import numpy as np + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) + +# 左图:简单边界 +# 主竖线 +boundary_x = -1 +ax1.axvline(x=boundary_x, color='black', linewidth=4) + +# 添加斜线装饰 +num_slashes = 12 +for y in np.linspace(-2, 2, num_slashes): + # 创建短斜线簇(每组3条) + for offset in [-0.1, 0, 0.1]: + ax1.plot([boundary_x, boundary_x + 0.5], + [y + offset, y + offset + 0.3], + color='darkred', linewidth=1.5, alpha=0.7) + +ax1.set_xlim(-2, 1) +ax1.set_ylim(-3, 3) +ax1.set_aspect('equal') +ax1.set_title('Simple boundary with slash clusters') +ax1.grid(True, alpha=0.3) + +# 右图:复杂边界模式 +# 多条竖线形成边界带 +for x in np.arange(-1, 1.1, 0.2): + ax2.axvline(x=x, color='gray', linewidth=1, alpha=0.3) + +# 主边界线 +main_boundary = 0 +ax2.axvline(x=main_boundary, color='darkblue', linewidth=3) + +# 在边界上添加有规律的斜线 +y_positions = np.arange(-2.5, 3, 0.5) +for i, y in enumerate(y_positions): + # 交替的斜线方向 + direction = -1 if i % 2 == 0 else 1 + angle = 45 * direction + + # 画斜线 + length = 0.4 + x_end = main_boundary + length * np.cos(np.radians(angle)) + y_end = y + length * np.sin(np.radians(angle)) + + ax2.plot([main_boundary, x_end], [y, y_end], + color='orange', linewidth=2, alpha=0.8) + + # 在斜线末端添加箭头 + ax2.scatter(x_end, y_end, color='red', s=30, alpha=0.7) + +# 添加文字说明 +ax2.text(main_boundary + 0.5, 2.5, 'Field/Area', + fontsize=12, ha='center', va='center', + bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5)) +ax2.text(main_boundary - 0.5, 2.5, 'Boundary', + fontsize=12, ha='center', va='center', + bbox=dict(boxstyle='round', facecolor='lightcoral', alpha=0.5)) + +ax2.set_xlim(-1.5, 1.5) +ax2.set_ylim(-3, 3) +ax2.set_aspect('equal') +ax2.set_title('Complex boundary pattern with directional indicators') +ax2.grid(True, alpha=0.3) + +plt.tight_layout() +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04e/testprj.py b/example/figure/1d/mesh/04e/testprj.py new file mode 100644 index 00000000..c6619668 --- /dev/null +++ b/example/figure/1d/mesh/04e/testprj.py @@ -0,0 +1,27 @@ +import matplotlib.pyplot as plt +import numpy as np + +# 最简单的边界面实现 +plt.figure(figsize=(8, 6)) + +# 1. 画主竖线 +plt.axvline(x=0, color='black', linewidth=4) + +# 2. 在旁边添加斜线 +y_positions = np.arange(-4, 4.5, 0.5) # 从-4到4,步长0.5 + +for y in y_positions: + # 每个位置画一条斜线 + plt.plot([0, 0.5], # x坐标:从0到0.5 + [y, y + 0.3], # y坐标:向上倾斜 + color='red', linewidth=2) + +# 3. 设置显示范围 +plt.xlim(-2, 2) +plt.ylim(-5, 5) + +# 4. 添加标题和网格 +plt.title('Simple boundary: vertical line with diagonal slashes') +plt.grid(True, alpha=0.3) + +plt.show() \ No newline at end of file diff --git a/example/figure/1d/mesh/04f/testprj.py b/example/figure/1d/mesh/04f/testprj.py new file mode 100644 index 00000000..9ff31193 --- /dev/null +++ b/example/figure/1d/mesh/04f/testprj.py @@ -0,0 +1,104 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.collections import LineCollection # 用于批量绘制(优化性能) + +def draw_border_with_slants( + border_x: float = 5, # 主竖线的x坐标 + y_start: float = 1, # 斜线起始y坐标 + y_end: float = 7, # 斜线结束y坐标 + num_lines: int = 20, # 斜线数量 + line_length: float = 0.8, # 斜线长度 + angle: float = 30, # 斜线与水平方向夹角(度,负数反向) + border_color: str = 'black', # 主竖线颜色 + border_width: float = 2, # 主竖线宽度 + slant_color: str = 'red', # 斜线颜色 + slant_width: float = 1, # 斜线宽度 + slant_alpha: float = 0.7, # 斜线透明度 + x_lim: tuple = (0, 10), # x轴范围 + y_lim: tuple = (0, 8), # y轴范围 + title: str = '竖线+短斜线边界', # 图表标题 + save_path: str = None # 保存路径(None则不保存) +) -> None: + """ + 绘制带短斜线的竖线边界图(Matplotlib版) + + 参数说明: + - border_x: 主竖线的x坐标 + - y_start/y_end: 斜线在y轴的分布范围 + - num_lines: 斜线数量(越多越密集) + - line_length: 斜线的长度 + - angle: 斜线角度(正数向右倾,负数向左倾) + - border_color/border_width: 主竖线样式 + - slant_color/slant_width/slant_alpha: 斜线样式 + - x_lim/y_lim: 坐标轴范围 + - title: 图表标题 + - save_path: 保存路径(如'border.png'),None则仅显示 + """ + # 1. 初始化画布 + fig, ax = plt.subplots(figsize=(8, 6)) + ax.set_xlim(x_lim) + ax.set_ylim(y_lim) + ax.set_xlabel('X轴') + ax.set_ylabel('Y轴') + ax.set_title(title) + ax.grid(alpha=0.3) # 网格线(可选) + + # 2. 绘制主竖线 + ax.axvline(x=border_x, color=border_color, linewidth=border_width, label='边界线') + + # 3. 计算斜线坐标(批量生成,优化性能) + y_positions = np.linspace(y_start, y_end, num_lines) + angle_rad = np.deg2rad(angle) + dx = line_length * np.cos(angle_rad) + dy = line_length * np.sin(angle_rad) + + # 生成所有斜线的坐标对(格式:[[(x1,y1), (x2,y2)], ...]) + lines = [] + for y in y_positions: + x1, y1 = border_x, y + x2, y2 = x1 + dx, y1 + dy + lines.append([(x1, y1), (x2, y2)]) + + # 4. 批量绘制斜线(比循环plot更高效,尤其斜线数量多时) + lc = LineCollection( + lines, + colors=slant_color, + linewidths=slant_width, + alpha=slant_alpha + ) + ax.add_collection(lc) + + # 5. 等比例显示(保证角度准确)+ 图例 + plt.axis('equal') + ax.legend() + + # 6. 保存/显示 + if save_path: + plt.savefig(save_path, dpi=300, bbox_inches='tight') # 高分辨率保存 + plt.show() + +# ------------------------------ +# 调用示例(直接运行即可) +# ------------------------------ +if __name__ == '__main__': + # 示例1:默认参数(向右倾斜的红色斜线) + draw_border_with_slants() + + # 示例2:自定义参数(向左倾斜的蓝色斜线,更密集) + draw_border_with_slants( + border_x=4, + num_lines=30, # 更多斜线 + angle=-45, # 向左倾斜 + slant_color='blue', # 蓝色斜线 + title='向左倾斜的密集斜线边界', + save_path='custom_border.png' # 保存到本地 + ) + + # 示例3:更粗的斜线+黑色主竖线 + draw_border_with_slants( + slant_width=2, + border_width=3, + line_length=1.2, + angle=60, + slant_color='green' + ) \ No newline at end of file diff --git a/example/figure/1d/mesh/05/testprj.py b/example/figure/1d/mesh/05/testprj.py new file mode 100644 index 00000000..0eba75f4 --- /dev/null +++ b/example/figure/1d/mesh/05/testprj.py @@ -0,0 +1,171 @@ +import matplotlib.pyplot as plt +import numpy as np + +class Ghost: + def __init__(self, xstart, dx, ncells, lr): + self.lr = lr + self.xstart = xstart + self.dx = dx + self.ncells = ncells + self.nnodes = self.ncells + 1 + def generate_mesh(self): + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + for i in range(0, self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0 + + for i in range(0, self.ncells): + self.xcc[i] = 0.5*(self.x[i]+self.x[i+1]) + self.ycc[i] = 0 + + def printinfo(self): + print(f"Ghost ncells={self.ncells}") + print(f"Ghost nnodes={self.nnodes}") + print(f"Ghost xstart={self.xstart}") + print(f"Ghost lr={self.lr}") + print(f"Ghost dx={self.dx}") + print(f"Ghost x={self.x}") + print(f"Ghost xcc={self.xcc}") + + + def plot(self): + y0 = 0 + ytext = y0 - 0.1 + for i in range(0, self.ncells): + if self.lr == "L": + mystr = f"${0-i}$" + else: + mystr = f"$N+{i+1}$" + plt.text(self.xcc[i], ytext, mystr, fontsize=12, ha='center') + + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + +class Mesh: + def __init__(self): + self.ncells = 9 + self.nnodes = self.ncells + 1 + self.xmin = 0.0 + self.xmax = 1.0 + + def generate_mesh(self): + self.dx = (self.xmax-self.xmin) / self.ncells + print(f"self.dx={self.dx}") + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + for i in range(0, self.nnodes): + self.x[i] = self.xmin + self.dx*(i) + self.y[i] = 0 + + for i in range(0, self.ncells): + self.xcc[i] = 0.5*(self.x[i]+self.x[i+1]) + self.ycc[i] = 0 + + #print(f"self.x={self.x}") + self.nghosts = 2 + + self.ghost_mesh_left = Ghost(self.x[0],-self.dx,self.nghosts,"L") + self.ghost_mesh_right = Ghost(self.x[-1],self.dx,self.nghosts,"R") + + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + print(f"ncells={self.ncells}") + print(f"nnodes={self.nnodes}") + print(f"xmin,xmax={self.xmin,self.xmax}") + self.ghost_mesh_left.printinfo() + self.ghost_mesh_right.printinfo() + + def plot_vertical_interface_lines(self): + dy = 0.1 * self.dx + for i in range(0, self.nnodes): + xm = self.x[i] + ym = self.y[i] + coef = 1 + if i == 0 or i== self.nnodes -1: + coef = 2 + plt.plot([xm, xm], [ym-coef*dy, ym+coef*dy], 'k-') # 绘制垂直线 + + def plot(self): + plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + plt.plot(self.x, self.y, 'k-', linewidth=1) + dy = 0.1 * self.dx + self.plot_vertical_interface_lines() + """ + for i in range(0, self.nnodes): + xm = self.x[i] + ym = self.y[i] + coef = 1 + if i == 0 or i== self.nnodes -1: + coef = 2 + plt.plot([xm, xm], [ym-coef*dy, ym+coef*dy], 'k-') # 绘制垂直线 + """ + plt.text(self.x[0], self.y[0]+3*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3*dy, r'$x=b$', fontsize=12, ha='center') + + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + +def plot_cfd_line( x_points, y0 ): + # 绘制除中间 5 个点和特定边缘点外的其他点 (内部红色,边缘黑色) + edge_points_red = np.concatenate([x_points[:2], x_points[:-2]]) + plt.scatter(edge_points_red, np.full_like(edge_points_red, y0), s=100, facecolor='red', edgecolor='black', linewidth=1) + + # 绘制左侧第三点 (i=-4) 和右侧第三点 (i=4) 为纯黑色点 + special_black_points = np.array([-4, 4]) + plt.scatter(special_black_points, np.full_like(special_black_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + # 绘制中间 6 个点 (i=-2, -1, 0, 1, 2, 3) + #middle_points = x_points[3:8] + middle_points = x_points[3:9] + plt.scatter(middle_points, np.full_like(middle_points, y0), s=100, facecolor='black', edgecolor='black', linewidth=1) + + # 绘制中间 6 个点的黑实线连接 + plt.plot(middle_points, np.full_like(middle_points, y0), 'k-', linewidth=1) + + # 添加左起第三点和第四点之间的分段连线(-4到-2) + plot_mixed_line(-4,-2) + + # 添加右起第三点和第四点之间的分段连线(2到4) + plot_mixed_line(2,4) + +def plot_cfd_figure(): + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + plt.figure(figsize=(12, 4)) + + #x_points = np.array([-20,-10, -7, -6, -5, -4, -2, -1, 0, 1, 2, 3, 4, 5, 6], dtype=np.float64) + inner_points = 7 + bc_points_left = 2 + bc_points_right = 2 + points_max = inner_points + bc_points_right + x_points = np.arange(-bc_points_left, points_max+1, 1, dtype=np.float64) # 终止值(开区间),步长1 + print(x_points) # 输出:[-2. -1. 0. 1. 2.] + mesh = Mesh() + mesh.generate_mesh() + mesh.printinfo() + mesh.plot() + + y0 = 0 + + + # Key: Set symmetric axis limits + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + plt.axis('equal') + plt.axis('off') + + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05a/testprj.py b/example/figure/1d/mesh/05a/testprj.py new file mode 100644 index 00000000..afeabd94 --- /dev/null +++ b/example/figure/1d/mesh/05a/testprj.py @@ -0,0 +1,186 @@ +import matplotlib.pyplot as plt +import numpy as np + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0.0 + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.0 + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, lr=lr) + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + mystr = f"${- (i+1)}$" # Label format for left ghost cells + else: + mystr = f"$N+{i+1}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, mystr, fontsize=12, ha='center') + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + def plot_ghost_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + for i in range(1, self.nnodes-1): + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self): + # Define main mesh physical boundaries and grid resolution + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 9 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, lr=None) + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + # Plot main mesh cell-center points (black fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + # Plot horizontal line connecting main mesh nodes + plt.plot(self.x, self.y, 'k-', linewidth=1) + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dy = 0.1 * self.dx + plt.text(self.x[0], self.y[0]+3*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3*dy, r'$x=b$', fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh() + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05b/testprj.py b/example/figure/1d/mesh/05b/testprj.py new file mode 100644 index 00000000..825709a9 --- /dev/null +++ b/example/figure/1d/mesh/05b/testprj.py @@ -0,0 +1,237 @@ +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x,y_start,y_end,lr): + # 3. 批量绘制短斜线 + num_lines = 10 # 斜线数量 + #y_start = 1 # 斜线起始y坐标 + #y_end = 7 # 斜线结束y坐标 + dy = y_end - y_start + y_positions = np.linspace(y_start+0.02*dy, y_end-0.02*dy, num_lines) # 均匀分布的y坐标 + line_length = 0.2*dy # 斜线长度 + if lr == "L": + angle = 180 + 30 + else: + angle = 30 # 斜线与水平方向的夹角(度),可改负数反向 + + # 计算斜线的终点坐标(三角函数转换) + angle_rad = np.deg2rad(angle) + dx = line_length * np.cos(angle_rad) + dy = line_length * np.sin(angle_rad) + + #plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # 循环绘制每条斜线 + for y in y_positions: + # 斜线起点:竖线位置 + x1 = border_x + y1 = y + # 斜线终点:基于角度计算 + x2 = x1 + dx + y2 = y1 + dy + # 绘制斜线(可自定义样式) + #plt.plot([x1, x2], [y1, y2], color='red', linewidth=1, alpha=0.7) + #plt.plot([x1, x2], [y1, y2], color='black', linewidth=1, alpha=0.7) + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0.0 + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.0 + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i}$" # Label format for left ghost cells + else: + cell_label = f"$N+{i+1}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self): + # Define main mesh physical boundaries and grid resolution + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 9 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, lr=None) + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh + nlabels = 2 + for i in range(self.ncells): + if i < nlabels: + cell_label = f"${i+1}$" + elif i > self.ncells - 1 - nlabels: + inew = i - (self.ncells - 1) + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{inew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + # Plot main mesh cell-center points (black fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + # Plot horizontal line connecting main mesh nodes + plt.plot(self.x, self.y, 'k-', linewidth=1) + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + self.plot_cell_label() + + # Add boundary labels for main mesh + dy = 0.1 * self.dx + plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh() + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05c/testprj.py b/example/figure/1d/mesh/05c/testprj.py new file mode 100644 index 00000000..b75e3e20 --- /dev/null +++ b/example/figure/1d/mesh/05c/testprj.py @@ -0,0 +1,302 @@ +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = 0.0 + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.0 + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i}$" # Label format for left ghost cells + else: + cell_label = f"$N+{i+1}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self): + # Define main mesh physical boundaries and grid resolution + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, lr=None) + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{inew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-ytext_shift, f"$i$", fontsize=12, ha='center') + print(f"icenter={icenter}") + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + #plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + + # Plot horizontal line connecting main mesh nodes + #plt.plot(self.x, self.y, 'k-', linewidth=1) + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + self.plot_cell_label() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dy = 0.1 * abs(self.dx) + plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + dym = abs(self.dx) + + plt.text(self.x[0], self.y[0]-dym, r"$x_{\frac{1}{2}}$", fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, r"$x_{\frac{3}{2}}$", fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, r"$x_{N+\frac{1}{2}}$", fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, r"$x_{N-\frac{1}{2}}$", fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh() + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05d/testprj.py b/example/figure/1d/mesh/05d/testprj.py new file mode 100644 index 00000000..aeac3484 --- /dev/null +++ b/example/figure/1d/mesh/05d/testprj.py @@ -0,0 +1,363 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1)+self.ishift + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{inew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-ytext_shift, f"$i$", fontsize=12, ha='center') + print(f"icenter={icenter}") + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + #plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + #plt.plot(self.x, self.y, 'k-', linewidth=1) + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def getstring(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringN(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dy = 0.1 * abs(self.dx) + plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + dym = abs(self.dx) + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstring(a1) + str_a2 = self.getstring(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringN(an1) + str_na2 = self.getstringN(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + #plt.text(self.x[-1], self.y[-1]-dym, r"$x_{N+\frac{1}{2}}$", fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, r"$x_{N-\frac{1}{2}}$", fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 4)) + + # Initialize main mesh and generate grid coordinates + mesh = Mesh(0,0.2) + mesh.generate_total_mesh() + + # Print mesh information for verification + mesh.printinfo() + # Render all mesh components + mesh.plot() + + mesh1 = Mesh(-1,-0.2) + mesh1.generate_total_mesh() + + # Print mesh information for verification + mesh1.printinfo() + # Render all mesh components + mesh1.plot() + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05e/testprj.py b/example/figure/1d/mesh/05e/testprj.py new file mode 100644 index 00000000..a02b7d05 --- /dev/null +++ b/example/figure/1d/mesh/05e/testprj.py @@ -0,0 +1,362 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + absinew = abs(inew) + sign = "-" if inew < 0 else "+" + if inew == 0: + cell_label = f"$N$" + else: + cell_label = f"$N{sign}{absinew}$" + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-ytext_shift, f"$i$", fontsize=12, ha='center') + print(f"icenter={icenter}") + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + #plt.scatter(self.xcc, self.ycc, s=50, facecolor='black', edgecolor='black', linewidth=1) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + #plt.plot(self.x, self.y, 'k-', linewidth=1) + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + def getstring(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringN(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dy = 0.1 * abs(self.dx) + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + #plt.text(self.x[0], self.y[0]+3.5*dy, r'$x=a$', fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[0]+3.5*dy, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstring(a1) + str_a2 = self.getstring(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringN(an1) + str_na2 = self.getstringN(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + #plt.text(self.x[-1], self.y[-1]-dym, r"$x_{N+\frac{1}{2}}$", fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, r"$x_{N-\frac{1}{2}}$", fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + mesh1 = Mesh(0,0.6) + mesh2 = Mesh(2,0.2) + mesh3 = Mesh(-1,-0.2) + mesh3 = Mesh(1,-0.6) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05f/testprj.py b/example/figure/1d/mesh/05f/testprj.py new file mode 100644 index 00000000..7b6da6e3 --- /dev/null +++ b/example/figure/1d/mesh/05f/testprj.py @@ -0,0 +1,375 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = 2 # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ist={1+self.ishift},ied={cell_label},ied-ist=N$" + text = f"$ied-ist=N$" + plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + mesh1 = Mesh(0,0.6) + mesh2 = Mesh(2,0.2) + mesh3 = Mesh(-1,-0.2) + mesh3 = Mesh(1,-0.6) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/mesh/05g/testprj.py b/example/figure/1d/mesh/05g/testprj.py new file mode 100644 index 00000000..932d3d21 --- /dev/null +++ b/example/figure/1d/mesh/05g/testprj.py @@ -0,0 +1,378 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ist={1+self.ishift},ied={cell_label},ied-ist=N$" + text = f"$ied-ist=N$" + plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + nghost3 = 3 + mesh1 = Mesh(nghost3,0,0.6) + mesh2 = Mesh(nghost3,nghost3,0.2) + mesh3 = Mesh(nghost3,-1,-0.2) + mesh3 = Mesh(nghost3,nghost3-1,-0.6) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/periodic/01/testprj.py b/example/figure/1d/periodic/01/testprj.py new file mode 100644 index 00000000..38786f55 --- /dev/null +++ b/example/figure/1d/periodic/01/testprj.py @@ -0,0 +1,375 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0): + # Define main mesh physical boundaries and grid resolution + print(f"Mesh ishift={ishift}") + self.ym = ym + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ist={1+self.ishift},ied={cell_label},ied-ist=N$" + text = f"$ied-ist=N$" + plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + +def plot_cfd_figure(): + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + nghost3 = 3 + mesh = Mesh(nghost2,nghost2-1) + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + plt.savefig('cfd.png', bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure() \ No newline at end of file diff --git a/example/figure/1d/periodic/01a/testprj.py b/example/figure/1d/periodic/01a/testprj.py new file mode 100644 index 00000000..bd9cc0e3 --- /dev/null +++ b/example/figure/1d/periodic/01a/testprj.py @@ -0,0 +1,396 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # 新增:periodic参数,默认False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # 新增:周期标志 + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + + # 新增:如果periodic=True,绘制周期连接线 + if self.periodic: + self.plot_periodic_connections() + + # 新增方法:绘制周期边界折线箭头 + def plot_periodic_connections(self): + """绘制从源细胞到鬼细胞的折线箭头,表示值赋值""" + vlen = 0.3 * abs(self.dx) # 竖线长度 + h_offset = 0.02 # 水平线微调,避免重叠 + color = 'green' # 绿色区分 + lw = 1 # 线宽 + + # 右鬼赋值:cell2 (主i=1) -> N+2 (右鬼i=0), cell3 (主i=2) -> N+3 (右鬼i=1) + for src_i, tgt_i in [(1, 0), (2, 1)]: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + # 竖线向上 + plt.plot([src_x, src_x], [src_y, src_y + vlen], color=color, linewidth=lw) + # 水平线到目标上方 + plt.plot([src_x, tgt_x], [src_y + vlen, tgt_y + vlen], color=color, linewidth=lw) + # 带箭头竖线向下到目标 + plt.arrow(tgt_x, tgt_y + vlen, 0, tgt_y - (tgt_y + vlen), + head_width=0.01, head_length=0.01, fc=color, ec=color, linewidth=lw) + + # 左鬼赋值:cell N (主i=6) -> u[0] (左鬼i=1, 标签0), cell N-1 (主i=5) -> u[1] (左鬼i=0, 标签1) + # 匹配用户:u[N]->u[0] (主i=6 -> 左i=1), u[N+1]->u[1] 但N+1无,用主i=5近似u[N-1]->u[1] (左i=0) + for src_i, tgt_i in [(6, 1), (5, 0)]: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + # 竖线向上 + plt.plot([src_x, src_x], [src_y, src_y + vlen], color=color, linewidth=lw) + # 水平线到目标上方 (向左) + plt.plot([src_x, tgt_x], [src_y + vlen, tgt_y + vlen], color=color, linewidth=lw) + # 带箭头竖线向下到目标 + plt.arrow(tgt_x, tgt_y + vlen, 0, tgt_y - (tgt_y + vlen), + head_width=0.01, head_length=0.01, fc=color, ec=color, linewidth=lw) + +def plot_cfd_figure(periodic=False): # 新增:periodic参数,默认False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # 传递periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # 示例:启用周期可视化 \ No newline at end of file diff --git a/example/figure/1d/periodic/01b/testprj.py b/example/figure/1d/periodic/01b/testprj.py new file mode 100644 index 00000000..5c398b5b --- /dev/null +++ b/example/figure/1d/periodic/01b/testprj.py @@ -0,0 +1,418 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + #vlen = 0.3 * abs(self.dx) # Vertical line length + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 1 # Line width + + # Left ghost assignments: Draw above x-axis (y > 0) + # cell N (main i=6) -> u[0] (left ghost i=1), cell N-1 (main i=5) -> u[1] (left ghost i=0) + connections_left = [(6, 1, '-', 'blue'), (5, 0, '--', 'purple')] # Different line styles and colors + for src_i, tgt_i, ls, color in connections_left: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + # Source vertical line upward with arrow (from src_y + offset_y to src_y + vlen + offset_y) + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target above + plt.plot([src_x, tgt_x], [end_y_src, tgt_y + vlen + offset_y], color=color, ls=ls, linewidth=lw) + print(f"end_y_src,tgt_y + vlen + offset_y={end_y_src,end_y_src}") + # Target vertical line downward with arrow (from tgt_y + vlen + offset_y to tgt_y + offset_y) + start_y_tgt = tgt_y + vlen + offset_y + end_y_tgt = tgt_y + offset_y + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + + # Right ghost assignments: Draw below x-axis (y < 0) by mirroring the logic + # cell2 (main i=1) -> N+2 (right ghost i=0), cell3 (main i=2) -> N+3 (right ghost i=1) + connections_right = [(0, 0, '-', 'green'), (1, 1, '--', 'orange')] # Different line styles and colors + for src_i, tgt_i, ls, color in connections_right: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + # Source vertical line downward with arrow (from src_y - offset_y to src_y - vlen - offset_y) + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target below + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + # Target vertical line upward with arrow (from tgt_y - vlen - offset_y to tgt_y - offset_y) + start_y_tgt = tgt_y - vlen - offset_y + h_offset + end_y_tgt = tgt_y - offset_y + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01c/testprj.py b/example/figure/1d/periodic/01c/testprj.py new file mode 100644 index 00000000..0d52fc80 --- /dev/null +++ b/example/figure/1d/periodic/01c/testprj.py @@ -0,0 +1,424 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + #vlen = 0.3 * abs(self.dx) # Vertical line length + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 1 # Line width + + # Left ghost assignments: Draw above x-axis (y > 0) + # cell N (main i=6) -> u[0] (left ghost i=1), cell N-1 (main i=5) -> u[1] (left ghost i=0) + connections_left = [(6, 0, '-', 'blue'), (5, 1, '--', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + # Source vertical line upward with arrow (from src_y + offset_y to src_y + vlen + offset_y) + dy = idx * vlen + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + dy + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target above + #plt.plot([src_x, tgt_x], [end_y_src, tgt_y + vlen + offset_y], color=color, ls=ls, linewidth=lw) + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + print(f"end_y_src,tgt_y + vlen + offset_y={end_y_src,end_y_src}") + # Target vertical line downward with arrow (from tgt_y + vlen + offset_y to tgt_y + offset_y) + start_y_tgt = tgt_y + vlen + offset_y + dy + #start_y_tgt = start_y_src + end_y_tgt = tgt_y + offset_y + #print(f"") + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + + # Right ghost assignments: Draw below x-axis (y < 0) by mirroring the logic + # cell2 (main i=1) -> N+2 (right ghost i=0), cell3 (main i=2) -> N+3 (right ghost i=1) + connections_right = [(0, 0, '-', 'green'), (1, 1, '--', 'orange')] # Different line styles and colors + for src_i, tgt_i, ls, color in connections_right: + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + # Source vertical line downward with arrow (from src_y - offset_y to src_y - vlen - offset_y) + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y + plt.arrow(src_x, start_y_src, 0, end_y_src - start_y_src, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + # Horizontal line to target below + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + # Target vertical line upward with arrow (from tgt_y - vlen - offset_y to tgt_y - offset_y) + start_y_tgt = tgt_y - vlen - offset_y + h_offset + end_y_tgt = tgt_y - offset_y + plt.arrow(tgt_x, start_y_tgt, 0, end_y_tgt - start_y_tgt, + head_width=head_width, head_length=head_length, fc=color, ec=color, linewidth=lw) + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01d/testprj.py b/example/figure/1d/periodic/01d/testprj.py new file mode 100644 index 00000000..9e13c521 --- /dev/null +++ b/example/figure/1d/periodic/01d/testprj.py @@ -0,0 +1,464 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + #vlen = 0.3 * abs(self.dx) # Vertical line length + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 2 # Line width + + connections_left = [(6, 0, '-', 'blue'), (5, 1, '-', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + dy + + plt.plot([src_x, src_x], [start_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, start_y_src, src_x, end_y_src, color=color) + + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, end_y_src, tgt_x, end_y_src, color=color) + + start_y_tgt = tgt_y + vlen + offset_y + dy + end_y_tgt = tgt_y + offset_y + + plt.plot([tgt_x, tgt_x], [start_y_tgt, end_y_tgt], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, tgt_x, start_y_tgt, tgt_x, end_y_tgt, color=color) + + connections_right = [(0, 0, '-', 'green'), (1, 1, '-', 'orange')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_right): + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y - dy + + plt.plot([src_x, src_x], [start_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, start_y_src, src_x, end_y_src, color=color) + + plt.plot([src_x, tgt_x], [end_y_src, end_y_src], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, src_x, end_y_src, tgt_x, end_y_src, color=color) + + #start_y_tgt = tgt_y + vlen + offset_y + dy + #end_y_tgt = tgt_y + offset_y + + start_y_tgt = tgt_y - vlen - offset_y - dy + end_y_tgt = tgt_y - offset_y + + plt.plot([tgt_x, tgt_x], [start_y_tgt, end_y_tgt], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, tgt_x, start_y_tgt, tgt_x, end_y_tgt, color=color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01e/testprj.py b/example/figure/1d/periodic/01e/testprj.py new file mode 100644 index 00000000..98e62ba5 --- /dev/null +++ b/example/figure/1d/periodic/01e/testprj.py @@ -0,0 +1,480 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + #self.nghosts = 2 # Number of ghost cell layers on each side + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + #h_offset = 0.02 # Horizontal line fine-tuning to avoid overlap + h_offset = 0.0 + head_width = 0.01 # Arrow head width + head_length = 0.01 # Arrow head length + lw = 2 # Line width + + connections_left = [(6, 0, '-', 'blue'), (5, 1, '-', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y + offset_y + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + connections_right = [(0, 0, '-', 'green'), (1, 1, '-', 'orange')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_right): + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + dy = idx * vlen + start_y_src = src_y - offset_y + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01f/testprj.py b/example/figure/1d/periodic/01f/testprj.py new file mode 100644 index 00000000..b4027a31 --- /dev/null +++ b/example/figure/1d/periodic/01f/testprj.py @@ -0,0 +1,489 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + print(f"Mesh ishift={ishift}") + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + #self.ncells = 9 # Number of cells in main mesh + self.ncells = 7 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + self.nghosts = nghosts # Number of ghost cell layers on each side + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = 2 + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = 2 + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + # Added method: Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + connections_left = [(6, 0, '-', 'blue'), (5, 1, '-', 'purple')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_left): + #print(f"连接 {idx}: 从节点{src_i}到节点{tgt_i}, 线型{ls}, 颜色{color}") + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[tgt_i], self.ghost_mesh_left.ycc[tgt_i] + + dy = idx * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + connections_right = [(0, 0, '-', 'green'), (1, 1, '-', 'orange')] # Different line styles and colors + for idx, (src_i, tgt_i, ls, color) in enumerate(connections_right): + src_x, src_y = self.xcc[src_i], self.ycc[src_i] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[tgt_i], self.ghost_mesh_right.ycc[tgt_i] + + dy = idx * vlen + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + nghost2 = 2 + mesh = Mesh(nghost2,nghost2-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01g/testprj.py b/example/figure/1d/periodic/01g/testprj.py new file mode 100644 index 00000000..d3aba0f0 --- /dev/null +++ b/example/figure/1d/periodic/01g/testprj.py @@ -0,0 +1,491 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + #self.nlabels = 2 + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + #self.nlabels = 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/periodic/01g0/testprj.py b/example/figure/1d/periodic/01g0/testprj.py new file mode 100644 index 00000000..a0a5cc9b --- /dev/null +++ b/example/figure/1d/periodic/01g0/testprj.py @@ -0,0 +1,490 @@ +from fractions import Fraction +import matplotlib.pyplot as plt +import numpy as np +from itertools import cycle + +def draw_arrow_only(ax, x_start, y_start, x_end, y_end, color='blue', position=0.5, + arrow_style='->', linewidth=2, + head_size=15, zorder=2): + """ + Draw only the arrow head without the connecting line. + """ + # Calculate arrow position + arrow_x = x_start + position * (x_end - x_start) + arrow_y = y_start + position * (y_end - y_start) + + # Calculate direction + dx = x_end - x_start + dy = y_end - y_start + length = np.sqrt(dx**2 + dy**2) + + if length > 0: + dx_norm = dx / length + dy_norm = dy / length + else: + dx_norm, dy_norm = 0, 0 + + # Very small offset to create just the arrow head + offset = 0.001 * length + + # Create arrow + arrow = ax.annotate('', + xy=(arrow_x + offset * dx_norm, arrow_y + offset * dy_norm), + xytext=(arrow_x - offset * dx_norm, arrow_y - offset * dy_norm), + arrowprops=dict(arrowstyle=arrow_style, + color=color, + linewidth=linewidth, + mutation_scale=head_size, + #linestyle='none', # No line! + shrinkA=0, + shrinkB=0), + zorder=zorder) + + return arrow + +def draw_periodic_connections_by_points(xp, yp, color): + ls = '-' + lw = 2 # Line width + for i in range(len(xp)-1): + x0 = xp[i] + y0 = yp[i] + + x1 = xp[i+1] + y1 = yp[i+1] + + plt.plot([x0, x1], [y0, y1], color=color, ls=ls, linewidth=lw) + draw_arrow_only(plt, x0, y0, x1, y1, color=color) + +def plot_vertical_boundary(border_x, y_start, y_end, lr): + """ + Plot a vertical boundary with diagonal lines on one side. + + Parameters: + ----------- + border_x : float + The x-coordinate of the vertical boundary line + y_start : float + The starting y-coordinate of the vertical boundary + y_end : float + The ending y-coordinate of the vertical boundary + lr : str + Direction indicator: 'L' for left side, 'R' for right side + Determines the orientation of diagonal lines + """ + # Batch plot diagonal lines + num_lines = 10 # Number of diagonal lines + + # Calculate the total height + dy = y_end - y_start + + # Generate evenly spaced y positions for diagonal lines + # Add small margins to avoid lines at the very edges + y_positions = np.linspace(y_start + 0.02 * dy, y_end - 0.02 * dy, num_lines) + + # Length of each diagonal line (as percentage of total height) + line_length = 0.2 * dy + + # Determine angle based on direction + if lr == "L": + angle = 180 + 30 # 210 degrees for left side + else: + angle = 30 # 30 degrees for right side + + # Convert angle to radians for trigonometric calculations + angle_rad = np.deg2rad(angle) + + # Calculate dx and dy components for the diagonal lines + dx = line_length * np.cos(angle_rad) + dy_component = line_length * np.sin(angle_rad) + + # Plot the main vertical boundary line + plt.plot([border_x, border_x], [y_start, y_end], 'k-', linewidth=2) + + # Plot each diagonal line + for y in y_positions: + # Start point: on the vertical line + x1 = border_x + y1 = y + + # End point: offset by dx and dy + x2 = x1 + dx + y2 = y1 + dy_component + + # Plot the diagonal line + plt.plot([x1, x2], [y1, y2], color='blue', linewidth=1) + +class BaseMesh: + """Base class for mesh (main mesh/ghost mesh) with common grid logic""" + def __init__(self, ncells, xstart, dx, ishift=0, ym=0, lr=None): + self.ncells = ncells + self.nnodes = self.ncells + 1 + self.xstart = xstart # Starting coordinate of the mesh + self.dx = dx # Grid spacing (negative value for left ghost mesh) + self.lr = lr # Identifier for ghost mesh: "L" (left) / "R" (right), None for main mesh + self.ym = ym + self.ishift = ishift + + # Initialize empty arrays for node coordinates and cell-center coordinates + self.x = np.zeros(self.nnodes, dtype=np.float64) + self.y = np.zeros(self.nnodes, dtype=np.float64) + self.xcc = np.zeros(self.ncells, dtype=np.float64) + self.ycc = np.zeros(self.ncells, dtype=np.float64) + def generate_mesh(self): + """Calculate node coordinates and cell-center coordinates for the mesh""" + # Compute node coordinates along x-axis (y=0 for 1D mesh) + for i in range(self.nnodes): + self.x[i] = self.xstart + i * self.dx + self.y[i] = self.ym + + # Compute cell-center coordinates as midpoint of adjacent nodes + for i in range(self.ncells): + self.xcc[i] = 0.5 * (self.x[i] + self.x[i+1]) + self.ycc[i] = 0.5 * (self.y[i] + self.y[i+1]) + def printinfo(self, prefix="Mesh"): + """Print detailed mesh parameters and coordinates""" + print(f"{prefix} ncells = {self.ncells}") + print(f"{prefix} nnodes = {self.nnodes}") + print(f"{prefix} xstart = {self.xstart:.6f}") + if self.lr is not None: + print(f"{prefix} lr = {self.lr}") + print(f"{prefix} dx = {self.dx:.6f}") + print(f"{prefix} x coordinates = {self.x}") + print(f"{prefix} cell-center x coordinates = {self.xcc}") + + def plot_boundary_vertical_interface_lines(self): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + ipoints = [0,self.nnodes-1] + for i in ipoints: + xm = self.x[i] + ym = self.y[i] + #plt.plot([xm, xm], [ym - 3*dy, ym + 3*dy], 'k-', linewidth=1) + lr = "L" if i == 0 else "R" + plot_vertical_boundary(xm,ym - 3*dy,ym + 3*dy, lr) + def plot_vertical_interface_lines(self, indices=None): + """Plot vertical lines at all mesh node positions (cell boundaries)""" + dy = 0.1 * abs(self.dx) # Absolute value ensures positive vertical line length + if indices is None: + indices = [i for i in range(0, self.nnodes)] + for i in indices: + xm = self.x[i] + ym = self.y[i] + plt.plot([xm, xm], [ym - dy, ym + dy], 'k-', linewidth=1) + +class Ghost(BaseMesh): + """Ghost mesh class with cell labeling and visualization""" + def __init__(self, xstart, dx, ncells, ishift, ym, lr): + # Inherit initialization logic from BaseMesh class + super().__init__(ncells=ncells, xstart=xstart, dx=dx, ishift=ishift, ym=ym, lr=lr) + + def plot_cell_label(self): + ytext_shift = 0.5*abs(self.dx) # Y-position for labels (avoid overlap with main mesh) + print(f"self.ishift={self.ishift}") + for i in range(self.ncells): + # Define cell labels based on left/right ghost mesh type + if self.lr == "L": + cell_label = f"${- i+self.ishift}$" # Label format for left ghost cells + else: + ii = i+1+self.ishift + if ii == 0: + cell_label = f"$N$" + else: + cell_label = f"$N+{i+1+self.ishift}$" # Label format for right ghost cells + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-ytext_shift, cell_label, fontsize=12, ha='center') + + def plot(self): + """Visualize ghost mesh: cell-center points and cell index labels""" + self.plot_cell_label() + # Plot cell-center points (red fill with black edge) + plt.scatter(self.xcc, self.ycc, s=50, facecolor='red', edgecolor='black', linewidth=1) + + indices = [i for i in range(1, self.nnodes)] + self.plot_vertical_interface_lines(indices) + +class Mesh(BaseMesh): + """Main mesh class with ghost mesh generation and management""" + def __init__(self, nghosts=2,ishift=0, ym=0, periodic=False): # Added: periodic parameter, default False + self.nghosts = nghosts # Number of ghost cell layers on each side + self.ym = ym + self.periodic = periodic # Added: periodic flag + self.xmin = 0.0 # Left physical boundary of main mesh + self.xmax = 1.0 # Right physical boundary of main mesh + self.ncells = 2*self.nghosts+3 # Number of cells in main mesh + self.dx = (self.xmax - self.xmin) / self.ncells # Grid spacing of main mesh + self.nnodes = self.ncells + 1 # Number of nodes in main mesh + # Initialize main mesh using BaseMesh constructor + super().__init__(ncells=self.ncells, xstart=self.xmin, dx=self.dx, ishift=ishift, ym=self.ym, lr=None) + print(f"Mesh self.ishift={self.ishift}") + print(f"Mesh self.ncells={self.ncells}") + + # Create left ghost mesh (mirror extension to the left of main mesh) + self.ghost_mesh_left = Ghost( + xstart=self.xmin, + dx=-self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="L" + ) + + # Create right ghost mesh (mirror extension to the right of main mesh) + self.ghost_mesh_right = Ghost( + xstart=self.xmax, + dx=self.dx, + ncells=self.nghosts, + ishift=ishift, + ym=self.ym, + lr="R" + ) + + self.generate_total_mesh() + + # Print mesh information for verification + self.printinfo() + # Render all mesh components + self.plot() + + def generate_total_mesh(self): + self.generate_mesh() + self.ghost_mesh_left.generate_mesh() + self.ghost_mesh_right.generate_mesh() + def printinfo(self): + """Print main mesh and ghost mesh information""" + super().printinfo(prefix="Main Mesh") + self.ghost_mesh_left.printinfo(prefix="Left Ghost Mesh") + self.ghost_mesh_right.printinfo(prefix="Right Ghost Mesh") + + def getstringFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "" + str_a = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_a = rf"$x_{{{sign}\frac{str_a}}}$" + return str_a + + def getstringNFrac(self, a): + absa = abs(a) + sign = "-" if a < 0 else "+" + str_na = f"{{{absa.numerator}}}{{{absa.denominator}}}" + str_na = rf"$x_{{N{sign}\frac{str_na}}}$" + return str_na + + def get_label(self,strin,index): + absindex = abs(index) + sign = "-" if index < 0 else "+" + if index == 0: + #label = f"${strin}$" + label = f"{strin}" + else: + #label = f"${strin}{sign}{absindex}$" + label = f"{strin}{sign}{absindex}" + return label + + def get_dollar_label(self,strin,index): + return f"${self.get_label(strin,index)}$" + + def plot_cell_label(self): + dytext = 0.5*abs(self.dx) + self.nlabels = self.nghosts + for i in range(self.ncells): + if i < self.nlabels: + cell_label = f"${i+1+self.ishift}$" + elif i > self.ncells - 1 - self.nlabels: + inew = i - (self.ncells - 1) + self.ishift + cell_label = self.get_dollar_label("N",inew) + else: + cell_label="" + # Add text label at cell center + plt.text(self.xcc[i], self.ycc[i]-dytext, cell_label, fontsize=12, ha='center') + icenter = self.ncells // 2 + plt.text(self.xcc[icenter], self.ycc[icenter]-dytext, f"$i$", fontsize=12, ha='center') + cell_label = self.get_label("N",self.ishift+1) + #text = f"$ied-ist=N$" + #plt.text(self.xcc[icenter], self.ycc[icenter]-3*dytext, text, fontsize=12, ha='center') + xm = self.xcc[self.ncells-1] + self.dx + ym = self.ycc[self.ncells-1] + #plt.arrow(self.xcc[0], self.ycc[0]-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + #plt.arrow(xm, ym-2.5*dytext, 0, 1.0*dytext, head_width=0.01, head_length=0.01, fc='blue', ec='blue', linewidth=1) + text1 = f"$ist={1+self.ishift}$" + text2 = f"$ied={cell_label}$" + #plt.text(self.xcc[0], self.ycc[0]-3*dytext, text1, fontsize=12, ha='center') + #plt.text(xm, ym-3*dytext, text2, fontsize=12, ha='center') + def plot_cell_center(self): + # Plot main mesh cell-center points (black fill with black edge) + xcc_new = [] + ycc_new = [] + for i in range(self.ncells): + if self.cellmark[i] == 1: + xcc_new.append( self.xcc[i] ) + ycc_new.append( self.ycc[i] ) + plt.scatter(xcc_new, ycc_new, s=50, facecolor='black', edgecolor='black', linewidth=1) + + def plot_cell_mesh(self): + self.icenter = self.ncells // 2 + self.nlabels = self.nghosts + self.cellmark = np.zeros(self.ncells, dtype=int) + for i in range(self.ncells): + if i < self.nlabels: + self.cellmark[i] = 1 + elif i > self.ncells - 1 - self.nlabels: + self.cellmark[i] = 1 + self.cellmark[self.icenter] = 1 + + self.plot_cell_center() + self.plot_cell_label() + + # Plot horizontal line connecting main mesh nodes + for i in range(self.ncells): + if self.cellmark[i] == 1: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k-', linewidth=1) + else: + plt.plot([self.x[i], self.x[i+1]], [self.y[i], self.y[i+1]], 'k--', linewidth=1) + + + def plot(self): + """Complete visualization of main mesh and ghost meshes""" + + self.plot_cell_mesh() + # Plot vertical interface lines for main mesh + indices = [i for i in range(1, self.nnodes-1)] + self.plot_vertical_interface_lines(indices) + self.plot_boundary_vertical_interface_lines() + + # Add boundary labels for main mesh + dym = abs(self.dx) + plt.text(self.x[0], self.y[0]+0.5*dym, r'$x=a$', fontsize=12, ha='center') + plt.text(self.x[-1], self.y[0]+0.5*dym, r'$x=b$', fontsize=12, ha='center') + + half = Fraction(1,2) + a1 = half+self.ishift + a2 = half+self.ishift+1 + print(f"a1={a1}") + print(f"a2={a2}") + str_a1 = self.getstringFrac(a1) + str_a2 = self.getstringFrac(a2) + + an1 = half+self.ishift + an2 = -half+self.ishift + + str_na1 = self.getstringNFrac(an1) + str_na2 = self.getstringNFrac(an2) + + print(f"an1={an1}") + print(f"an2={an2}") + + print(f"str_na1={str_na1}") + print(f"str_na2={str_na2}") + + #plt.text(self.x[0], self.y[0]-dym, str_a1, fontsize=12, ha='center') + #plt.text(self.x[1], self.y[1]-dym, str_a2, fontsize=12, ha='center') + #plt.text(self.x[-1], self.y[-1]-dym, str_na1, fontsize=12, ha='center') + #plt.text(self.x[-2], self.y[-2]-dym, str_na2, fontsize=12, ha='center') + + # Plot ghost mesh components (points and labels) + self.ghost_mesh_left.plot() + self.ghost_mesh_right.plot() + # Added: If periodic=True, draw periodic connections + if self.periodic: + self.plot_periodic_connections() + + def plot_periodic_connections(self): + """Plot periodic boundary fold-line arrows from source cells to ghost cells, indicating value assignment""" + + vlen = 0.6 * abs(self.dx) # Vertical line length + offset_y = 0.05 * abs(self.dx) # Small offset from cell center to avoid overlap + lw = 2 # Line width + + color_cycle = cycle(['blue', 'purple', 'green', 'orange', 'red', 'cyan', 'magenta', 'yellow']) + + left_list = [] + right_list = [] + + icellmax = self.ncells - 1 + cindex = 0 + for i in range(self.nghosts): + left_list.append((icellmax-i, i, next(color_cycle))) + + for i in range(self.nghosts): + right_list.append((i, i, next(color_cycle))) + + print(f"left_list={left_list}") + print(f"right_list={right_list}") + + for i in range(len(left_list)): + isrc, itgt, color = left_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_left.xcc[itgt], self.ghost_mesh_left.ycc[itgt] + + dy = i * vlen + end_y_src = src_y + vlen + offset_y + dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + for i in range(len(right_list)): + isrc, itgt, color = right_list[i] + src_x, src_y = self.xcc[isrc], self.ycc[isrc] + tgt_x, tgt_y = self.ghost_mesh_right.xcc[itgt], self.ghost_mesh_right.ycc[itgt] + + dy = i * vlen + #end_y_src = src_y + vlen + offset_y + dy + end_y_src = src_y - vlen - offset_y - dy + + xp = [] + yp = [] + + xp.append( src_x ) + xp.append( src_x ) + xp.append( tgt_x ) + xp.append( tgt_x ) + + yp.append( src_y ) + yp.append( end_y_src ) + yp.append( end_y_src ) + yp.append( tgt_y ) + + draw_periodic_connections_by_points(xp,yp,color) + + +def plot_cfd_figure(periodic=False): # Added: periodic parameter, default False + """Generate and save CFD mesh visualization figure""" + # Configure LaTeX for text rendering and font settings + plt.rc('text', usetex=True) + plt.rc('font', family='serif', serif=['Times New Roman']) + + # Create figure with fixed dimensions + plt.figure(figsize=(12, 8)) + # Initialize main mesh and generate grid coordinates + #nghost2 = 2 + nghost3 = 3 + #mesh = Mesh(nghost3,nghost3-1, periodic=periodic) # Pass periodic + mesh = Mesh(nghost3,-1, periodic=periodic) # Pass periodic + + # Set axis limits for symmetric display + plt.xlim(-1.5, 1.5) + plt.ylim(-1, 1) + + # Set equal axis scale and hide axis lines + plt.axis('equal') + plt.axis('off') + + # Save figure with high resolution and tight bounding box + filename = 'cfd_periodic.png' if periodic else 'cfd.png' + plt.savefig(filename, bbox_inches='tight', dpi=300) + # Display the figure + plt.show() + +if __name__ == '__main__': + plot_cfd_figure(periodic=True) # Example: Enable periodic visualization \ No newline at end of file diff --git a/example/figure/1d/weno/LinearWeights/01/xi.py b/example/figure/1d/weno/LinearWeights/01/xi.py new file mode 100644 index 00000000..1b3fcb25 --- /dev/null +++ b/example/figure/1d/weno/LinearWeights/01/xi.py @@ -0,0 +1,542 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +if __name__ == "__main__": + maxk = 4 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01/testprj.py b/example/figure/1d/weno/interplate/0st/01/testprj.py new file mode 100644 index 00000000..4d4ad9ae --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01/testprj.py @@ -0,0 +1,120 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # x方向宽度(英寸) +fig_height = 8.0 # y方向高度(英寸),随意改实现任意长宽比 +# 示例:20x8、18x6、24x9 等都完美自适应不交叉 + +k = 3 +# ========================================================================= + +# === 缩放因子(以 16×8 为基准)=== +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +# === 水平尺寸(图形略微放大 → 边距自然极小)=== +visual_cell_width = fig_width / 6.9 # 6.9 → 边距更接近 Word “窄”边距(≈0.4~0.6cm) +center_x = 3.0 * visual_cell_width # 完美居中(左2.5 + 右3.5 的平均) + +# === 垂直尺寸 === +base_dyref = 1.85 +rv, sv = [], [] +kk = k - 1 +for m in range(0, k + 1): + s_val = m + r_val = kk - s_val + rv.append(r_val) + sv.append(s_val) +num_rows = len(rv) + +dyref = base_dyref * scale_y * (4.0 / max(num_rows, 1)) +vertical_unit = dyref * 0.54 + +def plot_cell_center_rs(yref, r, s): + ms = list(range(-r, s + 1)) + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.25 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r, s): + ms = list(range(-r, s + 1)) + if not ms: + return + + y_vertex = yref + 0.25 * vertical_unit + y_cell = yref - 0.68 * vertical_unit + y_rs = yref - 1.05 * vertical_unit + + # vertex 标签 + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # cell 标签 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + + # (r,s) + shift = (-r + s) / 2.0 + xc = center_x + shift * visual_cell_width + plt.text(xc, y_rs, rf'$(r={r},\;s={s})$', fontsize=15*scale_y, ha='center', + color='darkblue', weight='bold') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +for i in range(num_rows): + yref = -i * dyref + r = rv[i] + s = sv[i] + plot_cell_center_rs(yref, r, s) + plot_mesh_rs(yref, r, s) + plot_label_rs(yref, r, s) + +# === 极小边距(真正像 Word “窄”边距)=== +margin_x = 0.12 * visual_cell_width # 左右 ≈0.3~0.5cm +margin_y = 0.25 * vertical_unit # 上下极小(保证不裁剪最后一行的 (r,s)) + +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x +min_y = -(num_rows-1)*dyref - 1.3*vertical_unit - margin_y +max_y = 0 + 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') + +plt.savefig('cfd_stencil_word_narrow.png', bbox_inches='tight', pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01a/testprj.py b/example/figure/1d/weno/interplate/0st/01a/testprj.py new file mode 100644 index 00000000..1c23101c --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01a/testprj.py @@ -0,0 +1,72 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 图形宽度(英寸),比如 16、20、24 都行 +fig_height = 4.0 # 图形高度(英寸),单行建议 3~5 之间好看 +# ========================================================================= + +# 以 16×8 为基准的缩放(即使改成其他比例也完全自适应) +scale_x = fig_width / 16.0 +scale_y = fig_height / 8.0 + +# 水平布局:5 个点,视觉上等距,左右边距接近 Word “窄” +visual_cell_width = fig_width / 7.2 # 7.2 这个数让左右边距≈0.4~0.6cm(Word窄) +center_x = fig_width / 2 # 完美水平居中 + +# 垂直方向只需要一行,高度微调让标签不拥挤 +vertical_unit = 1.6 * scale_y # 控制标签与点的相对距离 + +# 主绘图函数(只画一行) +def plot_single_row(): + # 5 个网格点的位置:i-2, i-1, i, i+1, i+2 + offsets = [-2, -1, 0, 1, 2] + xs = [center_x + m * visual_cell_width for m in offsets] + + # 1. 画网格线(蓝色粗横线 + 黑色细竖线) + y_line = 0.0 + # 横线(覆盖整个 5 个 cell) + plt.hlines(y_line, xs[0] - 0.5*visual_cell_width, + xs[-1] + 0.5*visual_cell_width, + color='b', linewidth=2.8*scale_x) + + # 竖线(每个点左右各一条细黑线) + for x in xs: + plt.vlines(x, y_line - 0.25*vertical_unit, + y_line + 0.25*vertical_unit, + color='k', linewidth=1.8*scale_x) + + # 2. 画中心黑点 + plt.scatter(xs, [0]*5, s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + + # 3. 标签(cell 标签:i-2, i-1, i, i+1, i+2) + for m, x in zip(offsets, xs): + if m == 0: + plt.text(x, 0.68*vertical_unit, r'$i$', fontsize=16*scale_y, + ha='center', va='bottom', color='red', weight='bold') + elif m > 0: + plt.text(x, 0.68*vertical_unit, rf'$i+{m}$', fontsize=14*scale_y, + ha='center', va='bottom') + else: + plt.text(x, 0.68*vertical_unit, rf'$i{-m}$', fontsize=14*scale_y, + ha='center', va='bottom') + +# ========================== 绘图 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +fig = plt.figure(figsize=(fig_width, fig_height)) + +plot_single_row() + +# 极窄边距(真正像 Word “窄”) +margin_x = 0.12 * visual_cell_width +margin_y = 0.4 * vertical_unit +plt.xlim(center_x - 2.5*visual_cell_width - margin_x, + center_x + 2.5*visual_cell_width + margin_x) +plt.ylim(-1.2*vertical_unit - margin_y, 1.6*vertical_unit + margin_y) + +plt.axis('off') +plt.savefig('cfd_stencil_5point_single_row.png', bbox_inches='tight', + pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01b/testprj.py b/example/figure/1d/weno/interplate/0st/01b/testprj.py new file mode 100644 index 00000000..a6cb85c3 --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01b/testprj.py @@ -0,0 +1,95 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 随意改,18、20、24 都完美自适应 +fig_height = 4.0 # 单行建议 3.5~5,太高会空 +# ========================================================================= + +# 缩放(保持和原来完全一致的自适应能力) +scale_x = fig_width / 16.0 +scale_y = fig_height / 8.0 + +visual_cell_width = fig_width / 6.9 # 左右边距≈0.4~0.6cm(Word 窄) +center_x = 3.0 * visual_cell_width # 原始代码的完美居中方式(左2.5 + 右3.5 的中点) + +# 垂直方向单位(和原代码一致) +base_dyref = 1.85 +dyref = base_dyref * scale_y * (4.0 / 4) # 原来是按行数缩放,这里固定为单行 +vertical_unit = dyref * 0.54 +yref = 0.0 # 只画一行,固定在 y=0 + +# ========================== 绘图函数(严格复刻你原始逻辑) ========================== +def plot_cell_center_rs(yref, r=2, s=2): # 固定 r=2, s=2 → 对应 i-2 ~ i+2 + ms = list(range(-r, s + 1)) # [-2, -1, 0, 1, 2] + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + if not ms: + return + dy = 0.25 * vertical_unit + # 竖线:位于 face 位置(m-0.5 和 m+0.5) + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + # 蓝色粗横线:每个 cell 内部 + for m in ms: + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + y_vertex = yref + 0.25 * vertical_unit # x_{i±1/2} 标签位置 + y_cell = yref - 0.68 * vertical_unit # i, i±1 标签位置 + + # 1. face 标签:x_{i-1/2}, x_{i+1/2}, ... + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) # |v| = 0.5,1.5,2.5 → 1,3,5 → 1/2, 3/2, 5/2 + sign = '+' if v > 0 else ('-' if v < 0 else '') + if frac % 2 == 1: # 奇数 → x_{i±1/2}, x_{i±3/2}, ... + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + else: + label = rf'$x_{{i{sign}{frac//2}}}$' # 暂时不会出现整数,但保留 + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=14*scale_y, ha='center', va='bottom') + + # 2. cell 标签:i-2, i-1, i, i+1, i+2(写在 cell 中心) + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=16*scale_y, ha='center', + color='red', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=14*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=14*scale_y, ha='center') + +# ========================== 主程序 ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +plot_cell_center_rs(yref) +plot_mesh_rs(yref) +plot_label_rs(yref) + +# 极窄边距(和原来完全一致) +margin_x = 0.12 * visual_cell_width +margin_y = 0.25 * vertical_unit +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 2.5 * visual_cell_width + margin_x # 原来右边是3.5,这里对称改为2.5(5点更紧凑美观) +min_y = -1.3*vertical_unit - margin_y +max_y = 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') +plt.savefig('cfd_5point_stencil_standard.png', bbox_inches='tight', pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01c/testprj.py b/example/figure/1d/weno/interplate/0st/01c/testprj.py new file mode 100644 index 00000000..4b8559bd --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01c/testprj.py @@ -0,0 +1,92 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 随意改宽度 +fig_height = 4.0 # 单行建议 3.8~4.5,最好别低于 3.5 +# ========================================================================= + +# 完全复刻你原始代码的缩放与布局参数(一点都没改!) +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +visual_cell_width = fig_width / 6.9 # 原始值,保证 Word 窄边距 +center_x = 3.0 * visual_cell_width # 原始完美居中(左2.5 + 右3.5 的中点) + +ffsize = 30 + +base_dyref = 1.85 +dyref = base_dyref * scale_y * (4.0 / 4) # 原始公式,这里固定 4 使单行间距与原来一行完全一致 +vertical_unit = dyref * 0.54 # 完全不变 +yref = 0.0 # 只画一行 + +# ========================== 绘图函数(100% 复刻原始逻辑) ========================== +def plot_cell_center_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) # [-2,-1,0,1,2] + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + dy = 0.1 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: # 画黑色细竖线(face位置) + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: # 画蓝色粗横线(每个cell) + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + y_vertex = yref + 0.2 * vertical_unit # x_{i±1/2} 在竖线上方,和原来完全一样 + y_cell = yref - 0.25 * vertical_unit # i, i±1 在点下方,和原来完全一样 + + # 1. face 标签 x_{i-3/2}, x_{i-1/2}, x_{i+1/2}, x_{i+3/2}, x_{i+5/2} + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) # 1,3,5 → 1/2,3/2,5/2 + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=ffsize*scale_y, ha='center', va='bottom') + + # 2. cell 标签 i-2, i-1, i, i+1, i+2 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=ffsize*scale_y, ha='center', + color='black', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=ffsize*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=ffsize*scale_y, ha='center') + +# ========================== 主程序(和原来一模一样) ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +plot_cell_center_rs(yref) +plot_mesh_rs(yref) +plot_label_rs(yref) + +# 边距也完全复刻原始代码(极窄,插入Word完美) +margin_x = 0.12 * visual_cell_width +margin_y = 0.25 * vertical_unit +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x # 保持原始左右不对称(右边多留一点,和原来一致) +min_y = -1.3*vertical_unit - margin_y +max_y = 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') +plt.savefig('cfd_5point_perfect_same_as_original.png', bbox_inches='tight', + pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/0st/01d/testprj.py b/example/figure/1d/weno/interplate/0st/01d/testprj.py new file mode 100644 index 00000000..e7cccf3f --- /dev/null +++ b/example/figure/1d/weno/interplate/0st/01d/testprj.py @@ -0,0 +1,101 @@ +import numpy as np +import matplotlib.pyplot as plt + +# ========================== 可调参数(只改这里!) ========================== +fig_width = 16.0 # 随意改宽度 +fig_height = 4.0 # 单行建议 3.8~4.5,最好别低于 3.5 +# ========================================================================= + +# 完全复刻你原始代码的缩放与布局参数(一点都没改!) +base_width = 16.0 +base_height = 8.0 +scale_x = fig_width / base_width +scale_y = fig_height / base_height + +visual_cell_width = fig_width / 6.9 # 原始值,保证 Word 窄边距 +center_x = 3.0 * visual_cell_width # 原始完美居中(左2.5 + 右3.5 的中点) + +ffsize = 30 + +base_dyref = 1.85 +dyref = base_dyref * scale_y * (4.0 / 4) # 原始公式,这里固定 4 使单行间距与原来一行完全一致 +vertical_unit = dyref * 0.54 # 完全不变 +yref = 0.0 # 只画一行 + +# ========================== 绘图函数(100% 复刻原始逻辑) ========================== +def plot_cell_center_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) # [-2,-1,0,1,2] + xs = [center_x + m * visual_cell_width for m in ms] + plt.scatter(xs, np.full_like(xs, yref), s=140*scale_x**2, + facecolor='black', edgecolor='black', linewidth=1.2*scale_x) + +def plot_mesh_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + dy = 0.1 * vertical_unit + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: # 画黑色细竖线(face位置) + xv = center_x + v * visual_cell_width + plt.plot([xv, xv], [yref - dy, yref + dy], 'k-', linewidth=1.8*scale_x) + + for m in ms: # 画蓝色粗横线(每个cell) + left = center_x + (m - 0.5) * visual_cell_width + right = center_x + (m + 0.5) * visual_cell_width + plt.plot([left, right], [yref, yref], 'b-', linewidth=2.8*scale_x) + +def plot_label_rs(yref, r=2, s=2): + ms = list(range(-r, s + 1)) + y_vertex = yref + 0.2 * vertical_unit # x_{i±1/2} 在竖线上方,和原来完全一样 + y_cell = yref - 0.25 * vertical_unit # i, i±1 在点下方,和原来完全一样 + y_xi = yref - 0.3 * vertical_unit + y_xidef = yref - 0.5 * vertical_unit + + # 1. face 标签 x_{i-3/2}, x_{i-1/2}, x_{i+1/2}, x_{i+3/2}, x_{i+5/2} + v_rels = sorted(set(m - 0.5 for m in ms) | set(m + 0.5 for m in ms)) + for v in v_rels: + frac = int(round(abs(v) * 2)) # 1,3,5 → 1/2,3/2,5/2 + sign = '+' if v > 0 else '-' + label = rf'$x_{{i{sign}\frac{{{frac}}}{{2}}}}$' + xv = center_x + v * visual_cell_width + plt.text(xv, y_vertex, label, fontsize=ffsize*scale_y, ha='center', va='bottom') + + label_xi = rf'$\xi={sign}\frac{{{frac}}}{{2}}$' + plt.text(xv, y_xi, label_xi, fontsize=ffsize*scale_y, ha='center', va='bottom') + + # 2. cell 标签 i-2, i-1, i, i+1, i+2 + for m in ms: + xc = center_x + m * visual_cell_width + if m == 0: + plt.text(xc, y_cell, r'$i$', fontsize=ffsize*scale_y, ha='center', + color='black', weight='bold') + elif m > 0: + plt.text(xc, y_cell, rf'$i+{m}$', fontsize=ffsize*scale_y, ha='center') + else: + plt.text(xc, y_cell, rf'$i-{-m}$', fontsize=ffsize*scale_y, ha='center') + + plt.text(center_x, y_xidef, r'$\xi=\frac{x-x_{i}}{\Delta x}$', + fontsize=ffsize*scale_y, ha='center',color='black', weight='bold') + + +# ========================== 主程序(和原来一模一样) ========================== +plt.rc('text', usetex=True) +plt.rc('font', family='serif', serif=['Times New Roman']) +plt.figure(figsize=(fig_width, fig_height)) + +plot_cell_center_rs(yref) +plot_mesh_rs(yref) +plot_label_rs(yref) + +# 边距也完全复刻原始代码(极窄,插入Word完美) +margin_x = 0.12 * visual_cell_width +margin_y = 0.25 * vertical_unit +min_x = center_x - 2.5 * visual_cell_width - margin_x +max_x = center_x + 3.5 * visual_cell_width + margin_x # 保持原始左右不对称(右边多留一点,和原来一致) +min_y = -1.3*vertical_unit - margin_y +max_y = 0.4*vertical_unit + margin_y + +plt.xlim(min_x, max_x) +plt.ylim(min_y, max_y) +plt.axis('off') +plt.savefig('cfd_5point_perfect_same_as_original.png', bbox_inches='tight', + pad_inches=0.02, dpi=400) +plt.show() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py new file mode 100644 index 00000000..3a85b1ca --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01/compute_integral.py @@ -0,0 +1,271 @@ +import numpy as np +from typing import Tuple, Optional +from functools import lru_cache + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """ + 计算∫_{α}^{β} ξ^power dξ的精确值 + + 数学公式: + power = 0: ∫ 1 dξ = β - α + power ≥ 1: ∫ ξ^power dξ = (β^(power+1) - α^(power+1)) / (power+1) + + 参数: + alpha: 积分下限 + beta: 积分上限 + power: 非负整数幂次 + + 返回: + 积分值(浮点数) + + 示例: + >>> compute_integral(-0.5, 0.5, 0) + 1.0 + >>> compute_integral(-0.5, 0.5, 1) + 0.0 # 奇函数对称区间 + >>> compute_integral(-0.5, 0.5, 2) + 0.08333333333333333 + """ + if power < 0: + raise ValueError(f"幂次必须为非负整数,但得到{power}") + + # 特殊情况:power=0时,积分值为β-α + if power == 0: + return beta - alpha + + # 一般情况:使用解析解 + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + + +def compute_alpha_beta(i: int, r: int) -> Tuple[float, float]: + """ + 计算第i行的积分区间[α_i, β_i] + + 公式: + α_i = -r + i - 1/2 + β_i = -r + i + 1/2 + + 参数: + i: 行索引 (0 ≤ i < k) + r: 给定的参数值 + + 返回: + (α_i, β_i) 元组 + """ + offset = -r + i + alpha = offset - 0.5 + beta = offset + 0.5 + return alpha, beta + + +def compute_matrix_M(k: int, r: int, vectorized: bool = True) -> np.ndarray: + """ + 计算k×k的矩阵M + + 矩阵M的定义: + M[i][j] = ∫_{α_i}^{β_i} ξ^j dξ, 其中 i,j = 0,...,k-1 + + 参数说明: + k: 矩阵维度(正整数) + r: 参数值(0 ≤ r < k) + vectorized: 是否使用向量化计算(默认True,更快) + + 返回: + k×k的NumPy数组 + + 示例: + >>> compute_matrix_M(3, 1) + array([[1. , 0. , 0.08333333], + [1. , 1. , 0.75 ], + [1. , 2. , 2.08333333]]) + """ + # 参数验证 + if not isinstance(k, int) or k <= 0: + raise ValueError(f"k必须是正整数,但得到{k}") + if not (0 <= r < k): + raise ValueError(f"r必须在[0, {k-1}]范围内,但得到{r}") + + # 使用向量化计算(推荐,速度快) + if vectorized: + return _compute_matrix_M_vectorized(k, r) + + # 或使用循环计算(更直观,但较慢) + return _compute_matrix_M_loop(k, r) + + +def _compute_matrix_M_loop(k: int, r: int) -> np.ndarray: + """循环版本(易于理解)""" + M = np.zeros((k, k)) + + # 逐行计算 + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + + # 逐列计算 + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + + return M + + +@lru_cache(maxsize=32) +def _compute_matrix_M_vectorized(k: int, r: int) -> np.ndarray: + """ + 向量化版本(性能最优) + + 利用NumPy广播机制,一次性计算所有元素 + 缓存结果:相同(k,r)重复调用时直接返回缓存 + """ + # 创建行索引数组 [0, 1, ..., k-1] + i = np.arange(k) + + # 计算所有α_i和β_i + # α_i = -r + i - 0.5 + # β_i = -r + i + 0.5 + alpha = -r + i - 0.5 # shape: (k,) + beta = -r + i + 0.5 # shape: (k,) + + # 创建列索引数组(幂次+1) [1, 2, ..., k] + # 因为积分公式需要 j+1 + j = np.arange(1, k + 1) # shape: (k,) + + # 计算α_i^{j+1}和β_i^{j+1} + # alpha[:, None] shape: (k, 1) + # j shape: (k,) + # 广播后结果shape: (k, k) + alpha_pow = alpha[:, None] ** j # α_i^{j+1} + beta_pow = beta[:, None] ** j # β_i^{j+1} + + # 计算积分值矩阵 + # M[i,j] = (β_i^{j+1} - α_i^{j+1}) / (j+1) + M = (beta_pow - alpha_pow) / j + + return M + + +def format_matrix(M: np.ndarray, precision: int = 6) -> str: + """ + 美化打印矩阵 + + 参数: + M: 矩阵 + precision: 小数点后保留位数 + + 返回: + 格式化字符串 + """ + return np.array2string( + M, + precision=precision, + suppress_small=True, + max_line_width=100 + ) + + +# ============= 测试和演示 ============= + +def demonstrate_matrix_properties(k: int = 4, r: int = 1): + """ + 演示矩阵M的性质 + + 性质1: 当r固定时,M[i+1][j] = M[i][j] + 偏移量 + 性质2: 每行第一个元素 M[i][0] = β_i - α_i = 1(恒定) + """ + print("="*60) + print(f"矩阵M性质演示 (k={k}, r={r})") + print("="*60) + + M = compute_matrix_M(k, r) + print(f"矩阵M:\n{format_matrix(M)}\n") + + # 验证性质2 + print("验证性质:每行第一个元素 = β_i - α_i = 1") + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + print(f" 第{i}行: M[{i}][0] = {M[i,0]:.6f}, β_i-α_i = {beta-alpha:.6f}") + + # 验证性质1(展示偏移模式) + print(f"\n验证性质:相邻行之间的差异模式") + for j in range(min(3, k)): # 只看前3列 + print(f" 第{j}列差分: {M[1:, j] - M[:-1, j]}") + + # 显示区间信息 + print(f"\n积分区间详情:") + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + print(f" 第{i}行: [α_{i}, β_{i}] = [{alpha:6.2f}, {beta:6.2f}]") + + +def compare_performance(k_values: List[int] = [10, 50, 100]): + """ + 性能对比:循环版 vs 向量化版 + + 参数: + k_values: 要测试的k值列表 + """ + print("\n" + "="*60) + print("性能对比测试") + print("="*60) + + import time + + for k in k_values: + print(f"\nk = {k}:") + + # 测试循环版本 + start = time.time() + M_loop = compute_matrix_M(k, r=0, vectorized=False) + time_loop = time.time() - start + + # 测试向量化版本 + start = time.time() + M_vec = compute_matrix_M(k, r=0, vectorized=True) + time_vec = time.time() - start + + print(f" 循环版本: {time_loop:.6f} 秒") + print(f" 向量化版: {time_vec:.6f} 秒") + print(f" 速度提升: {time_loop / time_vec:.2f} 倍") + + # 验证结果是否一致 + if np.allclose(M_loop, M_vec): + print(f" 结果一致性: ✓ 通过") + else: + print(f" 结果一致性: ✗ 失败!") + + +def test_special_cases(): + """ + 测试特殊k和r值 + """ + print("\n" + "="*60) + print("特殊值测试") + print("="*60) + + # 测试k=1(最小维度) + print("\n测试k=1, r=0:") + M = compute_matrix_M(1, 0) + print(f"矩阵M: {M}") + + # 测试r=0(区间左端点从-0.5开始) + print("\n测试k=3, r=0:") + M = compute_matrix_M(3, 0) + print(f"矩阵M:\n{format_matrix(M)}") + print("区间: [-0.5, 0.5], [0.5, 1.5], [1.5, 2.5]") + + +if __name__ == "__main__": + # 基础演示 + demonstrate_matrix_properties(k=4, r=1) + + # 性能测试 + compare_performance([10, 50, 100, 200]) + + # 特殊值测试 + test_special_cases() + + # 生成特定矩阵 + print("\n" + "="*60) + print("生成k=5, r=2的矩阵:") + print("="*60) + M = compute_matrix_M(5, 2) + print(format_matrix(M)) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py new file mode 100644 index 00000000..3e0c42f5 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01a/compute_integral.py @@ -0,0 +1,115 @@ +import numpy as np +from typing import Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """ + 计算∫_{α}^{β} ξ^power dξ的精确值 + """ + if power < 0: + raise ValueError(f"幂次必须为非负整数,但得到 {power}") + + if power == 0: + return beta - alpha + + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """ + 计算第row_index行的积分区间[α, β] + """ + middle = -r + row_index + alpha = middle - 0.5 + beta = middle + 0.5 + + return alpha, beta + + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """ + 计算k×k的矩阵M + + 矩阵定义: M[i][j] = ∫_{α_i}^{β_i} ξ^j dξ + """ + print(f"\n开始计算矩阵M (k={k}, r={r})...") + print("=" * 50) + + # 创建全零矩阵 + M = np.zeros((k, k), dtype=float) + + # 逐行计算 + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + + print(f"\n第 {i} 行: 区间 [{alpha:.2f}, {beta:.2f}]") + print("-" * 40) + + # 逐列计算 + for j in range(k): + value = compute_integral(alpha, beta, j) + M[i, j] = value + + print(f" M[{i}][{j}] = ∫_{alpha:.2f}^{beta:.2f} ξ^{j} dξ = {value:.6f}") + + print(f" 第{i}行完成: {M[i, :]}") + + print("\n" + "=" * 50) + print("矩阵计算完成!") + + return M + + +def print_matrix_nicely(M: np.ndarray, r: int, precision: int = 4): + """ + 美化打印矩阵 + """ + k = M.shape[0] + + print(f"\n{'='*60}") + print(f"最终矩阵M (k={k}, r={r})") + print(f"{'='*60}\n") + + # ✅ 修复:suppress_small 改为 suppress + np.set_printoptions(precision=precision, suppress=True, linewidth=120) + + print("矩阵数值:") + print(M) + print() + + # 打印区间信息 + print("积分区间详情:") + print(f"{'行号 i':<8} {'α_i':<10} {'β_i':<10} {'宽度 β_i-α_i':<15}") + print("-" * 45) + + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + width = beta - alpha + print(f"{i:<8} {alpha:<10.2f} {beta:<10.2f} {width:<15.2f}") + + # 验证第一列 + print(f"\n验证:每行第一列M[i][0] = β_i - α_i = 1.0") + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + expected = beta - alpha + actual = M[i, 0] + status = "✓ 正确" if abs(actual - expected) < 1e-6 else "✗ 错误" + print(f" 第{i}行: M[{i}][0] = {actual:.4f}, 期望值 = {expected:.4f} {status}") + + +def demonstrate_3x3_example(): + """ + 演示3×3矩阵的例子,r=1 + """ + print("="*70) + print("演示:计算3×3矩阵M (r=1)") + print("="*70) + + # 计算3×3矩阵 + M = compute_matrix_M(k=3, r=1) + + # 美化打印 + print_matrix_nicely(M, r=1, precision=4) + + +if __name__ == "__main__": + demonstrate_3x3_example() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py new file mode 100644 index 00000000..23aad7d9 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01b/compute_integral.py @@ -0,0 +1,121 @@ +import numpy as np +from fractions import Fraction +from typing import Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M""" + M = np.zeros((k, k), dtype=float) + + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + + return M + +def matrix_to_fractions(M: np.ndarray) -> list: + """ + 将矩阵转换为Fraction格式,便于美观显示 + + 处理技巧: + - 分母不超过1000 + - 避免过大分数 + """ + fraction_matrix = [] + for row in M: + fraction_row = [] + for val in row: + # 将小数转换为分数,限制分母大小 + frac = Fraction(val).limit_denominator(1000) + fraction_row.append(frac) + fraction_matrix.append(fraction_row) + + return fraction_matrix + +def format_fraction_matrix(frac_matrix: list) -> str: + """格式化Fraction矩阵为字符串""" + lines = [] + for row in frac_matrix: + # 将每个Fraction转换为字符串,并统一宽度 + row_str = " ".join([f"{str(frac):>10}" for frac in row]) + lines.append(f"[ {row_str} ]") + + return "\n".join(lines) + +def format_float_matrix(M: np.ndarray, precision: int = 4) -> str: + """格式化浮点矩阵为字符串""" + # 设置numpy打印选项 + with np.printoptions(precision=precision, suppress=True, linewidth=100): + return str(M) + +def compute_and_display(k: int, r: int): + """ + 主函数:计算并显示矩阵M及其逆矩阵 + + 参数: + k: 矩阵维度 + r: 参数值 (0 ≤ r < k) + """ + print(f"\n{'='*70}") + print(f"k = {k}, r = {r} 的矩阵M 及其逆矩阵") + print(f"{'='*70}\n") + + # 1. 计算矩阵M + M = compute_matrix_M(k, r) + + # 2. 计算逆矩阵 + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + print("⚠️ 矩阵不可逆!") + return + + # 3. 转换为Fraction格式 + M_frac = matrix_to_fractions(M) + M_inv_frac = matrix_to_fractions(M_inv) + + # 4. 显示实数格式 + print("📊 实数格式:") + print("-" * 30) + print("矩阵 M:") + print(format_float_matrix(M, precision=6)) + print("\n逆矩阵 M⁻¹:") + print(format_float_matrix(M_inv, precision=6)) + + # 5. 显示Fraction格式(美观) + print(f"\n{'='*70}") + print("🎨 Fraction分数格式(美观):") + print("-" * 30) + print("矩阵 M:") + print(format_fraction_matrix(M_frac)) + print("\n逆矩阵 M⁻¹:") + print(format_fraction_matrix(M_inv_frac)) + + # 6. 验证 M × M⁻¹ = I + identity = M @ M_inv + print(f"\n{'='*70}") + print("✅ 验证 M × M⁻¹ = I (单位矩阵):") + print("-" * 30) + print(format_float_matrix(identity, precision=10)) + +# ==================== 运行示例 ==================== +if __name__ == "__main__": + # 示例1:3×3矩阵,r=0 + compute_and_display(k=3, r=0) + + # 示例2:3×3矩阵,r=1 + compute_and_display(k=3, r=1) + + # 示例3:4×4矩阵,r=2 + compute_and_display(k=4, r=2) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py new file mode 100644 index 00000000..5f556eee --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01c/compute_integral.py @@ -0,0 +1,173 @@ +import numpy as np +from fractions import Fraction +from typing import List, Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式 + + 参数: + k: 矩阵维度 + r: 参数值 (0 ≤ r < k) + i: 基础索引(用于构造v的下标) + + 返回: + a_coeffs: a0, a1, ..., a_{k-1}的符号表达式列表 + """ + # 1. 计算矩阵M + M = compute_matrix_M(k, r) + + # 2. 计算逆矩阵 + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + # 3. 构造符号向量v的下标 + # v_indices = [i-r+0, i-r+1, ..., i-r+k-1] + v_indices = [i - r + m for m in range(k)] + + # 4. 求解每个a_j + a_coeffs = [] + + for j in range(k): # j是a的下标 + # a_j = Σ(M_inv[j][m] * v_{i-r+m}) + terms = [] + + for m in range(k): # m是求和变量 + coeff = M_inv[j, m] # 数值系数 + + # 只处理非零系数 + if abs(coeff) > 1e-10: + # 格式化系数 + if abs(coeff - 1.0) < 1e-10: + term_str = f"v[{v_indices[m]}]" + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-v[{v_indices[m]}]" + else: + # 尝试转换为分数显示 + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + term_str = f"{frac.numerator}*v[{v_indices[m]}]" + else: + term_str = f"{frac}*v[{v_indices[m]}]" + + terms.append(term_str) + + # 合并项 + if not terms: + a_coeffs.append("0") + elif len(terms) == 1: + a_coeffs.append(terms[0]) + else: + a_coeffs.append(" + ".join(terms)) + + return a_coeffs + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[{i-r+0}], v[{i-r+1}], ..., v[{i-r+k-1}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" [a_0, a_1, ..., a_{k-1}]^T = M⁻¹ * [v[i-r+0], v[i-r+1], ..., v[i-r+k-1]]^T") + +# ============= 示例:k=3 ============= + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 # 基础索引 + + print("="*70) + print("示例:k=3, r=1, i=0") + print("="*70) + + # 计算矩阵M + M = compute_matrix_M(k, r) + print("\n步骤1: 计算矩阵M") + print("-" * 40) + print("M[i][j] = ∫_{α_i}^{β_i} ξ^j dξ") + print("\n其中区间:") + for row in range(k): + alpha, beta = compute_alpha_beta(row, r) + print(f" 第{row}行: α_{row}={alpha:.2f}, β_{row}={beta:.2f}") + + print("\n得到的矩阵M:") + print(M) + + # 计算逆矩阵 + M_inv = np.linalg.inv(M) + print("\n步骤2: 计算逆矩阵 M⁻¹") + print("-" * 40) + print(M_inv) + + # 验证 M * M⁻¹ = I + identity = M @ M_inv + print("\n验证 M × M⁻¹ = I:") + print(identity) + + # 求解系数 + print("\n步骤3: 求解 a = M⁻¹ v") + print("-" * 40) + print("符号向量 v = [v[i-r+0], v[i-r+1], v[i-r+2]]") + print(f" = [v[{0-r+0}], v[{0-r+1}], v[{0-r+2}]]") + print(f" = [v[{-r}], v[{-r+1}], v[{-r+2}]]") + + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 额外:展示r=0和r=2的对比 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: # r必须小于k + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + example_k_3() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py new file mode 100644 index 00000000..112a790f --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01d/compute_integral.py @@ -0,0 +1,136 @@ +import numpy as np +from fractions import Fraction +from typing import List, Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式(输出格式:v[i±offset]) + + 核心修改:将v[-1], v[0]改为v[i-1], v[i+0]的形式 + """ + # 计算矩阵M和逆矩阵 + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + + # 对每个系数a_j(j是a的下标) + for j in range(k): + terms = [] + + # 对求和项m(m是v的下标偏移) + for m in range(k): + coeff = M_inv[j, m] # 数值系数 + + if abs(coeff) > 1e-10: # 只处理非零系数 + # 计算v的下标相对于i的偏移量 + # v_index = i - r + m + # offset = v_index - i = m - r + offset = m - r # 这是核心!不再是具体的数值下标 + + # 根据偏移量格式化符号下标 + if offset == 0: + v_str = f"v[i]" # 偏移为0 + elif offset > 0: + v_str = f"v[i+{offset}]" # 正偏移 + else: # offset < 0 + v_str = f"v[i{offset}]" # 负偏移(offset自带负号) + + # 格式化系数 + if abs(coeff - 1.0) < 1e-10: + term_str = v_str # 系数为1,不显示 + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" # 系数为-1,只显示负号 + else: + # 分数格式化 + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 合并项 + if not terms: + a_coeffs.append("0") + else: + a_coeffs.append(" + ".join(terms)) + + return a_coeffs + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[i-r+0], v[i-r+1], ..., v[i-r+{k-1}]]") + if i == 0: # 简化显示 + print(f" = [v[-{r}], v[-{r}+1], ..., v[{k-1-r}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" [a_0, a_1, ..., a_{k-1}]^T = M⁻¹ * [v[i-r], v[i-r+1], ..., v[i-r+k-1]]^T") + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 + + print("="*70) + print("示例:k=3, r=1, i=0(输出格式为v[i±offset])") + print("="*70) + + # 求解系数 + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 对比不同r值 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + example_k_3() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py new file mode 100644 index 00000000..17020f43 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/01e/compute_integral.py @@ -0,0 +1,141 @@ +import numpy as np +from fractions import Fraction +from typing import List, Tuple + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式(输出格式:v[i±offset]) + + 关键改进:负系数前直接显示减号,不显示+ - + """ + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + + # 对每个系数a_j + for j in range(k): + terms = [] + + # 对求和项m + for m in range(k): + coeff = M_inv[j, m] + + if abs(coeff) > 1e-10: # 非零系数 + # 计算相对偏移量 + offset = m - r + + # 格式化符号下标 + if offset == 0: + v_str = "v[i]" + elif offset > 0: + v_str = f"v[i+{offset}]" + else: # offset < 0 + v_str = f"v[i{offset}]" + + # 格式化系数部分 + if abs(coeff - 1.0) < 1e-10: + term_str = v_str # 系数为1 + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" # 系数为-1 + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 关键改进:智能连接各项,避免+ -问题 + if not terms: + a_coeffs.append("0") + else: + # 第一个项直接添加(保留其原始符号) + expr = terms[0] + + # 后续项根据符号智能连接 + for term in terms[1:]: + if term.startswith('-'): + # 负号项:直接加空格和项(负号自带) + expr += f" {term}" + else: + # 正号项:加 + 和项 + expr += f" + {term}" + + a_coeffs.append(expr) + + return a_coeffs + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[i-r+0], v[i-r+1], ..., v[i-r+{k-1}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" a = M⁻¹ * v") + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 + + print("="*70) + print("示例:k=3, r=1, i=0(修正符号连接)") + print("="*70) + + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 对比不同r值 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式(已修正)") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + example_k_3() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py new file mode 100644 index 00000000..2a577c09 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02/compute_integral.py @@ -0,0 +1,299 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict + + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """ + 求解系数a的符号表达式(输出格式:v[i±offset]) + + 关键改进:负系数前直接显示减号,不显示+ - + """ + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + + # 对每个系数a_j + for j in range(k): + terms = [] + + # 对求和项m + for m in range(k): + coeff = M_inv[j, m] + + if abs(coeff) > 1e-10: # 非零系数 + # 计算相对偏移量 + offset = m - r + + # 格式化符号下标 + if offset == 0: + v_str = "v[i]" + elif offset > 0: + v_str = f"v[i+{offset}]" + else: # offset < 0 + v_str = f"v[i{offset}]" + + # 格式化系数部分 + if abs(coeff - 1.0) < 1e-10: + term_str = v_str # 系数为1 + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" # 系数为-1 + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 关键改进:智能连接各项,避免+ -问题 + if not terms: + a_coeffs.append("0") + else: + # 第一个项直接添加(保留其原始符号) + expr = terms[0] + + # 后续项根据符号智能连接 + for term in terms[1:]: + if term.startswith('-'): + # 负号项:直接加空格和项(负号自带) + expr += f" {term}" + else: + # 正号项:加 + 和项 + expr += f" + {term}" + + a_coeffs.append(expr) + + return a_coeffs + +# ============ 新增:符号复合功能 ============ + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + a_coeffs: List[str]) -> str: + """ + 对多项式进行积分,并将a_j替换为v表达式 + + 参数: + polynomial: {指数: [(系数, [符号下标列表])]} + a_coeffs: a_j的v表达式列表 + + 返回: + 复合表达式字符串(如"1.0*(-1/2*v[i-1] - 1/2*v[i+1])^2") + """ + # 在[-0.5, 1/2]上积分 + a, b = -0.5, 0.5 + + # 用字典累积符号项的系数 + result_dict = defaultdict(float) + + for exp, expr_list in polynomial.items(): + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + + for coeff, symbols in expr_list: + # 生成符号键:如"a1*a1"或"a1*a2" + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" # 如"a1" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" # 如"a1^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) # 如"a1*a2" + + # 累加积分后的系数 + contribution = coeff * integral_factor + result_dict[symbol_key] += contribution + + # 构建复合表达式 + terms = [] + for symbol_key, total_coeff in result_dict.items(): + if abs(total_coeff) < 1e-10: + continue + + # 获取符号对应的a_j表达式 + # symbol_key如"a1^2" -> 需要找到a1的表达式 + base_symbol = symbol_key.split('*')[0].split('^')[0] # 提取"a1" + a_index = int(base_symbol[1:]) - 1 # a1 -> 索引0 + + if 0 <= a_index < len(a_coeffs): + a_expr = a_coeffs[a_index] + + # 构建项 + if '^2' in symbol_key: + # 平方项:用括号 + term = f"{total_coeff}*({a_expr})^2" + else: + # 一次项:直接用 + term = f"{total_coeff}*{a_expr}" + + terms.append(term) + + # 智能连接各项 + if not terms: + return "0" + + expr = terms[0] + for term in terms[1:]: + if term.startswith('-'): + expr += f" {term}" + else: + expr += f" + {term}" + + return expr + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """ + 生成所有r值的复合表达式f_r + + 参数: + k: 矩阵维度 + polynomial: 多项式(用a_j表示) + i: 基础索引 + + 返回: + f_r_dict: {r: 复合表达式字符串} + """ + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + # 1. 生成a系数的v表达式 + a_coeffs = solve_for_coefficients(k, r, i) + print("a系数的v表达式:") + for idx, expr in enumerate(a_coeffs): + print(f" a_{idx} = {expr}") + + # 2. 生成复合表达式f_r + f_r = evaluate_polynomial_integral_symbolic(polynomial, a_coeffs) + f_r_dict[r] = f_r + + print(f"\nf_{r} = ∫ P(x) dx (代入a系数后)") + print("-" * 50) + print(f"f_{r} = {f_r}") + + return f_r_dict + +# ============= 测试:用你的例子 ============= + +def test_your_example(): + """ + 测试你的例子:P1(x) = (1*a1^2)*x^0 + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + P2(x) = (4*a2^2)*x^0 + """ + k = 3 + + # 构建多项式(用a1, a2表示) + # 注意:a1对应索引1,a2对应索引2 + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2(来自P2) + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2(来自P1的平方项) + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式") + print("="*70) + + print("\n原始多项式(用a_j表示):") + # 简单打印多项式结构 + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp}: {' + '.join(term_strs)}") + + # 生成复合表达式 + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + + # 总结输出 + print("\n" + "="*70) + print("总结:所有r的复合表达式f_r") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} = {expr}") + +def format_coefficients(a_coeffs: List[str], k: int, r: int, i: int = 0): + """美化打印系数表达式""" + print(f"\n{'='*70}") + print(f"k={k}, r={r}, i={i} 的求解结果") + print(f"{'='*70}\n") + + print("矩阵M:") + M = compute_matrix_M(k, r) + print(M) + + print(f"\n逆矩阵 M⁻¹:") + M_inv = np.linalg.inv(M) + print(M_inv) + + print(f"\n符号向量 v:") + print(f"v = [v[i-r+0], v[i-r+1], ..., v[i-r+{k-1}]]") + + print(f"\n求解得到的系数 a:") + print("-" * 50) + for j, expr in enumerate(a_coeffs): + print(f" a_{j} = {expr}") + + print("\n向量形式:") + print(" a = M⁻¹ * v") + +def example_k_3(): + """k=3的完整示例""" + k = 3 + r = 1 + i = 0 + + print("="*70) + print("示例:k=3, r=1, i=0(修正符号连接)") + print("="*70) + + a_coeffs = solve_for_coefficients(k, r, i) + format_coefficients(a_coeffs, k, r, i) + + # 对比不同r值 + print("\n" + "="*70) + print("对比:r=0, 1, 2 的系数表达式(已修正)") + print("="*70) + + for test_r in [0, 1, 2]: + if test_r < k: + coeffs = solve_for_coefficients(k, test_r, i) + print(f"\nr={test_r}:") + for j, expr in enumerate(coeffs): + print(f" a_{j} = {expr}") + +if __name__ == "__main__": + #example_k_3() + test_your_example() + \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py new file mode 100644 index 00000000..67048cab --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02a/compute_integral.py @@ -0,0 +1,263 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """生成a系数的v表达式(已修正符号连接)""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + for j in range(k): + terms = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + + if abs(coeff - 1.0) < 1e-10: + term_str = v_str + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + # 智能连接 + if not terms: + a_coeffs.append("0") + else: + expr = terms[0] + for term in terms[1:]: + if term.startswith('-'): + expr += f" {term}" + else: + expr += f" + {term}" + a_coeffs.append(expr) + + return a_coeffs + +# ============ 核心改进:Fraction格式化 ============ + +def format_coefficient(frac: Fraction, force_display: bool = False) -> str: + """ + 智能格式化Fraction为字符串 + + 参数: + frac: Fraction对象 + force_display: 是否强制显示系数(即使为1) + + 返回: + 格式化后的系数字符串 + """ + # 处理零 + if frac == 0: + return "0" + + # 整数系数 + if frac.denominator == 1: + value = int(frac.numerator) + if value == 1 and not force_display: + return "" # 系数1,省略 + elif value == -1: + return "-" # 系数-1,只返回负号 + else: + return str(value) # 整数系数,如"3" + + # 分数系数 + return f"{frac.numerator}/{frac.denominator}" + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + a_coeffs: List[str]) -> str: + """ + 对多项式进行积分,并将a_j替换为v表达式 + + 关键修复:全程使用Fraction,避免浮点数 + """ + # ✅ 修复:使用Fraction表示积分限 + a = Fraction(-1, 2) # -1/2 + b = Fraction(1, 2) # 1/2 + + # 用字典累积符号项的系数(使用Fraction) + result_dict = defaultdict(lambda: Fraction(0, 1)) + + # 步骤1:积分并累加系数(使用Fraction) + for exp, expr_list in polynomial.items(): + # 计算积分因子:(b^(exp+1) - a^(exp+1))/(exp+1) + # 分子和分母都是Fraction + numerator = b**(exp + 1) - a**(exp + 1) # Fraction类型 + integral_factor = Fraction(numerator, exp + 1) # 构造函数接收Fraction和int + + for coeff, symbols in expr_list: + # 生成符号键 + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + # 累加分数系数 + # Fraction(coeff)将float转为Fraction(近似) + # 更精确的做法是:直接传入Fraction(str(coeff)) + contribution = Fraction(str(coeff)) * integral_factor + result_dict[symbol_key] += contribution + + # 步骤2:构建复合表达式 + terms = [] + for symbol_key, total_frac in result_dict.items(): + if total_frac == 0: + continue + + # 获取对应的a_j表达式 + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) - 1 + a_expr = a_coeffs[a_index] + + # 格式化系数(接收Fraction) + coeff_str = format_coefficient(total_frac) + + # 构建项 + if '^2' in symbol_key: + # 平方项:需要括号 + if coeff_str: + term = f"{coeff_str}*({a_expr})^2" + else: + term = f"({a_expr})^2" + else: + # 一次项:直接连接 + if coeff_str: + term = f"{coeff_str}*{a_expr}" + else: + term = f"{a_expr}" + + terms.append(term) + + # 步骤3:智能连接各项 + if not terms: + return "0" + + # 找到第一个非负项 + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +# ============ 测试函数 ============ + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式f_r(Fraction优化版)""" + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r(Fraction优化版)") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + # 1. 生成a系数的v表达式 + a_coeffs = solve_for_coefficients(k, r, i) + print("a系数的v表达式:") + for idx, expr in enumerate(a_coeffs): + print(f" a_{idx} = {expr}") + + # 2. 生成复合表达式f_r + f_r = evaluate_polynomial_integral_symbolic(polynomial, a_coeffs) + f_r_dict[r] = f_r + + print(f"\nf_{r}(Fraction格式):") + print("-" * 50) + # 美化输出:分行显示 + if '+' in f_r: + lines = f_r.split('+') + for line in lines: + print(f" {line.strip()}") + else: + print(f" {f_r}") + + # 总结 + print("\n" + "="*70) + print("总结:所有r的复合表达式f_r(Fraction格式)") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} = {expr}") + + return f_r_dict + +def test_composite(): + """测试你的例子""" + k = 3 + + # 多项式:a1^2 + 4*a1*a2*x + (4+4)*a2^2*x^2 + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2(系数4) + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2(系数4) + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(Fraction优化)") + print("="*70) + + print("\n原始多项式(用a_j表示):") + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp}: {' + '.join(term_strs)}") + + # 生成复合表达式 + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py new file mode 100644 index 00000000..19c9aac5 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02b/compute_integral.py @@ -0,0 +1,344 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def solve_for_coefficients(k: int, r: int, i: int = 0) -> List[str]: + """生成a系数的v表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs = [] + for j in range(k): + terms = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + + if abs(coeff - 1.0) < 1e-10: + term_str = v_str + elif abs(coeff + 1.0) < 1e-10: + term_str = f"-{v_str}" + else: + frac = Fraction(coeff).limit_denominator(1000) + term_str = f"{frac}*{v_str}" + + terms.append(term_str) + + if not terms: + a_coeffs.append("0") + else: + expr = terms[0] + for term in terms[1:]: + if term.startswith('-'): + expr += f" {term}" + else: + expr += f" + {term}" + a_coeffs.append(expr) + + return a_coeffs + +# ============ 核心改进:分离积分与代入 ============ + +def format_coefficient(frac: Fraction, force_display: bool = False) -> str: + """智能格式化Fraction为字符串""" + if frac == 0: + return "0" + + if frac.denominator == 1: + value = int(frac.numerator) + if value == 1 and not force_display: + return "" + elif value == -1: + return "-" + else: + return str(value) + + return f"{frac.numerator}/{frac.denominator}" + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """ + ✅ 第一步:对x在[-1/2, 1/2]上积分,消去x变量 + + 数学原理: + - 对x^0项:∫_{-1/2}^{1/2} coeff * a_j * x^0 dx = coeff * a_j * 1 + - 对x^1项:∫_{-1/2}^{1/2} coeff * a_j * x^1 dx = coeff * a_j * 0 + - 对x^2项:∫_{-1/2}^{1/2} coeff * a_j * x^2 dx = coeff * a_j * (1/12) + + 参数: + polynomial: {幂次: [(系数, [a索引,...]), ...]} + 例如: {0: [(1.0, [1,1]), (4.0, [2,2])], 1: [(4.0, [1,2])], 2: [(4.0, [2,2])]} + + 返回: + 积分后的项列表: [(分数系数, [a索引,...]), ...] + """ + # ✅ 积分限: [-1/2, 1/2] + a = Fraction(-1, 2) + b = Fraction(1, 2) + + integrated_terms = [] + + print("\n积分过程详细计算:") + print("-" * 50) + + # 遍历多项式的每个幂次项 + for exp, expr_list in polynomial.items(): + # ✅ 计算 ∫x^exp dx 在[-1/2, 1/2]上的值 + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + print(f" x^{exp} 积分因子: ∫ξ^{exp}dξ = {integral_factor}") + + # 对当前幂次下的每个a_j表达式项应用积分 + for coeff, symbols in expr_list: + # 将浮点系数转为精确分数 + coeff_frac = Fraction(str(coeff)) + + # 积分后的系数 = 原系数 × 积分因子 + new_coeff = coeff_frac * integral_factor + + # 生成符号表示 + if len(symbols) == 1: + symbol_str = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_str = f"a{symbols[0]}^2" + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + + print(f" {coeff_frac} * {symbol_str} × {integral_factor} = {new_coeff} * {symbol_str}") + + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs: List[str]) -> str: + """ + ✅ 第二步:将积分后的a_j表达式代入v表达式 + + 参数: + integrated_terms: 积分后项列表 [(系数, [a索引,...]), ...] + a_coeffs: a系数的v表达式列表 [a0_expr, a1_expr, a2_expr, ...] + + 返回: + 最终的复合表达式字符串 + """ + # 累积同类项(如所有a1^2项合并) + result_dict = defaultdict(lambda: Fraction(0, 1)) + + print("\n合并同类项:") + print("-" * 50) + + for coeff, symbols in integrated_terms: + # 生成唯一键用于合并 + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + # 累积系数 + result_dict[symbol_key] += coeff + + # 构建最终表达式 + terms = [] + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + print(f" {symbol_key}: 系数 = {total_coeff}") + + # ✅ 正确映射:多项式中的a1对应a_coeffs[1],a2对应a_coeffs[2] + base_symbol = symbol_key.split('*')[0].split('^')[0] # "a1"或"a2" + a_index = int(base_symbol[1:]) # a1→1,a2→2 + + # ✅ 安全检查 + if a_index >= len(a_coeffs): + raise ValueError(f"多项式索引{a_index}超出系数范围[0, {len(a_coeffs)-1}]") + + a_expr = a_coeffs[a_index] + coeff_str = format_coefficient(total_coeff) + + # 构建项字符串 + if '^2' in symbol_key: + # 平方项需要括号: coeff*(expr)^2 + term = f"{coeff_str}*({a_expr})^2" if coeff_str else f"({a_expr})^2" + else: + # 一次项: coeff*expr + term = f"{coeff_str}*{a_expr}" if coeff_str else f"{a_expr}" + + terms.append(term) + + return smart_join_terms(terms) + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项,处理符号""" + if not terms: + return "0" + + # 找到第一个非负项作为起点 + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + # 负号项前置 + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + # 正号项依次添加 + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + # 全为负号项 + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + a_coeffs: List[str]) -> str: + """ + ✅ 主函数:先积分,后代入 + + 步骤: + 1. 对x在[-1/2, 1/2]上积分,消去x变量 + 2. 将a_j替换为v表达式 + """ + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients(integrated_terms, a_coeffs) + +# ============ 测试函数 ============ + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式f_r""" + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + # 1. 生成a系数的v表达式 + a_coeffs = solve_for_coefficients(k, r, i) + print("\na系数的v表达式:") + for idx, expr in enumerate(a_coeffs): + print(f" a_{idx} = {expr}") + + # 2. 生成复合表达式f_r + f_r = evaluate_polynomial_integral_symbolic(polynomial, a_coeffs) + f_r_dict[r] = f_r + + print(f"\nf_{r}(最终复合表达式):") + print("-" * 50) + print_expression_pretty(f_r, indent=" ") + + # 总结 + print("\n" + "="*70) + print("总结:所有r的复合表达式f_r") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} =") + print_expression_pretty(expr, indent=" ") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = ""): + """美化打印表达式""" + if '+' not in expr: + print(f"{indent}{expr}") + return + + # 按顶层'+'分割 + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + # 打印 + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + # 多项式:(a1^2 + 4*a2^2) + 4*a1*a2*x + 4*a2^2*x^2 + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2(来自二阶导数部分) + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2(来自一阶导数平方的x^2项) + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(严格先积分后代入)") + print("="*70) + + print("\n原始多项式(含x幂次):") + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp} 项: {' + '.join(term_strs)}") + + # 生成复合表达式 + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py new file mode 100644 index 00000000..c82338c8 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02c/compute_integral.py @@ -0,0 +1,427 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +# ============ 核心改进:优化整数化与符号处理 ============ + +def format_coefficient(frac: Fraction) -> str: + """智能格式化Fraction为字符串""" + if frac == 0: + return "0" + if frac.denominator == 1: + value = int(frac.numerator) + return str(value) + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """ + ✅ 修复版:使用正则表达式安全解析表达式 + + 参数: + expr: 原始表达式,如 "-3/2*v[i] + 2*v[i+1] - 1/2*v[i+2]" + exponent: 指数(1为线性,2为平方) + + 返回: + (优化后表达式, 提取的因子) + """ + if expr == "0": + return "0", Fraction(1, 1) + + # 1. 如果表达式以正负号开头,添加隐含的+号 + if expr[0] not in '+-': + expr = '+' + expr + + # 2. 使用正则表达式安全解析表达式 + # 模式: ([+-]) 可选符号 + (系数) + (变量名) + # 系数支持: 整数、分数、带符号 + # 变量名: v[i] 或 v[i+1] 或 v[i-1] 等 + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + + matches = re.findall(pattern, expr) + + coeff_vars = [] # List of (Fraction系数, 变量字符串) + denominators = [] # 所有分母 + + for match in matches: + sign, coeff_part, var = match + + # 处理系数 + if coeff_part: + # 清理系数字符串 + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + # 没有显式系数,如 "+v[i]" + coeff = Fraction(1, 1) + + # 应用符号 + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + # 记录分母 + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + # 检查是否有任何项被解析 + if not coeff_vars: + print(f"警告:表达式 '{expr}' 未解析出任何项") + return expr, Fraction(1, 1) + + # 3. 计算最小公倍数 + lcm = compute_lcm(denominators) if denominators else 1 + + # 4. 转换为整数系数 + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + # 5. 符号优化:统计正负号数量 + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + # 如果负数多于正数,提取负号 + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + # 6. 根据指数调整因子 + factor_with_exponent = factor ** exponent + + # 7. 重建表达式字符串 + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: # coeff < 0 + expr_parts.append(f"{coeff}*{var}") + + # 拼接并清理开头 + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """ + ✅ 生成优化的a系数表达式(整数化+符号优化) + + 返回: [(优化后表达式, 提取因子), ...] + """ + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + # 构建原始表达式字符串 + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + # 优化表达式(exponent=1,因为是a_j本身) + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +# ============ 多项式积分与代入 ============ + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """ + ✅ 第一步:对x在[-1/2, 1/2]上积分 + """ + a = Fraction(-1, 2) + b = Fraction(1, 2) + + integrated_terms = [] + print("\n积分过程详细计算:") + print("-" * 50) + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + print(f" x^{exp} 在[-1/2,1/2]上的积分: {integral_factor}") + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + # 生成符号表示 + if len(symbols) == 1: + symbol_str = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_str = f"a{symbols[0]}^2" + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + + if new_coeff != 0: + print(f" {coeff_frac} * {symbol_str} × {integral_factor} = {new_coeff} * {symbol_str}") + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> str: + """ + ✅ 第二步:将积分后的a_j表达式代入v表达式(支持优化格式) + """ + result_dict = defaultdict(lambda: Fraction(0, 1)) + + print("\n合并同类项:") + print("-" * 50) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建最终表达式 + final_terms = [] + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + print(f" {symbol_key}: 总系数 = {total_coeff}") + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围[0, {len(a_coeffs_opt)-1}]") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + # ✅ 计算最终系数 + final_coeff = total_coeff * (a_factor ** exponent) + coeff_str = format_coefficient(final_coeff) + + # ✅ 构建最终项 + if is_squared: + if coeff_str == "1": + term = f"({a_expr})^2" + elif coeff_str == "-1": + term = f"-({a_expr})^2" + else: + term = f"{coeff_str}*({a_expr})^2" + else: + if coeff_str == "1": + term = f"{a_expr}" + elif coeff_str == "-1": + term = f"-{a_expr}" + else: + term = f"{coeff_str}*{a_expr}" + + final_terms.append(term) + + return smart_join_terms(final_terms) + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> str: + """ + ✅ 完整流程:积分 → 代入(支持优化) + """ + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + + print(f"\nr = {r} 的优化a系数:") + for idx, (expr, factor) in enumerate(a_coeffs_opt): + if factor == 1: + print(f" a_{idx} = ({expr})") + else: + print(f" a_{idx} = {format_coefficient(factor)} * ({expr})") + + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式f_r(完整优化版)""" + f_r_dict = {} + + print(f"\n生成k={k}的复合表达式f_r(整数化+符号优化)") + print("="*70) + + for r in range(k): + print(f"\n--- r = {r} ---") + + f_r = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = f_r + + print(f"\nf_{r}(最终优化表达式):") + print("-" * 50) + print_expression_pretty(f_r, indent=" ") + + # 总结 + print("\n" + "="*70) + print("总结:所有r的优化表达式f_r") + print("="*70) + for r, expr in f_r_dict.items(): + print(f"\nf_{r} =") + print_expression_pretty(expr, indent=" ") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = ""): + """美化打印表达式""" + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), # a1^2 + (4.0, [2, 2])], # a2^2 + 1: [(4.0, [1, 2])], # a1*a2 + 2: [(4.0, [2, 2])] # a2^2 + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + print("\n原始多项式(含x幂次):") + for exp, terms in polynomial.items(): + term_strs = [] + for coeff, symbols in terms: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + term_strs.append(f"{coeff}*a{symbols[0]}*a{symbols[1]}") + print(f" x^{exp} 项: {' + '.join(term_strs)}") + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py new file mode 100644 index 00000000..3e280a05 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02d/compute_integral.py @@ -0,0 +1,388 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction) -> str: + """智能格式化Fraction为字符串""" + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +# ============ 多项式积分与代入 ============ + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 修复:正确分离系数和主体,避免重复 + if '*(' in term_str: + # 分离系数和主体 + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.replace('v[i', 'v_{i').replace(']', '}') + latex_term = main_part + else: + # 无显式系数 + latex_term = term_str.replace('v[i', 'v_{i').replace(']', '}') + + # 转换系数(确保只出现一次) + coeff_str = format_coefficient(final_coeff) + if coeff_str == "1": + latex_parts.append(latex_term) + elif coeff_str == "-1": + latex_parts.append(f"-{latex_term}") + else: + latex_parts.append(f"{coeff_str}{latex_term}") + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(便于复制) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + for r in range(k): + latex_expr = latex_dict[r] + print(f"β{r} = {latex_expr}") + + # ============ LaTeX代码块(便于复制) ============ + print("\n" + "="*70) + print("LaTeX代码块") + print("="*70) + print("\n```latex") + for r in range(k): + latex_expr = latex_dict[r] + print(f"\\beta_{r} = {latex_expr}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py new file mode 100644 index 00000000..30089036 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02e/compute_integral.py @@ -0,0 +1,482 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction, latex_mode: bool = False) -> str: + """ + ✅ 智能格式化Fraction为字符串 + 参数: + latex_mode: 是否使用LaTeX格式(\cfrac) + """ + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + + # ✅ LaTeX模式:使用\cfrac + if latex_mode: + return f"\\cfrac{{{frac.numerator}}}{{{frac.denominator}}}" + + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +def optimize_two_term_expression(expr: str) -> str: + """ + ✅ 优化两项表达式:确保首项为正 + 例如: "-v[i-1]+v[i+1]" -> "v[i+1]-v[i-1]" + """ + if expr.count('v[') != 2: + return expr # 不是两项表达式 + + # 解析两项 + pattern = r'([+-]?)((?:\d+/)?\d+\*)?v\[i([+-]\d*)\]' + matches = re.findall(pattern, expr) + + if len(matches) != 2: + return expr + + terms = [] + for sign, coeff_part, index in matches: + if not sign: + sign = '+' + + # 系数 + if coeff_part: + coeff = coeff_part.rstrip('*') + else: + coeff = '1' + + terms.append((sign, coeff, index)) + + # 如果首项为负,交换顺序 + if terms[0][0] == '-': + # 保持数学等价:-a + b = b - a + sign1, coeff1, idx1 = terms[0] + sign2, coeff2, idx2 = terms[1] + + # 新表达式: +coeff2*v[i+idx2] - coeff1*v[i+idx1] + new_parts = [] + + if coeff2 == '1': + new_parts.append(f"v[i{idx2}]") + else: + new_parts.append(f"{coeff2}*v[i{idx2}]") + + if coeff1 == '1': + new_parts.append(f"-v[i{idx1}]") + else: + new_parts.append(f"-{coeff1}*v[i{idx1}]") + + return ''.join(new_parts) + + return expr + +def convert_to_latex(term_str: str) -> str: + """ + ✅ 将表达式转换为LaTeX格式(支持两项优化) + + 参数: + term_str: 如 "13/12*(v[i]-2*v[i+1]+v[i+2])^2" 或 "1/4*(-v[i-1]+v[i+1])^2" + + 返回: + LaTeX字符串,如 "\\cfrac{13}{12}(v_{i}-2v_{i+1}+v_{i+2})^2" + """ + # 分离系数和主体 + if '*(' in term_str: + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.strip() + else: + coeff_part = "1" + main_part = term_str + + # ✅ 提取括号内的表达式 + match = re.match(r'^\((.+)\)\^2$', main_part) + if match: + inner_expr = match.group(1) + + # ✅ 优化两项表达式(如 -v[i-1]+v[i+1]) + inner_expr = optimize_two_term_expression(inner_expr) + + # 转换v[i]为v_{i} + inner_latex = inner_expr.replace('*', '') + inner_latex = re.sub(r'v\[i([+-]?\d*)\]', r'v_{i\1}', inner_latex) + + # 构建LaTeX主体 + latex_main = f"({inner_latex})^2" + else: + # 非平方项 + latex_main = term_str.replace('*', '') + latex_main = re.sub(r'v\[i([+-]?\d*)\]', r'v_{i\1}', latex_main) + + return latex_main + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 转换主体表达式 + latex_term = convert_to_latex(term_str) + + # ✅ 转换系数(使用\cfrac格式) + coeff_str = format_coefficient(final_coeff, latex_mode=True) + if coeff_str == "1": + latex_parts.append(latex_term) + elif coeff_str == "-1": + latex_parts.append(f"-{latex_term}") + else: + latex_parts.append(f"{coeff_str}{latex_term}") + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(单行格式) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + for r in range(k): + latex_expr = latex_dict[r] + print(f"β{r} = {latex_expr}") + + # ============ LaTeX代码块 ============ + print("\n" + "="*70) + print("LaTeX代码块(统一在array环境中)") + print("="*70) + print("\n```latex") + print("\\begin{array}{l}") + for r in range(k): + if r == k - 1: # 最后一行不加\\ + print(f" \\beta_{r} = {latex_dict[r]}") + else: + print(f" \\beta_{r} = {latex_dict[r]}\\\\") + print("\\end{array}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py new file mode 100644 index 00000000..1e88084c --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/02f/compute_integral.py @@ -0,0 +1,541 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction, latex_mode: bool = False) -> str: + """ + 智能格式化Fraction为字符串 + 参数: + latex_mode: 是否使用LaTeX格式(\\cfrac) + """ + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + + if latex_mode: + return f"\\cfrac{{{frac.numerator}}}{{{frac.denominator}}}" + + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +# ============ 核心修改:两项表达式优化 ============ + +def optimize_two_term_expression(expr: str, is_latex: bool = False) -> str: + """ + ✅ 核心函数:优化两项表达式 + + 规则: + 1. 仅在两项时生效 + 2. -v[i-1]+v[i+1] -> v[i-1]-v[i+1](首项为正,保持顺序) + 3. 下标顺序:i-1在i+1前面 + + 参数: + expr: 括号内表达式,如 "-v[i-1]+v[i+1]" + is_latex: 是否为LaTeX格式 + + 返回: + 优化后表达式 + """ + # 检查是否为两项 + var_pattern = r'v_\{i[+-]\d*\}' if is_latex else r'v\[i[+-]?\d*\]' + if len(re.findall(var_pattern, expr)) != 2: + return expr + + # 匹配项:符号、系数、下标 + if is_latex: + pattern = r'([+-]?)(\d*)?v_\{i([+-]\d*)\}' + else: + pattern = r'([+-]?)(\d*\*)?v\[i([+-]\d*)\]' + + matches = re.findall(pattern, expr) + + if len(matches) != 2: + return expr + + # 解析两项 + terms = [] + for sign, coeff_part, index in matches: + if not sign: + sign = '+' + + if is_latex: + coeff = coeff_part if coeff_part else '1' + else: + coeff = coeff_part.rstrip('*') if coeff_part else '1' + + terms.append((sign, coeff, index)) + + # 检查首项是否为负 + if terms[0][0] == '-': + sign1, coeff1, idx1 = terms[0] # -v[i-1] + sign2, coeff2, idx2 = terms[1] # +v[i+1] + + # ✅ 保持顺序:i-1在i+1前面 + # -v[i-1] + v[i+1] -> v[i-1] - v[i+1] + new_parts = [] + + # 首项(原第一项,变号) + if is_latex: + if coeff1 == '1': + new_parts.append(f"v_{{i{idx1}}}") + else: + new_parts.append(f"{coeff1}v_{{i{idx1}}}") + else: + if coeff1 == '1': + new_parts.append(f"v[i{idx1}]") + else: + new_parts.append(f"{coeff1}*v[i{idx1}]") + + # 次项(原第二项,符号取反) + if is_latex: + if coeff2 == '1': + new_parts.append(f"-v_{{i{idx2}}}") + else: + new_parts.append(f"-{coeff2}v_{{i{idx2}}}") + else: + if coeff2 == '1': + new_parts.append(f"-v[i{idx2}]") + else: + new_parts.append(f"-{coeff2}*v[i{idx2}]") + + return ''.join(new_parts) + + return expr + +def apply_two_term_optimization_to_python(term_str: str) -> str: + """ + ✅ 对Python表达式应用两项优化 + + 示例: + "1/4*(-v[i-1]+v[i+1])^2" -> "1/4*(v[i-1]-v[i+1])^2" + """ + if '*(' not in term_str or '^2' not in term_str: + return term_str + + try: + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.strip() + + match = re.match(r'^\((.+)\)\^2$', main_part) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=False) + + if optimized_inner != inner_expr: + term_str = f"{coeff_part}*({optimized_inner})^2" + + except: + pass + + return term_str + +def apply_two_term_optimization_to_latex(term_str: str, latex_coeff: str) -> str: + """ + ✅ 对LaTeX表达式应用两项优化 + + 示例: + "\\cfrac{1}{4}(-v_{i-1}+v_{i+1})^2" -> "\\cfrac{1}{4}(v_{i-1}-v_{i+1})^2" + """ + # 提取括号部分 + match = re.search(r'\((.+)\)\^2', term_str) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=True) + + if optimized_inner != inner_expr: + term_str = term_str.replace(f"({inner_expr})^2", f"({optimized_inner})^2") + + return term_str + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + integrated_terms = integrate_polynomial_x(polynomial) + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + latex_coeff = format_coefficient(final_coeff, latex_mode=True) + + # ✅ 转换为LaTeX并应用两项优化 + latex_main = apply_two_term_optimization_to_latex(term_str, latex_coeff) + + # ✅ 构建完整LaTeX项 + if latex_coeff == "1": + latex_term = latex_main + elif latex_coeff == "-1": + latex_term = f"-{latex_main}" + else: + latex_term = f"{latex_coeff}{latex_main}" + + latex_parts.append(latex_term) + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(单行格式 - 应用两项优化) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + python_dict = {} + for r, terms_info in all_terms_info.items(): + python_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 应用两项优化到Python表达式 + optimized_term = apply_two_term_optimization_to_python(term_str) + python_parts.append(optimized_term) + + python_expr = " + ".join(python_parts).replace("+ -", "- ") + python_dict[r] = python_expr + + print(f"β{r} = {python_expr}") + + # ============ LaTeX代码块 ============ + print("\n" + "="*70) + print("LaTeX代码块(统一在array环境中)") + print("="*70) + print("\n```latex") + print("\\begin{array}{l}") + for r in range(k): + if r == k - 1: + print(f" \\beta_{r} = {latex_dict[r]}") + else: + print(f" \\beta_{r} = {latex_dict[r]}\\\\") + print("\\end{array}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py b/example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py new file mode 100644 index 00000000..13bd8bd7 --- /dev/null +++ b/example/figure/1d/weno/interplate/compute_integral/03/compute_integral.py @@ -0,0 +1,544 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +from math import gcd +from functools import reduce +import re + +# ============ 基础函数(保持不变) ============ + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def format_coefficient(frac: Fraction, latex_mode: bool = False) -> str: + """ + 智能格式化Fraction为字符串 + 参数: + latex_mode: 是否使用LaTeX格式(\\cfrac) + """ + if frac == 0: + return "0" + if frac.denominator == 1: + return str(int(frac.numerator)) + + if latex_mode: + return f"\\cfrac{{{frac.numerator}}}{{{frac.denominator}}}" + + return f"{frac.numerator}/{frac.denominator}" + +def compute_lcm(numbers: List[int]) -> int: + """计算最小公倍数""" + if not numbers: + return 1 + return reduce(lambda a, b: a * b // gcd(a, b), numbers) + +def parse_and_optimize_expression(expr: str, exponent: int = 1) -> Tuple[str, Fraction]: + """使用正则表达式安全解析表达式""" + if expr == "0": + return "0", Fraction(1, 1) + + if expr[0] not in '+-': + expr = '+' + expr + + pattern = r'([+-])?(?:(\d+/\d+|\d+|[+-]?\d+/\d+|[+-]?\d+)\*)?(v\[i[+-]?\d*\]|v\[i\])' + matches = re.findall(pattern, expr) + + coeff_vars = [] + denominators = [] + + for match in matches: + sign, coeff_part, var = match + + if coeff_part: + coeff_str = coeff_part.replace('+', '').replace('-', '') + if '/' in coeff_str: + num, den = map(int, coeff_str.split('/')) + coeff = Fraction(num, den) + else: + coeff = Fraction(int(coeff_str), 1) + else: + coeff = Fraction(1, 1) + + if sign == '-': + coeff = -coeff + + coeff_vars.append((coeff, var)) + + if coeff.denominator != 1: + denominators.append(coeff.denominator) + + if not coeff_vars: + return expr, Fraction(1, 1) + + lcm = compute_lcm(denominators) if denominators else 1 + + int_coeffs = [] + for coeff, var in coeff_vars: + int_coeff = coeff * lcm + int_coeffs.append((int(int_coeff.numerator), var)) + + neg_count = sum(1 for c, _ in int_coeffs if c < 0) + pos_count = sum(1 for c, _ in int_coeffs if c > 0) + + factor = Fraction(1, lcm) + if neg_count > pos_count: + factor = -factor + int_coeffs = [(-c, v) for c, v in int_coeffs] + + factor_with_exponent = factor ** exponent + + expr_parts = [] + for coeff, var in int_coeffs: + if coeff == 1: + expr_parts.append(f"+{var}") + elif coeff == -1: + expr_parts.append(f"-{var}") + elif coeff > 0: + expr_parts.append(f"+{coeff}*{var}") + else: + expr_parts.append(f"{coeff}*{var}") + + result_expr = ''.join(expr_parts) + if result_expr.startswith('+'): + result_expr = result_expr[1:] + + return result_expr, factor_with_exponent + +def solve_for_coefficients_optimized(k: int, r: int, i: int = 0) -> List[Tuple[str, Fraction]]: + """生成优化的a系数表达式""" + M = compute_matrix_M(k, r) + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + + a_coeffs_optimized = [] + for j in range(k): + nonzero_items = [] + for m in range(k): + coeff = M_inv[j, m] + if abs(coeff) > 1e-10: + offset = m - r + v_str = f"v[i{offset:+d}]" if offset != 0 else "v[i]" + nonzero_items.append((coeff, v_str)) + + if not nonzero_items: + a_coeffs_optimized.append(("0", Fraction(1, 1))) + continue + + expr_parts = [] + for coeff, v_str in nonzero_items: + frac = Fraction(coeff).limit_denominator(1000) + if coeff == 1.0: + expr_parts.append(f"+{v_str}") + elif coeff == -1.0: + expr_parts.append(f"-{v_str}") + else: + expr_parts.append(f"{frac}*{v_str}") + + expr = ''.join(expr_parts) + if expr.startswith('+'): + expr = expr[1:] + + optimized_expr, factor = parse_and_optimize_expression(expr, exponent=1) + a_coeffs_optimized.append((optimized_expr, factor)) + + return a_coeffs_optimized + +def integrate_polynomial_x(polynomial: Dict[int, List[Tuple[float, List[int]]]]) -> List[Tuple[Fraction, List[int]]]: + """对x在[-1/2, 1/2]上积分""" + a = Fraction(-1, 2) + b = Fraction(1, 2) + integrated_terms = [] + + for exp, expr_list in polynomial.items(): + numerator = b**(exp + 1) - a**(exp + 1) + integral_factor = Fraction(numerator, exp + 1) + print(f'exp={exp},expr_list={expr_list}') + + for coeff, symbols in expr_list: + coeff_frac = Fraction(str(coeff)) + new_coeff = coeff_frac * integral_factor + + if new_coeff != 0: + integrated_terms.append((new_coeff, symbols)) + + return integrated_terms + +def substitute_coefficients_optimized(integrated_terms: List[Tuple[Fraction, List[int]]], + a_coeffs_opt: List[Tuple[str, Fraction]]) -> Tuple[str, List]: + """代入并排序""" + result_dict = defaultdict(lambda: Fraction(0, 1)) + + for coeff, symbols in integrated_terms: + if len(symbols) == 1: + symbol_key = f"a{symbols[0]}" + elif symbols[0] == symbols[1]: + symbol_key = f"a{symbols[0]}^2" + else: + symbol_key = "*".join([f"a{s}" for s in symbols]) + + result_dict[symbol_key] += coeff + + # 构建带系数的项列表 + terms_with_coeffs = [] + + for symbol_key, total_coeff in result_dict.items(): + if total_coeff == 0: + continue + + base_symbol = symbol_key.split('*')[0].split('^')[0] + a_index = int(base_symbol[1:]) + + if a_index >= len(a_coeffs_opt): + raise ValueError(f"多项式索引{a_index}超出范围") + + a_expr, a_factor = a_coeffs_opt[a_index] + + is_squared = '^2' in symbol_key + exponent = 2 if is_squared else 1 + + final_coeff = total_coeff * (a_factor ** exponent) + coeff_abs = abs(final_coeff) + + coeff_str = format_coefficient(final_coeff) + + # 构建最终项 + if is_squared: + term_str = f"{coeff_str}*({a_expr})^2" if coeff_str not in ["1", "-1"] else \ + (f"({a_expr})^2" if coeff_str == "1" else f"-({a_expr})^2") + else: + term_str = f"{coeff_str}*{a_expr}" if coeff_str not in ["1", "-1"] else \ + (f"{a_expr}" if coeff_str == "1" else f"-{a_expr}") + + terms_with_coeffs.append((coeff_abs, final_coeff, term_str)) + + # ✅ 按系数绝对值降序排序 + terms_with_coeffs.sort(key=lambda x: x[0], reverse=True) + + # ✅ 提取排序后的项 + final_terms = [term_str for _, _, term_str in terms_with_coeffs] + + return smart_join_terms(final_terms), terms_with_coeffs + +def smart_join_terms(terms: List[str]) -> str: + """智能连接各项""" + if not terms: + return "0" + + first_idx = 0 + while first_idx < len(terms) and terms[first_idx].startswith('-'): + first_idx += 1 + + if first_idx < len(terms): + expr = terms[first_idx] + for t in terms[:first_idx]: + expr = f"{t} + {expr}" + for t in terms[first_idx+1:]: + if t.startswith('-'): + expr += f" {t}" + else: + expr += f" + {t}" + else: + expr = terms[0] + for t in terms[1:]: + expr += f" {t}" + + return expr + +# ============ 核心修改:两项表达式优化 ============ + +def optimize_two_term_expression(expr: str, is_latex: bool = False) -> str: + """ + ✅ 核心函数:优化两项表达式 + + 规则: + 1. 仅在两项时生效 + 2. -v[i-1]+v[i+1] -> v[i-1]-v[i+1](首项为正,保持顺序) + 3. 下标顺序:i-1在i+1前面 + + 参数: + expr: 括号内表达式,如 "-v[i-1]+v[i+1]" + is_latex: 是否为LaTeX格式 + + 返回: + 优化后表达式 + """ + # 检查是否为两项 + var_pattern = r'v_\{i[+-]\d*\}' if is_latex else r'v\[i[+-]?\d*\]' + if len(re.findall(var_pattern, expr)) != 2: + return expr + + # 匹配项:符号、系数、下标 + if is_latex: + pattern = r'([+-]?)(\d*)?v_\{i([+-]\d*)\}' + else: + pattern = r'([+-]?)(\d*\*)?v\[i([+-]\d*)\]' + + matches = re.findall(pattern, expr) + + if len(matches) != 2: + return expr + + # 解析两项 + terms = [] + for sign, coeff_part, index in matches: + if not sign: + sign = '+' + + if is_latex: + coeff = coeff_part if coeff_part else '1' + else: + coeff = coeff_part.rstrip('*') if coeff_part else '1' + + terms.append((sign, coeff, index)) + + # 检查首项是否为负 + if terms[0][0] == '-': + sign1, coeff1, idx1 = terms[0] # -v[i-1] + sign2, coeff2, idx2 = terms[1] # +v[i+1] + + # ✅ 保持顺序:i-1在i+1前面 + # -v[i-1] + v[i+1] -> v[i-1] - v[i+1] + new_parts = [] + + # 首项(原第一项,变号) + if is_latex: + if coeff1 == '1': + new_parts.append(f"v_{{i{idx1}}}") + else: + new_parts.append(f"{coeff1}v_{{i{idx1}}}") + else: + if coeff1 == '1': + new_parts.append(f"v[i{idx1}]") + else: + new_parts.append(f"{coeff1}*v[i{idx1}]") + + # 次项(原第二项,符号取反) + if is_latex: + if coeff2 == '1': + new_parts.append(f"-v_{{i{idx2}}}") + else: + new_parts.append(f"-{coeff2}v_{{i{idx2}}}") + else: + if coeff2 == '1': + new_parts.append(f"-v[i{idx2}]") + else: + new_parts.append(f"-{coeff2}*v[i{idx2}]") + + return ''.join(new_parts) + + return expr + +def apply_two_term_optimization_to_python(term_str: str) -> str: + """ + ✅ 对Python表达式应用两项优化 + + 示例: + "1/4*(-v[i-1]+v[i+1])^2" -> "1/4*(v[i-1]-v[i+1])^2" + """ + if '*(' not in term_str or '^2' not in term_str: + return term_str + + try: + coeff_part, main_part = term_str.split('*', 1) + main_part = main_part.strip() + + match = re.match(r'^\((.+)\)\^2$', main_part) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=False) + + if optimized_inner != inner_expr: + term_str = f"{coeff_part}*({optimized_inner})^2" + + except: + pass + + return term_str + +def apply_two_term_optimization_to_latex(term_str: str, latex_coeff: str) -> str: + """ + ✅ 对LaTeX表达式应用两项优化 + + 示例: + "\\cfrac{1}{4}(-v_{i-1}+v_{i+1})^2" -> "\\cfrac{1}{4}(v_{i-1}-v_{i+1})^2" + """ + # 提取括号部分 + match = re.search(r'\((.+)\)\^2', term_str) + if not match: + return term_str + + inner_expr = match.group(1) + + # ✅ 应用两项优化(保持顺序) + optimized_inner = optimize_two_term_expression(inner_expr, is_latex=True) + + if optimized_inner != inner_expr: + term_str = term_str.replace(f"({inner_expr})^2", f"({optimized_inner})^2") + + return term_str + +def evaluate_polynomial_integral_symbolic(polynomial: Dict[int, List[Tuple[float, List[int]]]], + k: int, r: int, i: int = 0) -> Tuple[str, List]: + """完整流程:积分 → 代入""" + a_coeffs_opt = solve_for_coefficients_optimized(k, r, i) + print(f'a_coeffs_opt={a_coeffs_opt}') + integrated_terms = integrate_polynomial_x(polynomial) + print(f'integrated_terms={integrated_terms}') + return substitute_coefficients_optimized(integrated_terms, a_coeffs_opt) + +def generate_composite_expressions(k: int, polynomial: Dict[int, List[Tuple[float, List[int]]]], i: int = 0): + """生成所有r值的复合表达式β_r(完整优化版)""" + f_r_dict = {} + all_terms_info = {} + + print(f"\n生成k={k}的复合表达式β_r") + print("="*70) + + for r in range(k): + final_expr, terms_with_coeffs = evaluate_polynomial_integral_symbolic(polynomial, k, r, i) + f_r_dict[r] = final_expr + all_terms_info[r] = terms_with_coeffs + + print(f"\nβ_{r} = {final_expr}") + + # ============ LaTeX格式总结 ============ + print("\n" + "="*70) + print("LaTeX格式总结(按系数绝对值排序)") + print("="*70) + + latex_dict = {} + for r, terms_info in all_terms_info.items(): + latex_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + latex_coeff = format_coefficient(final_coeff, latex_mode=True) + + # ✅ 转换为LaTeX并应用两项优化 + latex_main = apply_two_term_optimization_to_latex(term_str, latex_coeff) + + # ✅ 构建完整LaTeX项 + if latex_coeff == "1": + latex_term = latex_main + elif latex_coeff == "-1": + latex_term = f"-{latex_main}" + else: + latex_term = f"{latex_coeff}{latex_main}" + + latex_parts.append(latex_term) + + latex_expr = " + ".join(latex_parts) + latex_expr = latex_expr.replace("+ -", "- ") + latex_dict[r] = latex_expr + + print(f"\n$\\beta_{r} = {latex_expr}$") + + # ============ 最终汇总(单行格式 - 应用两项优化) ============ + print("\n" + "="*70) + print("最终汇总(单行格式)") + print("="*70) + + python_dict = {} + for r, terms_info in all_terms_info.items(): + python_parts = [] + for coeff_abs, final_coeff, term_str in terms_info: + # ✅ 应用两项优化到Python表达式 + optimized_term = apply_two_term_optimization_to_python(term_str) + python_parts.append(optimized_term) + + python_expr = " + ".join(python_parts).replace("+ -", "- ") + python_dict[r] = python_expr + + print(f"β{r} = {python_expr}") + + # ============ LaTeX代码块 ============ + print("\n" + "="*70) + print("LaTeX代码块(统一在array环境中)") + print("="*70) + print("\n```latex") + print("\\begin{array}{l}") + for r in range(k): + if r == k - 1: + print(f" \\beta_{r} = {latex_dict[r]}") + else: + print(f" \\beta_{r} = {latex_dict[r]}\\\\") + print("\\end{array}") + print("```") + + return f_r_dict + +def print_expression_pretty(expr: str, indent: str = "", single_line: bool = False): + """支持单行输出""" + if single_line: + print(f"{indent}{expr}") + return + + if '+' not in expr: + print(f"{indent}{expr}") + return + + lines = [] + bracket_depth = 0 + current = "" + + for char in expr: + current += char + if char == '(': + bracket_depth += 1 + elif char == ')': + bracket_depth -= 1 + elif char == '+' and bracket_depth == 0: + lines.append(current[:-1].strip()) + current = "" + + if current: + lines.append(current.strip()) + + for i, line in enumerate(lines): + if line.startswith('-'): + print(f"{indent}{line}") + elif i == 0: + print(f"{indent}{line}") + else: + print(f"{indent}+ {line}") + +def test_composite(): + """测试例子""" + k = 3 + + polynomial = { + 0: [(1.0, [1, 1]), (4.0, [2, 2])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + print("="*70) + print("测试:多项式积分后复合a系数表达式(完整优化版)") + print("="*70) + + f_r_dict = generate_composite_expressions(k, polynomial, i=0) + +if __name__ == "__main__": + test_composite() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/counter/01/counter.py b/example/figure/1d/weno/interplate/counter/01/counter.py new file mode 100644 index 00000000..9e6ceaa4 --- /dev/null +++ b/example/figure/1d/weno/interplate/counter/01/counter.py @@ -0,0 +1,20 @@ +from collections import Counter + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +# 使用示例 +index_list = [0, 1, 3, 2, 5, 1] +indices, counts = sort_indices_with_counts(index_list) + +print(f"原始列表: {index_list}") +print(f"排序下标: {indices}") # [0, 1, 2, 3, 5] +print(f"出现次数: {counts}") # [1, 2, 1, 1, 1] \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py b/example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py new file mode 100644 index 00000000..58da9b12 --- /dev/null +++ b/example/figure/1d/weno/interplate/max_common_factor/01/max_common_factor.py @@ -0,0 +1,90 @@ +from fractions import Fraction +from math import gcd +from functools import reduce + +def extract_max_common_factor(numbers): + """ + 从数值列表中提取最大的公共因子,使得剩余部分为互质整数列表 + + 参数: + numbers: 数值列表(可包含整数、浮点数、字符串分数等) + + 返回: + tuple: (factor, simplified_list) + factor: Fraction类型,提取的公共因子 + simplified_list: 整数列表,最简形式(gcd=1) + """ + # 1. 将所有输入转换为Fraction,确保精确的有理数运算 + # Fraction(0.5) = 1/2, Fraction(-1) = -1/1, Fraction("1/3") = 1/3 + fractions = [Fraction(x) for x in numbers] + + # 2. 特殊情况处理 + if not fractions: # 空列表 + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): # 全零列表 + return Fraction(1, 1), [0] * len(fractions) + + # 3. 提取所有分子和分母 + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + # 4. 计算分子的最大公约数(GCD) + # reduce函数连续应用gcd: gcd(gcd(p1,p2), p3), ... + numerator_gcd = reduce(gcd, numerators) + + # 5. 计算分母的最小公倍数(LCM) + def lcm(a, b): + """计算两个数的最小公倍数:lcm(a,b) = |a×b| / gcd(a,b)""" + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + # 连续应用lcm: lcm(lcm(q1,q2), q3), ... + denominator_lcm = reduce(lcm, denominators) + + # 6. 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 7. 计算简化后的列表 + simplified_fractions = [f / common_factor for f in fractions] + + # 8. 验证并转换为整数列表 + # 理论上所有分母都应为1,因为除以了最大公共因子 + simplified_integers = [sf.numerator for sf in simplified_fractions] + + # 9. 额外验证:确保结果整数列表互质(gcd=1) + verification_gcd = reduce(gcd, simplified_integers) + + # 如果verification_gcd≠1,说明还能继续提取,调整结果 + if verification_gcd != 1: + true_factor = common_factor * verification_gcd + simplified_integers = [x // verification_gcd for x in simplified_integers] + return true_factor, simplified_integers + + return common_factor, simplified_integers + + +# 测试函数 +def demonstrate(): + """演示各种情况""" + test_cases = [ + ("用户示例", [Fraction(1,2), -1, Fraction(1,2)]), + ("整数提取", [2, -4, 2]), + ("分数列表", [Fraction(1,3), Fraction(2,3), -1]), + ("浮点数", [0.5, -1.0, 0.5]), + ("互质整数", [1, 2, 3]), + ("负分数", [Fraction(-2,3), Fraction(4,3), -2]), + ] + + for name, case in test_cases: + factor, simplified = extract_max_common_factor(case) + print(f"【{name}】") + print(f" 原始列表: {case}") + print(f" 提取系数: {factor} (小数形式: {float(factor)})") + print(f" 简化列表: {simplified}") + print(f" 验证除法: {[Fraction(case[i]) / factor for i in range(len(case))]}") + print(f" 简化列表gcd: {reduce(gcd, simplified)}\n") + +# 运行演示 +demonstrate() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py new file mode 100644 index 00000000..6fc5299c --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01/polynomial_operations.py @@ -0,0 +1,225 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +# ============= 测试 ============= + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + +# 运行测试 +test_polynomial_operations() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py new file mode 100644 index 00000000..bfe47f27 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01a/polynomial_operations.py @@ -0,0 +1,263 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +# ============= 测试 ============= + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_verify_format(): + poly1 = {0: [(1, [1])], 1: [(2, [2])]} + + squared = polynomial_square(poly1) + # squared 仍然是 Polynomial 格式: + # { + # 0: [(1, [1, 1])], + # 1: [(4, [1, 2])], + # 2: [(4, [2, 2])] + # } + + integrated = integrate_polynomial(squared) + # integrated 仍然是 Polynomial 格式: + # { + # 1: [(1.0, [1, 1])], + # 2: [(2.0, [1, 2])], + # 3: [(1.333..., [2, 2])] + # } + + # 测试 + verify_format(poly1) # ✓ 通过 + verify_format(squared) # ✓ 通过 + verify_format(integrated) # ✓ 通过 + +# 运行测试 +#test_polynomial_operations() +test_verify_format() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py new file mode 100644 index 00000000..b0177cd2 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01b/polynomial_operations.py @@ -0,0 +1,441 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator_old(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + print('demo_smoothness_indicator_new') + + n = 5 + m = 2 + coeff, power = derivative_form(n, m) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k - 1 + cols = k - 1 + + # 改造重点2:matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 第一阶段:构建matrix并打印导数系数表 + print("\n=== 导数系数表 (coef*x^power) ===") + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + print(f"{coef}x^{power}", end=' ') + print() + + print(f'\nmatrix=\n{matrix}') + + # 改造重点3:从matrix构建Polynomial List + print("\n=== 符号表达式表 (coef*a{acoef}*x^power) ===") + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + print(f"{coef}*a{symbols[0]}*x^{power}", end=' ') + else: + print("0 ", end=' ') + + polynomial_list.append(dict(polynomial)) + print() + + print(f'\npolynomial_list={polynomial_list}') + + # 改造重点4:用新函数打印(保持旧格式) + print("\n=== 最终输出(使用新结构)===") + for i, poly in enumerate(polynomial_list): + print(f"-- 第{i+1}行导数 --") + print_polynomial_old_style(poly) + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_verify_format(): + poly1 = {0: [(1, [1])], 1: [(2, [2])]} + + squared = polynomial_square(poly1) + # squared 仍然是 Polynomial 格式: + # { + # 0: [(1, [1, 1])], + # 1: [(4, [1, 2])], + # 2: [(4, [2, 2])] + # } + + integrated = integrate_polynomial(squared) + # integrated 仍然是 Polynomial 格式: + # { + # 1: [(1.0, [1, 1])], + # 2: [(2.0, [1, 2])], + # 3: [(1.333..., [2, 2])] + # } + + # 测试 + verify_format(poly1) # ✓ 通过 + verify_format(squared) # ✓ 通过 + verify_format(integrated) # ✓ 通过 + +if __name__ == "__main__": + demo_smoothness_indicator() + #square_polynomial_test() + #test_verify_format() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py new file mode 100644 index 00000000..4c7d945d --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01c/polynomial_operations.py @@ -0,0 +1,436 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def format_expression(expr: Expression) -> str: + """格式化 Expression""" + if not expr: + return "0" + + term_strs = [format_term(term) for term in expr] + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator_old(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + k = 3 + rows = k - 1 + cols = k - 1 + + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + print("\n=== 导数系数表 (coef*x^power) ===") + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + print(f"{coef}x^{power}", end=' ') + print() + + print(f'\nmatrix=\n{matrix}') + + # 从matrix构建Polynomial List + print("\n=== 符号表达式表 (coef*a{acoef}*x^power) ===") + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + print(f"{coef}*a{symbols[0]}*x^{power}", end=' ') + else: + print("0 ", end=' ') + + polynomial_list.append(dict(polynomial)) + print() + + print(f'\npolynomial_list={polynomial_list}') + + for i, poly in enumerate(polynomial_list): + print(f"-- 第{i+1}行导数 --") + print_polynomial(poly) + squared = polynomial_square(poly) + print_polynomial(squared, "平方展开后") + integrated = integrate_polynomial(squared) + print_polynomial(integrated, "积分后") + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_verify_format(): + poly1 = {0: [(1, [1])], 1: [(2, [2])]} + + squared = polynomial_square(poly1) + # squared 仍然是 Polynomial 格式: + # { + # 0: [(1, [1, 1])], + # 1: [(4, [1, 2])], + # 2: [(4, [2, 2])] + # } + + integrated = integrate_polynomial(squared) + # integrated 仍然是 Polynomial 格式: + # { + # 1: [(1.0, [1, 1])], + # 2: [(2.0, [1, 2])], + # 3: [(1.333..., [2, 2])] + # } + + # 测试 + verify_format(poly1) # ✓ 通过 + verify_format(squared) # ✓ 通过 + verify_format(integrated) # ✓ 通过 + +if __name__ == "__main__": + demo_smoothness_indicator() + #square_polynomial_test() + #test_verify_format() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py new file mode 100644 index 00000000..685638d8 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01d/polynomial_operations.py @@ -0,0 +1,599 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator_old(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + k = 3 + rows = k - 1 + cols = k - 1 + + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + print("\n=== 导数系数表 (coef*x^power) ===") + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + print(f"{coef}x^{power}", end=' ') + print() + + print(f'\nmatrix=\n{matrix}') + + # 从matrix构建Polynomial List + print("\n=== 符号表达式表 (coef*a{acoef}*x^power) ===") + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + print(f"{coef}*a{symbols[0]}*x^{power}", end=' ') + else: + print("0 ", end=' ') + + polynomial_list.append(dict(polynomial)) + print() + + print(f'\npolynomial_list={polynomial_list}') + + for i, poly in enumerate(polynomial_list): + print(f"-- 第{i+1}行导数 --") + print_polynomial(poly) + squared = polynomial_square(poly) + print_polynomial(squared, "平方展开后") + integrated = integrate_polynomial(squared) + print_polynomial(integrated, "积分后") + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +if __name__ == "__main__": + #demo_smoothness_indicator() + test_same_bounds() diff --git a/example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py new file mode 100644 index 00000000..9aac95ea --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/01e/polynomial_operations.py @@ -0,0 +1,564 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def demo_smoothness_indicator(): + k = 3 + rows = k - 1 + cols = k - 1 + + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + print(f'matrix=\n{matrix}') + + # 从matrix构建Polynomial List + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + + polynomial_list.append(dict(polynomial)) + + print(f'\npolynomial_list={polynomial_list}') + + squared_list = [] + for i, poly in enumerate(polynomial_list): + #print(f"-- 第{i+1}行导数 --") + #print_polynomial(poly) + squared = polynomial_square(poly) + squared_list.append( squared ) + #print_polynomial(squared, "平方展开后") + + + print("\n原始多项式列表:") + for i, p in enumerate(squared_list, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(squared_list, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +if __name__ == "__main__": + #test_same_bounds() + demo_smoothness_indicator() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py new file mode 100644 index 00000000..94c62fd1 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02/polynomial_operations.py @@ -0,0 +1,598 @@ +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_matrix_M(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_derivate_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def demo_smoothness_indicator(k): + rows = k - 1 + cols = k - 1 + + matrix = create_derivate_matrix(k) + + print(f'matrix=\n{matrix}') + + # 从matrix构建Polynomial List + polynomial_list = [] + for i in range(rows): + # Polynomial = {指数: Expression} + polynomial = defaultdict(list) + + for j in range(cols): + term, power = matrix[i][j] + coef, symbols = term + + if coef != 0: + # 添加到对应指数的项列表 + polynomial[power].append(term) + + polynomial_list.append(dict(polynomial)) + + print(f'\npolynomial_list={polynomial_list}') + + squared_list = [] + for i, poly in enumerate(polynomial_list): + #print(f"-- 第{i+1}行导数 --") + #print_polynomial(poly) + squared = polynomial_square(poly) + squared_list.append( squared ) + #print_polynomial(squared, "平方展开后") + + + print("\n原始多项式列表:") + for i, p in enumerate(squared_list, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(squared_list, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + + print(f'total_result={total_result}') + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py new file mode 100644 index 00000000..36fb75f0 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02a/polynomial_operations.py @@ -0,0 +1,708 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + v_str = f'{csign}{frac_str}*v[{id}]' + return v_str + +def print_coeffs_expression(a_coeffs): + rows, cols = a_coeffs.shape + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def demo_smoothness_indicator(k): + """ + 计算平滑性指标的演示函数。 + 步骤: + 1. 创建差分矩阵。 + 2. 从矩阵构建多项式列表。 + 3. 计算每个多项式的平方。 + 4. 打印平方后的多项式。 + 5. 在区间[-1/2, 1/2]上积分求和。 + 6. 输出最终结果。 + """ + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + print(f'total_result={total_result}') + + r = 1 + M = compute_mass_matrix(k,r) + print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + print_coeffs_expression(a_coeffs) + + + + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py new file mode 100644 index 00000000..51d14040 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02b/polynomial_operations.py @@ -0,0 +1,717 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def demo_smoothness_indicator(k): + """ + 计算平滑性指标的演示函数。 + 步骤: + 1. 创建差分矩阵。 + 2. 从矩阵构建多项式列表。 + 3. 计算每个多项式的平方。 + 4. 打印平方后的多项式。 + 5. 在区间[-1/2, 1/2]上积分求和。 + 6. 输出最终结果。 + """ + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + print(f'total_result={total_result}') + + for r in range(k): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + print_coeffs_expression(a_coeffs,k,r) + + + + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py new file mode 100644 index 00000000..9dc4585c --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02c/polynomial_operations.py @@ -0,0 +1,796 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def print_reconstruction_coefficients(k, r_values, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + # Generate index range string + def get_index_range(r): + if r == 0: + return f"[i, i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r}, i+{k-1-r}]" + + # Process each r value + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +#def solve_polynomial_coefficients(k) + +def demo_smoothness_indicator(k): + """ + 计算平滑性指标的演示函数。 + 步骤: + 1. 创建差分矩阵。 + 2. 从矩阵构建多项式列表。 + 3. 计算每个多项式的平方。 + 4. 打印平方后的多项式。 + 5. 在区间[-1/2, 1/2]上积分求和。 + 6. 输出最终结果。 + """ + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + #print_coeffs_expression(a_coeffs,k,r) + coeffs_list.append( a_coeffs ) + r_values = list(range(k)) + print_reconstruction_coefficients(k, r_values, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py new file mode 100644 index 00000000..eb029fbd --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02d/polynomial_operations.py @@ -0,0 +1,805 @@ +from fractions import Fraction +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print(f'a_coeffs={a_coeffs}') + print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py new file mode 100644 index 00000000..70ee4a99 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02e/polynomial_operations.py @@ -0,0 +1,859 @@ +from fractions import Fraction +from collections import Counter +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + for i in range(nSize): + id = indices[i] + co = counts[i] + symbol_str.append(f"a[{id}]^{co}") + + symbol_str_final = "*".join(symbol_str) + print(f"symbol_str_final: {symbol_str_final}") + term_strs.append(f"{coeff}*{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"expr_str = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print(f'a_coeffs={a_coeffs}') + print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py new file mode 100644 index 00000000..c7fec81b --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/02f/polynomial_operations.py @@ -0,0 +1,870 @@ +from fractions import Fraction +from collections import Counter +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ''.join(expr_parts) + print(f'{expr}') + return expr + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + for i in range(nSize): + id = indices[i] + co = counts[i] + coefficients_str = polynomial_coefficients_str(a_coeffs[id],k,r) + symbol_str.append(f"({coefficients_str})^{co}") + + symbol_str_final = "*".join(symbol_str) + print(f"symbol_str_final: {symbol_str_final}") + term_strs.append(f"{coeff}*{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"expr_str = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + #print(f'a_coeffs={a_coeffs}') + #print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py new file mode 100644 index 00000000..6a62554e --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03/polynomial_operations.py @@ -0,0 +1,1124 @@ +from fractions import Fraction +from collections import Counter +from collections import defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factorBAK(numbers): + """ + 从数值列表中提取最大的公共因子,使得剩余部分为互质整数列表 + + 参数: + numbers: 数值列表(可包含整数、浮点数、字符串分数等) + + 返回: + tuple: (factor, simplified_list) + factor: Fraction类型,提取的公共因子 + simplified_list: 整数列表,最简形式(gcd=1) + """ + # 1. 将所有输入转换为Fraction,确保精确的有理数运算 + # Fraction(0.5) = 1/2, Fraction(-1) = -1/1, Fraction("1/3") = 1/3 + print(f'numbers={numbers}') + fractions = [Fraction(x) for x in numbers] + + # 2. 特殊情况处理 + if not fractions: # 空列表 + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): # 全零列表 + return Fraction(1, 1), [0] * len(fractions) + + # 3. 提取所有分子和分母 + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + # 4. 计算分子的最大公约数(GCD) + # reduce函数连续应用gcd: gcd(gcd(p1,p2), p3), ... + numerator_gcd = reduce(gcd, numerators) + + # 5. 计算分母的最小公倍数(LCM) + def lcm(a, b): + """计算两个数的最小公倍数:lcm(a,b) = |a×b| / gcd(a,b)""" + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + # 连续应用lcm: lcm(lcm(q1,q2), q3), ... + denominator_lcm = reduce(lcm, denominators) + + # 6. 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 7. 计算简化后的列表 + simplified_fractions = [f / common_factor for f in fractions] + + # 8. 验证并转换为整数列表 + # 理论上所有分母都应为1,因为除以了最大公共因子 + simplified_integers = [sf.numerator for sf in simplified_fractions] + + # 9. 额外验证:确保结果整数列表互质(gcd=1) + verification_gcd = reduce(gcd, simplified_integers) + + # 如果verification_gcd≠1,说明还能继续提取,调整结果 + if verification_gcd != 1: + true_factor = common_factor * verification_gcd + simplified_integers = [x // verification_gcd for x in simplified_integers] + return true_factor, simplified_integers + + return common_factor, simplified_integers + +def extract_max_common_factorBAK1(numbers): + """ + 从数值列表中提取最大的公共因子,使得剩余部分为互质整数列表 + + 参数: + numbers: 数值列表(支持int、float、numpy标量、字符串等) + + 返回: + tuple: (factor, simplified_list) + factor: Fraction类型,提取的公共因子 + simplified_list: 整数列表,最简形式(gcd=1) + """ + + def _to_python_number(x): + """ + 关键修复:将numpy标量转换为Python原生类型 + """ + if isinstance(x, (np.integer, np.floating, np.ndarray)): + # numpy标量转Python标量 + return x.item() + return x + + + print(f'numbers={numbers}') + + # 1. 转换前先处理numpy类型 + processed_numbers = [_to_python_number(x) for x in numbers] + + # 2. 转换为Fraction(现在安全了) + fractions = [Fraction(x) for x in processed_numbers] + print(f'fractions={fractions}') + + # 3. 后续逻辑不变 + if not fractions: + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + # 提取分子和分母 + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + # 计算分子GCD + numerator_gcd = reduce(gcd, numerators) + + # 计算分母LCM + def lcm(a, b): + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + denominator_lcm = reduce(lcm, denominators) + + # 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 简化列表 + simplified_fractions = [f / common_factor for f in fractions] + simplified_integers = [sf.numerator for sf in simplified_fractions] + + # 验证互质 + verification_gcd = reduce(gcd, simplified_integers) + if verification_gcd != 1: + true_factor = common_factor * verification_gcd + simplified_integers = [x // verification_gcd for x in simplified_integers] + return true_factor, simplified_integers + + return common_factor, simplified_integers + +def extract_max_common_factor(numbers, max_denominator=1000000, tolerance=1e-12): + """ + 提取最大公共因子,支持numpy浮点数精度修复 + + 参数: + numbers: 数值列表(支持numpy标量、float、int等) + max_denominator: 限制分母的最大值,用于修复浮点误差 + tolerance: 接近零值的容差阈值 + """ + + def _to_python_number(x): + """转换numpy标量为Python原生类型""" + if isinstance(x, (np.integer, np.floating)): + return x.item() + if isinstance(x, np.ndarray) and x.size == 1: + return x.item() + return x + + def _smart_fraction(x): + """智能转换为Fraction,自动修复精度问题""" + # 1. 转换为Python原生数值 + val = _to_python_number(x) + + # 2. 处理接近零的值 + if abs(val) < tolerance: + return Fraction(0, 1) + + # 3. 对浮点数先限制分母再转换 + if isinstance(val, float): + # 先创建Fraction,再限制分母复杂度 + return Fraction(val).limit_denominator(max_denominator) + + # 4. 其他类型直接转换 + return Fraction(val) + + # 主逻辑 + fractions = [_smart_fraction(x) for x in numbers] + + if not fractions: + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + # 计算分子GCD和分母LCM + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + + def lcm(a, b): + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + denominator_lcm = reduce(lcm, denominators) + + # 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 简化并验证互质 + simplified = [f / common_factor for f in fractions] + simplified_integers = [sf.numerator for sf in simplified] + + # 确保gcd=1 + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + common_factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return common_factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign}{frac_str}*v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{k-1}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ''.join(expr_parts) + print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + print(f"expr={expr}") + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + print(f'counts={counts}') + for i in range(nSize): + id = indices[i] + co = counts[i] + #a_coeff = get_numeric_list(a_coeffs[id]) + print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + symbol_str.append(f"({coefficients_str})^{co}") + symbol_str_final = "*".join(symbol_str) + + print(f"symbol_str_final: {symbol_str_final}") + term_strs.append(f"{coeff*factors}*{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print(f"expression = {format_expression(expression)}") + print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"expr_str = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + #print(f'a_coeffs={a_coeffs}') + #print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py new file mode 100644 index 00000000..5f6c50fd --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03a/polynomial_operations.py @@ -0,0 +1,1055 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factorBAK(numbers, max_denominator=1000000, tolerance=1e-12): + """ + 提取最大公共因子,支持numpy浮点数精度修复 + + 参数: + numbers: 数值列表(支持numpy标量、float、int等) + max_denominator: 限制分母的最大值,用于修复浮点误差 + tolerance: 接近零值的容差阈值 + """ + + def _to_python_number(x): + """转换numpy标量为Python原生类型""" + if isinstance(x, (np.integer, np.floating)): + return x.item() + if isinstance(x, np.ndarray) and x.size == 1: + return x.item() + return x + + def _smart_fraction(x): + """智能转换为Fraction,自动修复精度问题""" + # 1. 转换为Python原生数值 + val = _to_python_number(x) + + # 2. 处理接近零的值 + if abs(val) < tolerance: + return Fraction(0, 1) + + # 3. 对浮点数先限制分母再转换 + if isinstance(val, float): + # 先创建Fraction,再限制分母复杂度 + return Fraction(val).limit_denominator(max_denominator) + + # 4. 其他类型直接转换 + return Fraction(val) + + # 主逻辑 + fractions = [_smart_fraction(x) for x in numbers] + + if not fractions: + return Fraction(1, 1), [] + + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + # 计算分子GCD和分母LCM + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + + def lcm(a, b): + if a == 0 or b == 0: + return 0 + return abs(a * b) // gcd(a, b) + + denominator_lcm = reduce(lcm, denominators) + + # 最大公共因子 = 分子GCD / 分母LCM + common_factor = Fraction(numerator_gcd, denominator_lcm) + + # 简化并验证互质 + simplified = [f / common_factor for f in fractions] + simplified_integers = [sf.numerator for sf in simplified] + + # 确保gcd=1 + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + common_factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return common_factor, simplified_integers + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + print(f" 多项式{idx+1}积分: {format_expression(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def test_polynomial_operations(): + print("="*60) + print("多项式操作测试") + print("="*60) + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, [1])], # x^0 项: 1*a1 + 1: [(2, [2])] # x^1 项: 2*a2 + } + + print_polynomial(poly1, "原始多项式1") + # 输出: (1*a1) + (2*a2)*x^1 + + # 平方展开 + squared1 = polynomial_square(poly1) + print_polynomial(squared1, "平方展开后") + # 输出: (1*a1^2) + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 积分 + integrated1 = integrate_polynomial(squared1) + print_polynomial(integrated1, "积分后") + # 输出: (1*a1^2)*x^1 + (2*a1*a2)*x^2 + (4/3*a2^2)*x^3 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, [2])] # x^0 项: 2*a2 + } + + print_polynomial(poly2, "\n原始多项式2") + # 输出: (2*a2) + + squared2 = polynomial_square(poly2) + print_polynomial(squared2, "平方展开后") + # 输出: (4*a2^2) + + integrated2 = integrate_polynomial(squared2) + print_polynomial(integrated2, "积分后") + # 输出: (4*a2^2)*x^1 + + # 测试3: 包含多个项的表达式 + poly3 = { + 0: [(1, [1]), (1, [2])], # x^0 项: a1 + a2 + 2: [(3, [3])] # x^2 项: 3*a3 + } + + print_polynomial(poly3, "\n原始多项式3") + # 输出: (1*a1 + 1*a2) + (3*a3)*x^2 + + squared3 = polynomial_square(poly3) + print_polynomial(squared3, "平方展开后") + # 输出: (1*a1^2 + 2*a1*a2 + 1*a2^2) + (6*a1*a3 + 6*a2*a3)*x^2 + (9*a3^2)*x^4 + + integrated3 = integrate_polynomial(squared3) + print_polynomial(integrated3, "积分后") + + +def test_integral(): + print("="*60) + print("测试:平方后的积分") + print("="*60) + + # 原始多项式: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly_squared = { + 0: [(1.0, [1, 1])], # a1^2 * x^0 + 1: [(4.0, [1, 2])], # 4*a1*a2 * x^1 + 2: [(4.0, [2, 2])] # 4*a2^2 * x^2 + } + + # 显示原始多项式 + print("原始多项式:") + #print_polynomial_old_style(poly_squared) + print_polynomial(poly_squared) + + # 计算定积分 + evaluate_and_print(poly_squared, "定积分计算") + # 期望结果: 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + +def test_general_integral(): + """测试更复杂的情况""" + print("\n" + "="*60) + print("测试:一般多项式积分") + print("="*60) + + # 多项式: (2*a1 + 3*a2)*x^0 + (4*a1*a3)*x^2 + poly = { + 0: [(2.0, [1]), (3.0, [2])], # (2*a1 + 3*a2) + 2: [(4.0, [1, 3])] # 4*a1*a3 * x^2 + } + + print("原始多项式:") + #print_polynomial_old_style(poly) + print_polynomial(poly) + + # 在 [-0.5, 0.5] 上积分 + evaluate_and_print(poly, "定积分计算") + # 期望: + # x^0 项: (2*a1 + 3*a2) * 1 = 2*a1 + 3*a2 + # x^2 项: 4*a1*a3 * (0.5^3 - (-0.5)^3)/3 = 4*a1*a3 * 0.08333 = 0.3333*a1*a3 + +def test_same_bounds(): + print("="*60) + print("情况一:多个多项式在同一区间积分后求和") + print("="*60) + + # 多项式1: (1*a1^2) + (4*a1*a2)*x + (4*a2^2)*x^2 + poly1 = { + 0: [(1.0, [1, 1])], + 1: [(4.0, [1, 2])], + 2: [(4.0, [2, 2])] + } + + # 多项式2: (2*a3) + (3*a1*a3)*x + poly2 = { + 0: [(2.0, [3])], + 1: [(3.0, [1, 3])] + } + + # 多项式3: (5*a2*a3) + poly3 = { + 0: [(5.0, [2, 3])] + } + + polynomials = [poly1, poly2, poly3] + + # 打印原始多项式 + print("\n原始多项式列表:") + for i, p in enumerate(polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(p, "") + + # 积分并求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + total_result = sum_integrals_same_bounds(polynomials, -0.5, 0.5) + + # 打印最终结果 + print(f"\n最终结果:") + print(f"Σ ∫ P_i(x) dx = {format_expression(total_result)}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\n原始多项式列表:") + for i, poly in enumerate(squared_polynomials, 1): + print(f" P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}*" + frac_star = f"{frac_str}·" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f'\n多项式列表: {polynomial_list}') + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + #print(f"squared_polynomials={squared_polynomials}") + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + print("\n积分求和过程(区间[-1/2, 1/2]):") + lower_bound = -0.5 + upper_bound = 0.5 + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\n最终结果:") + formatted_result = format_expression(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + #print(f'total_result={total_result}') + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + #print(f'counts={counts}') + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + term_strs.append(f"{frac}·{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + #print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + #print(f'a_coeffs={a_coeffs}') + #print_coeffs_expression(a_coeffs,k,r) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py new file mode 100644 index 00000000..098b2401 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03b/polynomial_operations.py @@ -0,0 +1,893 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}*" + frac_star = f"{frac_str}·" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + #print(f'counts={counts}') + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + term_strs.append(f"{frac}·{symbol_str_final}") + + return " + ".join(term_strs) + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + #v_str = coef_to_str(coeff, id, j==0) + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ''.join(expr_parts) + print(f'{expr}') + + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def print_smoothness_indicator(expression,a_coeffs,k,r): + #print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py new file mode 100644 index 00000000..e7728cb9 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03c/polynomial_operations.py @@ -0,0 +1,906 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + #print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py new file mode 100644 index 00000000..51173fc4 --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03d/polynomial_operations.py @@ -0,0 +1,911 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(1) + demo_smoothness_indicator(2) + demo_smoothness_indicator(3) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py b/example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py new file mode 100644 index 00000000..a0d15dbb --- /dev/null +++ b/example/figure/1d/weno/interplate/polynomial_operations/03e/polynomial_operations.py @@ -0,0 +1,912 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + +if __name__ == "__main__": + demo_smoothness_indicator(1) + demo_smoothness_indicator(2) + demo_smoothness_indicator(3) + demo_smoothness_indicator(4) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py b/example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py new file mode 100644 index 00000000..265150e9 --- /dev/null +++ b/example/figure/1d/weno/interplate/smoothness_indicator/01/smoothness_indicator.py @@ -0,0 +1,591 @@ +import numpy as np +import math +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +def demo_weno_linear_weights_maxk(): + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def demo_smoothness_indicator(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + if coef == 0: + acoef = 0 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + + +if __name__ == "__main__": + demo_smoothness_indicator() diff --git a/example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py b/example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py new file mode 100644 index 00000000..b25fb9c1 --- /dev/null +++ b/example/figure/1d/weno/interplate/smoothness_indicator/01a/smoothness_indicator.py @@ -0,0 +1,623 @@ +import numpy as np +import math +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +def demo_weno_linear_weights_maxk(): + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + #print(f"{key}: {power_map[key]}") + mylist = power_map[key] + n = len( mylist ) + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + #print(f"idx={idx}",end =' ') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +if __name__ == "__main__": + demo_smoothness_indicator() diff --git a/example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py b/example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py new file mode 100644 index 00000000..3ec90af4 --- /dev/null +++ b/example/figure/1d/weno/interplate/smoothness_indicator/01b/smoothness_indicator.py @@ -0,0 +1,799 @@ +import numpy as np +import math +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +def demo_weno_linear_weights_maxk(): + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def demo_smoothness_indicator(): + print(f'demo_smoothness_indicator') + + n = 5 + m = 2 + coeff, power = derivative_form(5, 2) + print(f"d^{{{m}}}/dx^{{{m}}}(x^({n}))={coeff}x^{power}") + + k = 3 + rows = k-1 + cols = k-1 + matrix = np.empty((rows, cols), dtype=object) + #print(f'matrix=\n{matrix}') + + #x^1 x^2 x^3 + #d^1dx^1 1x^0 2x^1 3x^2 + #d^2dx^2 0x^0 2x^0 6x^1 + #d^3dx^3 0x^0 0x^0 6x^0 + for i in range(rows): + for j in range(cols): + coef, power = derivative_form(j+1, i+1) + acoef = j + 1 + matrix[i][j] = (coef, acoef, power) + print(f"{coeff}x^{power}",end=' ') + print() + + print(f'matrix=\n{matrix}') + power_map_list = [] + for i in range(rows): + power_map = defaultdict(list) + for j in range(cols): + coef, acoef, power = matrix[i][j] + if coef != 0: + power_map[power].append((coef, acoef)) + print(f"{coef}*a{acoef}*x^{power}",end=' ') + power_map_list.append(power_map) + print() + + print(f'power_map_list={power_map_list}') + for i in range(rows): + print_power_symbol(power_map_list[i]) + +def square_polynomial(power_map): + """ + 多项式平方展开 + 参数: + power_map: {指数: [(数值系数, 符号索引), ...]} + 返回: + 展开后的结果字典 + """ + result = defaultdict(list) + sorted_exps = sorted(power_map.keys()) + + # 1. 计算每个项的平方 (a^2) + for exp in sorted_exps: + terms = power_map[exp] + new_exp = exp * 2 + + for coef, acoef in terms: + # 存储为 (数值系数, 符号索引, 是否平方标志) + result[new_exp].append((coef, acoef, True)) + + # 2. 计算交叉项 (2ab) + for i in range(len(sorted_exps)): + for j in range(i + 1, len(sorted_exps)): + exp_i, exp_j = sorted_exps[i], sorted_exps[j] + new_exp = exp_i + exp_j + + for coef_i, acoef_i in power_map[exp_i]: + for coef_j, acoef_j in power_map[exp_j]: + # 交叉项系数乘以2 + result[new_exp].append((2 * coef_i * coef_j, + (acoef_i, acoef_j), False)) + + return result + +def merge_and_simplify(power_map): + """合并同类项并化简符号乘积""" + simplified = defaultdict(dict) # exp: {符号键: 总系数} + + for exp, terms in power_map.items(): + for term_info in terms: + # 解析 term_info: (系数, 符号信息, 是否平方项) + if len(term_info) == 3: + coef, symbols_part, is_square = term_info + else: + # 兼容旧数据格式 + coef, symbols_part = term_info + is_square = False + + # 关键修复:根据标志计算有效系数 + if is_square: + # 平方项:系数需要平方 (c*a)^2 → c^2 * a^2 + effective_coef = coef * coef + else: + # 交叉项:系数已是最终值 2*c_i*c_j + effective_coef = coef + + # 生成标准化符号键 + if isinstance(symbols_part, tuple): + # 交叉项:(索引1, 索引2) + i, j = symbols_part + symbol_names = [f"a{i}", f"a{j}"] + else: + # 平方项:单个符号索引 + symbol_names = [f"a{symbols_part}", f"a{symbols_part}"] + + # 排序保证 a1*a2 和 a2*a1 被视为相同 + key = tuple(sorted(symbol_names)) + + # 累加系数 + simplified[exp][key] = simplified[exp].get(key, 0) + effective_coef + + # 转换回列表格式 + result = defaultdict(list) + for exp, term_dict in simplified.items(): + for symbol_key, total_coef in term_dict.items(): + result[exp].append((total_coef, symbol_key)) + + return result + +def format_term(coef, symbols): + """ + 格式化单项式,支持多种输入类型 + symbols 可能是: + - 整数 (如 1) → 表示 a1 + - 字符串 (如 'a1') → 直接使用 + - 元组 (如 ('a1', 'a2')) → a1*a2 或 a1^2 + """ + # 处理 symbols 为整数的情况(如 1 → 'a1') + if isinstance(symbols, int): + symbol_str = f"a{symbols}" + # 处理 symbols 为字符串的情况 + elif isinstance(symbols, str): + symbol_str = symbols + # 处理 symbols 为元组/列表的情况 + elif isinstance(symbols, (tuple, list)): + if len(symbols) == 1: + symbol_str = str(symbols[0]) + elif symbols[0] == symbols[1]: # 平方项 + symbol_str = f"{symbols[0]}^2" + else: # 不同符号相乘 + symbol_str = "*".join(str(s) for s in symbols) + else: + raise TypeError(f"Unsupported symbols type: {type(symbols)}") + + return f"{coef}*{symbol_str}" + +def print_expanded_polynomial(power_map): + """ + 打印展开后的多项式,不显示最后一行的"+" + """ + if not power_map: + print("0") + return + + sorted_keys = sorted(power_map.keys()) + + for idx, key in enumerate(sorted_keys): + terms = power_map[key] + + # 构建该项的内部表达式 + inner_terms = [] + for coef, symbols in terms: + inner_terms.append(format_term(coef, symbols)) + + # 打印 (内层表达式)*x^key + if len(inner_terms) == 1: + print(f"({inner_terms[0]})*x^{key}", end='') + else: + print(f"({' + '.join(inner_terms)})*x^{key}", end='') + + # 判断是否是最后一项 + if idx < len(sorted_keys) - 1: + print(" + ", end='') + else: + print() # 最后一项只换行 + +def square_polynomial_test(): + # ============= 测试代码 ============= + + # 测试1: (1*a1)*x^0 + (2*a2)*x^1 + poly1 = { + 0: [(1, 1)], # x^0 项: 1*a1 + 1: [(2, 2)] # x^1 项: 2*a2 + } + + print("原始多项式1:") + print_expanded_polynomial(poly1) + # 输出: (1*a1)*x^0 + (2*a2)*x^1 + + print("\n平方展开后:") + expanded1 = square_polynomial(poly1) + print(f"expanded1={expanded1}") + merged1 = merge_and_simplify(expanded1) + print(f"merged1={merged1}") + print_expanded_polynomial(merged1) + # 期望: (1*a1^2)*x^0 + (4*a1*a2)*x^1 + (4*a2^2)*x^2 + + # 测试2: (2*a2)*x^0 + poly2 = { + 0: [(2, 2)] # x^0 项: 2*a2 + } + + print("\n原始多项式2:") + print_expanded_polynomial(poly2) + # 输出: (2*a2)*x^0 + + print("\n平方展开后:") + expanded2 = square_polynomial(poly2) + merged2 = merge_and_simplify(expanded2) + print_expanded_polynomial(merged2) + # 期望: (4*a2^2)*x^0 + +if __name__ == "__main__": + #demo_smoothness_indicator() + square_polynomial_test() + + + diff --git a/example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py b/example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py new file mode 100644 index 00000000..f043db33 --- /dev/null +++ b/example/figure/1d/weno/interplate/weno5_smoothness/01/weno5_smoothness_sympy.py @@ -0,0 +1,56 @@ +import sympy as sp + +def compute_weno5_beta_k(points_values, k): + """ + 计算 WENO5 子模板 k 的光滑因子 β_k,使用 SymPy 符号积分。 + + 参数: + - points_values: list of sympy symbols or floats, e.g., [v_im2, v_im1, vi] for k=0 + - k: int, 子模板索引 (0: 左偏, 1: 中心, 2: 右偏) + + 返回: + - sympy Expr: 符号表达式 of β_k + """ + if k not in [0, 1, 2]: + raise ValueError("k must be 0, 1, or 2 for WENO5") + + # 根据 k 定义点坐标 (Δx=1, 参考 x_i=0) + if k == 0: + coords = [-2, -1, 0] + elif k == 1: + coords = [-1, 0, 1] + else: # k=2 + coords = [0, 1, 2] + + x = sp.symbols('x') + v0, v1, v2 = points_values # 符号或数值 + + # 拉格朗日基函数 + l0 = ((x - coords[1]) * (x - coords[2])) / ((coords[0] - coords[1]) * (coords[0] - coords[2])) * v0 + l1 = ((x - coords[0]) * (x - coords[2])) / ((coords[1] - coords[0]) * (coords[1] - coords[2])) * v1 + l2 = ((x - coords[0]) * (x - coords[1])) / ((coords[2] - coords[0]) * (coords[2] - coords[1])) * v2 + + # 多项式 p_k(x) + p_k = l0 + l1 + l2 + + # 导数 (m=1 to 3) + dp1 = sp.diff(p_k, x) + dp2 = sp.diff(p_k, x, 2) + dp3 = sp.diff(p_k, x, 3) # 0 for quadratic + + # 积分限 + a, b = -sp.Rational(1, 2), sp.Rational(1, 2) + + # 各 m 项 + int_m1 = sp.integrate(dp1**2, (x, a, b)) + int_m2 = sp.integrate(dp2**2, (x, a, b)) + int_m3 = sp.integrate(dp3**2, (x, a, b)) + + # β_k + beta_k = int_m1 + int_m2 + int_m3 + return sp.simplify(beta_k) + +# 示例使用 (符号) +v_im2, v_im1, vi = sp.symbols('v_{i-2} v_{i-1} v_i') +beta0 = compute_weno5_beta_k([v_im2, v_im1, vi], 0) +print(beta0) # 输出: (4/3)*v_{i-2}^2 + (25/3)*v_{i-1}^2 + (10/3)*v_i^2 - (19/3)*v_{i-2}*v_{i-1} + ... \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/01/xi.py b/example/figure/1d/weno/interplate/xi/01/xi.py new file mode 100644 index 00000000..697d9d2d --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/01/xi.py @@ -0,0 +1,12 @@ +from fractions import Fraction + +i = 5 +id = 1 +j = i + id +#half = Fraction("1/2") +half = Fraction(1,2) +xia = id - half +xib = id + half +print(f'half={half}') +print(f'xia,xib={xia},{xib}') + diff --git a/example/figure/1d/weno/interplate/xi/01a/xi.py b/example/figure/1d/weno/interplate/xi/01a/xi.py new file mode 100644 index 00000000..1712ad77 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/01a/xi.py @@ -0,0 +1,16 @@ +from fractions import Fraction + +def calxi(j): + half = Fraction(1,2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +for j in range(jst, jed+1): + xia, xib = calxi(j) + #print(f'j={j:>2} xia,b=[{xia},{xib}]') + print(f'j={j:>2} xia,b=[{str(xia):>4},{str(xib):>4}]') + diff --git a/example/figure/1d/weno/interplate/xi/02/xi.py b/example/figure/1d/weno/interplate/xi/02/xi.py new file mode 100644 index 00000000..a706a3bd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02/xi.py @@ -0,0 +1,12 @@ +from fractions import Fraction + +def calxi(x,j): + return (x**(j+1))/(j+1) + +jst = 0 +jed = 4 +x = Fraction(1,2) +for j in range(0, jed+1): + v = calxi(x,j) + print(f'Intergral[({x})^{j}]={v}') + diff --git a/example/figure/1d/weno/interplate/xi/02a/xi.py b/example/figure/1d/weno/interplate/xi/02a/xi.py new file mode 100644 index 00000000..a706a3bd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02a/xi.py @@ -0,0 +1,12 @@ +from fractions import Fraction + +def calxi(x,j): + return (x**(j+1))/(j+1) + +jst = 0 +jed = 4 +x = Fraction(1,2) +for j in range(0, jed+1): + v = calxi(x,j) + print(f'Intergral[({x})^{j}]={v}') + diff --git a/example/figure/1d/weno/interplate/xi/02b/xi.py b/example/figure/1d/weno/interplate/xi/02b/xi.py new file mode 100644 index 00000000..a070f96e --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02b/xi.py @@ -0,0 +1,25 @@ +from fractions import Fraction + +def intregral_xi(x,j): + return (x**(j+1))/(j+1) + +def calxi(j): + half = Fraction(1,2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +for j in range(jst, jed+1): + xia, xib = calxi(j) + print(f'j={j:>2} xia,b=[{str(xia):>4},{str(xib):>4}]') + ist = 0 + ied = 4 + for i in range(ist, ied+1): + v1 = intregral_xi(xia,i) + v2 = intregral_xi(xib,i) + print(f' v1={v1} v2={v2} diff ={v2-v1}') + + diff --git a/example/figure/1d/weno/interplate/xi/02c/xi.py b/example/figure/1d/weno/interplate/xi/02c/xi.py new file mode 100644 index 00000000..547b34cd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02c/xi.py @@ -0,0 +1,26 @@ +from fractions import Fraction + +def intregral_xi(x,j): + return (x**(j+1))/(j+1) + +def calxi(j): + half = Fraction(1,2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +for j in range(jst, jed+1): + xia, xib = calxi(j) + print(f'j={j:>2},interval=[{str(xia):>4},{str(xib):>4}]',end=' ') + ist = 0 + ied = 4 + for i in range(ist, ied+1): + v1 = intregral_xi(xia,i) + v2 = intregral_xi(xib,i) + print(f'{v2-v1}',end=' ') + print() + + diff --git a/example/figure/1d/weno/interplate/xi/02d/xi.py b/example/figure/1d/weno/interplate/xi/02d/xi.py new file mode 100644 index 00000000..ae550bd1 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02d/xi.py @@ -0,0 +1,45 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def calxi(j): + half = Fraction(1, 2) + xia = j - half + xib = j + half + return xia, xib + +jst = -2 +jed = 2 + +# 先计算所有值,找出每列需要的最大宽度(可选,更极致对齐) +# 这里直接用固定宽度,也已经非常整齐 + +print(f"{'j':>3} {'interval':>14} i=0 i=1 i=2 i=3 i=4") +print("-" * 68) + +for j in range(jst, jed + 1): + xia, xib = calxi(j) + # 把区间格式化为固定宽度字符串 + interval_str = f"[{xia:>4},{xib:>4}]" + + line = f"{j:>2} {interval_str} " + + for i in range(0, 5): # i 从 0 到 4 + v1 = integral_xi(xia, i) + v2 = integral_xi(xib, i) + diff = v2 - v1 + + if diff == 0: + s = "0" + elif diff == 1: + s = "1" + elif diff == -1: + s = "-1" + else: + s = str(diff) + + # 每列固定 11 个字符宽度,居中对齐(足够容纳 1441/80 这类最长的) + line += f"{s:^11}" + + print(line) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/02e/xi.py b/example/figure/1d/weno/interplate/xi/02e/xi.py new file mode 100644 index 00000000..a7e4c4a4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02e/xi.py @@ -0,0 +1,17 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +print(f"{'j':>3} {'interval':>14} " + " ".join(f"i={i:^8}" for i in range(5))) +print("-" * 72) + +for j in range(-2, 3): + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + for i in range(5): + val = integral_xi(xib, i) - integral_xi(xia, i) + s = "0" if val == 0 else str(val) + print(f"{s:^11}", end="") + print() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/02f/xi.py b/example/figure/1d/weno/interplate/xi/02f/xi.py new file mode 100644 index 00000000..2d47938b --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02f/xi.py @@ -0,0 +1,24 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +data = [] +for j in range(-2, 3): + xia = Fraction(j) - Fraction(1, 2) + xib = Fraction(j) + Fraction(1, 2) + row = [j, f"[{xia}, {xib}]"] + for i in range(5): + val = integral_xi(xib, i) - integral_xi(xia, i) + row.append(0 if val == 0 else val) # 0 显示为整数 0,其他保持 Fraction + data.append(row) + +import pandas as pd +df = pd.DataFrame(data, columns=['j', 'interval'] + [f'i={i}' for i in range(5)]) + +# 关键:设置 pandas 显示选项 + 直接 print +pd.set_option('display.max_columns', None) +pd.set_option('display.width', None) +pd.set_option('display.colheader_justify', 'center') + +print(df.to_string(index=False)) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/02g/xi.py b/example/figure/1d/weno/interplate/xi/02g/xi.py new file mode 100644 index 00000000..6a7cc9d5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/02g/xi.py @@ -0,0 +1,76 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def calxi(j): + half = Fraction(1, 2) + return j - half, j + half + +jst, jed = -2, 2 + +# Step 1: 生成所有行数据(先单独处理 interval 的三部分:左括号、分数值、右括号) +rows = [] +left_brackets = [] # 全部是 "[" +right_brackets = [] # 全部是 "]" +intervals_a = [] # xia +intervals_b = [] # xib + +for j in range(jst, jed + 1): + xia, xib = calxi(j) + row = [str(j)] + for i in range(5): + diff = integral_xi(xib, i) - integral_xi(xia, i) + s = "0" if diff == 0 else ("1" if diff == 1 else ("-1" if diff == -1 else str(diff))) + row.append(s) + rows.append(row) + + left_brackets.append("[") + intervals_a.append(str(xia)) + intervals_b.append(str(xib)) + right_brackets.append("]") + +# Step 2: 标题(j 单独一列,interval 拆成 [ | 值 | ] 三列) +headers = ["j", "", "interval", "", "", "i=0", "i=1", "i=2", "i=3", "i=4"] +# 对应列: 0:j 1:[ 2:xia 3:xib 4:] 5~9:i=0~i=4 + +# Step 3: 计算每一列最大宽度 +col_widths = [0] * len(headers) + +# j 列 + 数值列 +for i, h in enumerate(headers): + col_widths[i] = max(col_widths[i], len(h)) + +for row, a, b in zip(rows, intervals_a, intervals_b): + col_widths[0] = max(col_widths[0], len(row[0])) # j + col_widths[2] = max(col_widths[2], len(a)) # xia + col_widths[3] = max(col_widths[3], len(b)) # xib + for k in range(5): # i=0~4 + col_widths[5 + k] = max(col_widths[5 + k], len(row[1 + k])) + +# 括号列固定宽度 1 就够了,但我们也算进去 +col_widths[1] = max(col_widths[1], len("[")) # 1 +col_widths[4] = max(col_widths[4], len("]")) # 1 + +# 为了美观,给每列再 +1 空格间隔(除了最右侧可以不加) +widths_with_space = [w + 1 for w in col_widths] + +# Step 4: 打印函数 +def print_line(parts, aligns=None): + if aligns is None: + aligns = ['^'] * len(parts) # 默认居中 + line = "" + for p, w, align in zip(parts, widths_with_space, aligns): + line += f"{p:{align}{w}}" + print(line.rstrip()) # 去掉行末多余空格 + +# 表头(interval 跨三列,我们手动合并显示) +print_line(headers[:5] + headers[5:], aligns=['^','^','^','^','^','^','^','^','^','^']) +print_line(["", "interval","","","","i=0","i=1","i=2","i=3","i=4"], + aligns=['<','^','^','^','>','^','^','^','^','^']) # interval 标题居中跨三列 +print("-" * (sum(widths_with_space) - 1)) + +# 数据行 +for j_row, lb, a, b, rb, data_row in zip(rows, left_brackets, intervals_a, intervals_b, right_brackets, rows): + parts = [data_row[0], lb, a, b, rb] + data_row[1:] + print_line(parts) diff --git a/example/figure/1d/weno/interplate/xi/03/xi.py b/example/figure/1d/weno/interplate/xi/03/xi.py new file mode 100644 index 00000000..7385a902 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03/xi.py @@ -0,0 +1,18 @@ +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +print(f"{'j':>3} {'interval':>14} " + " ".join(f"i={i:^8}" for i in range(5))) +print("-" * 72) + +vv = [-1,0,1] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + for i in range(5): + val = integral_xi(xib, i) - integral_xi(xia, i) + s = "0" if val == 0 else str(val) + print(f"{s:^11}", end="") + print() \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03a/xi.py b/example/figure/1d/weno/interplate/xi/03a/xi.py new file mode 100644 index 00000000..1196f356 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03a/xi.py @@ -0,0 +1,28 @@ +import numpy as np +from fractions import Fraction + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +vv = [-1,0,1] +isize = len(vv) +print(f"{'j':>3} {'interval':>14} " + " ".join(f"i={i:^8}" for i in range(isize))) +print("-" * 72) + +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + a_list = [] + for i in range( isize ): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + s = "0" if val == 0 else str(val) + print(f"{s:^11}", end="") + print() + arrays_list.append(a_list) + +# 使用 vstack 函数将列表中的数组堆叠成一个矩阵 +matrix = np.vstack(arrays_list) +print(f'matrix={matrix}') \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03b/xi.py b/example/figure/1d/weno/interplate/xi/03b/xi.py new file mode 100644 index 00000000..bd34b1ae --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03b/xi.py @@ -0,0 +1,88 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix): + # 将矩阵转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in matrix]) + + # 转换为字符串矩阵并计算每列的最大宽度 + str_matrix = [] + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + col_widths = [0] * cols # 每列的最大宽度 + + # 将数字转换为字符串,并记录每列最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 打印矩阵,每列等宽右对齐,添加逗号 + #print("Matrix in Fraction Form:") + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + # 右对齐,使用该列的最大宽度 + formatted_element = f"{element:>{col_widths[j]}}" + # 除最后一列外添加逗号和空格 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + # 拼接一行并打印 + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + + +vv = [-1,0,1] +isize = len(vv) +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + #print(f"{j:>3} [{xia:>4},{xib:>4}] ", end="") + a_list = [] + for i in range( isize ): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + s = "0" if val == 0 else str(val) + #print(f"{s:^11}", end="") + #print() + arrays_list.append(a_list) + +# 使用 vstack 函数将列表中的数组堆叠成一个矩阵 +matrix = np.vstack(arrays_list) +print_matrix_fraction(matrix) + +# 计算逆矩阵 +inverse = inverse_matrix(matrix) + +print("\nInverse Matrix in Fraction Form:") +print_matrix_fraction(inverse) + +# 计算两个矩阵的乘积 +product = np.dot(matrix, inverse) + +print("\nProduct of Matrix and Inverse Matrix:") +print_matrix_fraction(product) diff --git a/example/figure/1d/weno/interplate/xi/03c/xi.py b/example/figure/1d/weno/interplate/xi/03c/xi.py new file mode 100644 index 00000000..7789fb39 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03c/xi.py @@ -0,0 +1,128 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix): + # 将矩阵转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in matrix]) + + # 转换为字符串矩阵并计算每列的最大宽度 + str_matrix = [] + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + col_widths = [0] * cols # 每列的最大宽度 + + # 将数字转换为字符串,并记录每列最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 打印矩阵,每列等宽右对齐,添加逗号 + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + # 右对齐,使用该列的最大宽度 + formatted_element = f"{element:>{col_widths[j]}}" + # 除最后一列外添加逗号和空格 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + # 拼接一行并打印 + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A"): + """ + 输出矩阵的 LaTeX 格式 + :param matrix: 输入矩阵(列表嵌套或numpy数组) + :param matrix_name: 矩阵名称(用于注释) + """ + # 转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + # 整数直接输出分子 + row_elements.append(f"{f.numerator}") + else: + # 分数输出为 \frac{分子}{分母} + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 每行元素用 & 分隔,行尾加 \\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 打印LaTeX格式(用 === 分隔,方便复制) + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +vv = [-1,0,1] +isize = len(vv) +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(isize): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +# 使用 vstack 函数将列表中的数组堆叠成一个矩阵 +matrix = np.vstack(arrays_list) + +# 打印原始矩阵(分数字符串格式 + LaTeX格式) +print("Original Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算逆矩阵 +inverse = inverse_matrix(matrix) + +# 打印逆矩阵(分数字符串格式 + LaTeX格式) +print("Inverse Matrix in Fraction Form:") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算两个矩阵的乘积 +product = np.dot(matrix, inverse) + +# 打印乘积矩阵(分数字符串格式 + LaTeX格式) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03d/xi.py b/example/figure/1d/weno/interplate/xi/03d/xi.py new file mode 100644 index 00000000..0156753a --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03d/xi.py @@ -0,0 +1,169 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +vv = [-1,0,1] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xx = compute_coef(Fraction(1,2), k) +print(f"xx(一维行向量):") +print_matrix_fraction(xx) # 默认行向量格式 +print_matrix_latex(xx, "Vector xx (Row Vector)") # LaTeX行向量 + +# 可选:按列向量格式打印xx +print(f"\nxx(一维列向量):") +print_matrix_fraction(xx, is_column_vector=True) +print_matrix_latex(xx, "Vector xx (Column Vector)", is_column_vector=True) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/03e/xi.py b/example/figure/1d/weno/interplate/xi/03e/xi.py new file mode 100644 index 00000000..f729d67f --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/03e/xi.py @@ -0,0 +1,173 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +vv = [-1,0,1] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xx = compute_coef(Fraction(1,2), k) +print(f"xx(一维行向量):") +print_matrix_fraction(xx) # 默认行向量格式 +print_matrix_latex(xx, "Vector xx (Row Vector)") # LaTeX行向量 + +# 可选:按列向量格式打印xx +print(f"\nxx(一维列向量):") +print_matrix_fraction(xx, is_column_vector=True) +print_matrix_latex(xx, "Vector xx (Column Vector)", is_column_vector=True) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") + +yy = np.dot(xx, inverse) +print(f"yy:") +print_matrix_fraction(yy) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/04/xi.py b/example/figure/1d/weno/interplate/xi/04/xi.py new file mode 100644 index 00000000..d50ab397 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/04/xi.py @@ -0,0 +1,212 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = f'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0: + if var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + else: + fv1 = sf + ' ' + str(ff) + + var = f'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") + +xpcoef = np.dot(xp, inverse) +xncoef = np.dot(xn, inverse) +print(f"xpcoef:") +print_matrix_fraction(xpcoef) + +print_formula(xpcoef,vv) + +print(f"xncoef:") +print_matrix_fraction(xncoef) +print_formula(xncoef,vv) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/04a/xi.py b/example/figure/1d/weno/interplate/xi/04a/xi.py new file mode 100644 index 00000000..9686e05c --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/04a/xi.py @@ -0,0 +1,209 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = f'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = f'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +# 构建二维矩阵并打印 +arrays_list = [] +for j in vv: + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(k): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + +matrix = np.vstack(arrays_list) +print("\nOriginal Matrix in Fraction Form:") +print_matrix_fraction(matrix) +print_matrix_latex(matrix, "Original Matrix") + +# 计算并打印逆矩阵 +inverse = inverse_matrix(matrix) +print(f"inverse(二维矩阵):") +print_matrix_fraction(inverse) +print_matrix_latex(inverse, "Inverse Matrix") + +# 计算并打印乘积矩阵 +product = np.dot(matrix, inverse) +print("Product of Matrix and Inverse Matrix:") +print_matrix_fraction(product) +print_matrix_latex(product, "Matrix Product (Identity Matrix)") + +xpcoef = np.dot(xp, inverse) +xncoef = np.dot(xn, inverse) +print(f"xpcoef:") +print_matrix_fraction(xpcoef) + +print_formula(xpcoef,vv) + +print(f"xncoef:") +print_matrix_fraction(xncoef) +print_formula(xncoef,vv) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05/xi.py b/example/figure/1d/weno/interplate/xi/05/xi.py new file mode 100644 index 00000000..1ed9d853 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05/xi.py @@ -0,0 +1,192 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a = [Fraction(s) for s in a_str] +print_matrix_fraction(a) +ii = 2 +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) diff --git a/example/figure/1d/weno/interplate/xi/05a/xi.py b/example/figure/1d/weno/interplate/xi/05a/xi.py new file mode 100644 index 00000000..e0dd67a4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05a/xi.py @@ -0,0 +1,205 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + diff --git a/example/figure/1d/weno/interplate/xi/05b/xi.py b/example/figure/1d/weno/interplate/xi/05b/xi.py new file mode 100644 index 00000000..6627f525 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05b/xi.py @@ -0,0 +1,229 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + if f.denominator == 1: + s = f"{f.numerator}" + else: + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05c/xi.py b/example/figure/1d/weno/interplate/xi/05c/xi.py new file mode 100644 index 00000000..ef102a67 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05c/xi.py @@ -0,0 +1,246 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +arr = np.empty((rows, cols), dtype=object) +print(arr) +for i in range(rows): + for j in range(cols): + arr[i, j] = Fraction(i+1, j+1) +print(arr) +print_matrix_fraction(arr) + +for i in range(rows): + for j in range(cols): + arr[i, j] = alist[i][j] + +print_matrix_fraction(arr) \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05d/xi.py b/example/figure/1d/weno/interplate/xi/05d/xi.py new file mode 100644 index 00000000..46371b34 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05d/xi.py @@ -0,0 +1,269 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +arr = np.empty((rows, cols), dtype=object) + +for i in range(rows): + for j in range(cols): + arr[i, j] = alist[i][j] + +print_matrix_fraction(arr) +print_matrix_fraction(alist) + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + mystr += coef_tostring(alist[i][j],j)+ f"*v[i{var_rj}]" + print(f'mystr={mystr}') + + \ No newline at end of file diff --git a/example/figure/1d/weno/interplate/xi/05e/xi.py b/example/figure/1d/weno/interplate/xi/05e/xi.py new file mode 100644 index 00000000..1e9bacfb --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05e/xi.py @@ -0,0 +1,281 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +widths = np.empty(M, dtype=int) + +for j in range(rows): + w = 0 + for i in range(cols): + ww = len( coef_tostring(alist[i][j],j) ) + w = max(w, ww) + widths[j] = w + +print(f'widths={widths}') + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + mystr += coef_tostring(alist[i][j],j)+ f"*v[i{var_rj}]" + print(f'mystr={mystr}') + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + ss = f"{coef_tostring(alist[i][j],j):>{widths[j]}}" + mystr += ss+ f"*v[i{var_rj}]" + print(f'mystr={mystr}') diff --git a/example/figure/1d/weno/interplate/xi/05f/xi.py b/example/figure/1d/weno/interplate/xi/05f/xi.py new file mode 100644 index 00000000..75a32061 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05f/xi.py @@ -0,0 +1,284 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +N = len(a) +M = len(a0) +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +widths = np.empty(M, dtype=int) + +for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + +print(f'widths={widths}') + +for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'mystr={mystr}') diff --git a/example/figure/1d/weno/interplate/xi/05g/xi.py b/example/figure/1d/weno/interplate/xi/05g/xi.py new file mode 100644 index 00000000..301595c3 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/05g/xi.py @@ -0,0 +1,310 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi+1/2,r={r}={mystr}') + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), k) +xn = compute_coef(-Fraction(1,2), k) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +coef_matrix = np.array(alist) + +N = len(a) +M = len(a0) + +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +print_coef_formula(coef_matrix) + +kk=3 +for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + print("\nOriginal Matrix in Fraction Form:") + print_matrix_fraction(matrix) diff --git a/example/figure/1d/weno/interplate/xi/06/xi.py b/example/figure/1d/weno/interplate/xi/06/xi.py new file mode 100644 index 00000000..740d7652 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06/xi.py @@ -0,0 +1,321 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi+1/2,r={r}={mystr}') + +#vv = [-1,0,1] +vv = [-2,-1,0,1,2] +k = len(vv) + +""" + r | j=0 j=1 j=2 +-1 | 11/6 -7/6 1/3 (i+1,i+2,i+3) + 0 | 1/3 5/6 -1/6 (i ,i+1,i+2) + 1 | -1/6 5/6 1/3 (i-1,i ,i+1) + 2 | 1/3 -7/6 11/6 (i-2,i-1,i ) +""" + +a_str = ["1/30", "-13/60", "47/60", "9/20", "-1/20"] +a0_str = ["1/3", "5/6", "-1/6"] +a1_str = ["-1/6", "5/6", "1/3"] +a2_str = ["1/3", "-7/6", "11/6"] +a = [Fraction(s) for s in a_str] +a0 = [Fraction(s) for s in a0_str] +a1 = [Fraction(s) for s in a1_str] +a2 = [Fraction(s) for s in a2_str] +print_matrix_fraction(a) +print_matrix_fraction(a0) +print_matrix_fraction(a1) +print_matrix_fraction(a2) + +alist = [] +alist.append(a0) +alist.append(a1) +alist.append(a2) + +coef_matrix = np.array(alist) + +N = len(a) +M = len(a0) + +frac_list = [Fraction(0) for _ in range(N)] +print_matrix_fraction(frac_list) + +im = 2 +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( id, end=' ') + print() + +for r in range(M): + print( f'r={r}:', end='') + for j in range(M): + id = im - r + j + print( alist[r][j], end=' ') + print() + +dd = [Fraction(0) for _ in range(M)] +dd[0] = Fraction(3,10) +dd[1] = Fraction(6,10) +dd[2] = Fraction(1,10) + +for r in range(M): + for j in range(M): + id = im - r + j + frac_list[id] += dd[r]*alist[r][j] + + +print_matrix_fraction(frac_list) + +rows=M +cols=M +print_matrix_fraction(alist) + +print_coef_formula(coef_matrix) + +kk=3 +# 测试一维向量xx的打印(支持行向量/列向量两种格式) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +rows_list = [] +for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + #print(f'r={r}') + #print("\nOriginal Matrix in Fraction Form:") + #print_matrix_fraction(matrix) + inverse = inverse_matrix(matrix) + yy = np.dot(xp, inverse) + #print(f"yy:") + #print_matrix_fraction(yy) + rows_list.append(yy) + +mymat = np.vstack(rows_list) + +print_matrix_fraction(mymat) diff --git a/example/figure/1d/weno/interplate/xi/06a/xi.py b/example/figure/1d/weno/interplate/xi/06a/xi.py new file mode 100644 index 00000000..c2848b4d --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06a/xi.py @@ -0,0 +1,249 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + + +def print_formula(xpcoef,vv): + #print(f'print_formula vv={vv}') + ii = 0 + strr = '' + for v in vv: + absv = abs(v) + s = '' + t = str(absv) + if v > 0: + s='+' + elif v < 0: + s='-' + else: + t = '' + var1 = xpcoef[ii] + absv1 = abs(var1) + ff = r'\cfrac{{{absv1.numerator}}}{{{absv1.denominator}}}' + if var1 >= 0: + sf = '+' + else: + sf = '-' + + if ii == 0 and var1 >= 0: + fv1 = ff + else: + fv1 = sf + ' ' + str(ff) + + var = r'\overline{{v}}_{{i{s}{t}}}' + strr += f'{fv1}' + var + ii += 1 + + print(strr) + + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi+1/2,r={r}={mystr}') + +kk=3 +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +rows_list = [] +for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + #print(f'r={r}') + #print("\nOriginal Matrix in Fraction Form:") + #print_matrix_fraction(matrix) + inverse = inverse_matrix(matrix) + yy = np.dot(xp, inverse) + #print(f"yy:") + #print_matrix_fraction(yy) + rows_list.append(yy) + +mymat = np.vstack(rows_list) + +print_matrix_fraction(mymat) +print_coef_formula(mymat) diff --git a/example/figure/1d/weno/interplate/xi/06b/xi.py b/example/figure/1d/weno/interplate/xi/06b/xi.py new file mode 100644 index 00000000..38a45923 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06b/xi.py @@ -0,0 +1,223 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def print_matrix_latex(matrix, matrix_name="A", is_column_vector=False): + """ + 支持一维向量和二维矩阵的LaTeX格式打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param matrix_name: 矩阵名称(注释用) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + if np.ndim(matrix) == 1: + if is_column_vector: + two_d_matrix = [[x] for x in matrix] # 列向量:N×1 + else: + two_d_matrix = [matrix] # 行向量:1×N + else: + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:构建LaTeX字符串 + latex_lines = [] + latex_lines.append(f"% LaTeX 格式:{matrix_name}") + # 向量用bmatrix,矩阵也用bmatrix(统一风格,可改为pmatrix等) + latex_lines.append("\\begin{bmatrix}") + + for i in range(rows): + row_elements = [] + for j in range(cols): + f = fraction_matrix[i][j] + if f.denominator == 1: + row_elements.append(f"{f.numerator}") + else: + row_elements.append(f"\\frac{{{f.numerator}}}{{{f.denominator}}}") + # 行内元素用&分隔,行尾加\\ + latex_lines.append(" & ".join(row_elements) + " \\\\") + + latex_lines.append("\\end{bmatrix}") + latex_str = "\n".join(latex_lines) + + # 步骤4:打印LaTeX格式 + print(f"\n{'='*50}") + print(f"LaTeX 格式 - {matrix_name}:") + print(latex_str) + print(f"{'='*50}\n") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac): + rows, cols = alist.shape + print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + rj = -r + j + var_rj = id_tostring(rj) + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + #print(f'ss,absv={ss}{absv}') + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + if xfrac >= 0: + sxf='+' + print(f'vi{sxf}{xfrac},r={r}={mystr}') + +def calc_coef_formula(kk, xfrac): + xm = compute_coef(xfrac, kk) + rows_list = [] + for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + inverse = inverse_matrix(matrix) + ym = np.dot(xm, inverse) + rows_list.append(ym) + + return np.vstack(rows_list) + + +kk=3 +xfrac = Fraction(1,2) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +mymat1 = calc_coef_formula(kk, xfrac) +mymat2 = calc_coef_formula(kk, -xfrac) + +print_matrix_fraction(mymat1) +print_coef_formula(mymat1,xfrac) + +print_matrix_fraction(mymat2) +print_coef_formula(mymat2,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/06c/xi.py b/example/figure/1d/weno/interplate/xi/06c/xi.py new file mode 100644 index 00000000..fc444687 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/06c/xi.py @@ -0,0 +1,178 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def calc_coef_formula(kk, xfrac): + xm = compute_coef(xfrac, kk) + rows_list = [] + for r in range(kk): + #-r+l + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + inverse = inverse_matrix(matrix) + ym = np.dot(xm, inverse) + rows_list.append(ym) + + return np.vstack(rows_list) + + +kk=3 +xfrac = Fraction(1,2) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +mymat1 = calc_coef_formula(kk, xfrac) +mymat2 = calc_coef_formula(kk, -xfrac) + +print_matrix_fraction(mymat1) +print_coef_formula(mymat1,xfrac) + +print_matrix_fraction(mymat2) +print_coef_formula(mymat2,-xfrac,1) + diff --git a/example/figure/1d/weno/interplate/xi/07/xi.py b/example/figure/1d/weno/interplate/xi/07/xi.py new file mode 100644 index 00000000..c22ebb42 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07/xi.py @@ -0,0 +1,195 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + #xm = compute_coef(xfrac, kk) + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +kk=3 +xfrac = Fraction(1,2) +xp = compute_coef(Fraction(1,2), kk) +xn = compute_coef(-Fraction(1,2), kk) + +mymat1 = calc_coef_formula(kk, xfrac) +mymat2 = calc_coef_formula(kk, -xfrac) + +print_matrix_fraction(mymat1) +print_coef_formula(mymat1,xfrac) + +print_matrix_fraction(mymat2) +#print_coef_formula(mymat2,-xfrac,1) +print_coef_formula(mymat2,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/07a/xi.py b/example/figure/1d/weno/interplate/xi/07a/xi.py new file mode 100644 index 00000000..e50394c3 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07a/xi.py @@ -0,0 +1,200 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(rows, dtype=int) + + for j in range(rows): + w = 0 + for i in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +#print_coef_formula(mymat3R,-xfrac,1) +print_coef_formula(mymat3R,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/07b/xi.py b/example/figure/1d/weno/interplate/xi/07b/xi.py new file mode 100644 index 00000000..628a5df5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07b/xi.py @@ -0,0 +1,211 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + if rows == 1: + r = rin + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrix = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrix) +print_coef_formula(row_matrix,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrix = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrix) +print_coef_formula(row_matrix,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +#print_coef_formula(mymat3R,-xfrac,1) +print_coef_formula(mymat3R,-xfrac) + diff --git a/example/figure/1d/weno/interplate/xi/07c/xi.py b/example/figure/1d/weno/interplate/xi/07c/xi.py new file mode 100644 index 00000000..6b5f322d --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07c/xi.py @@ -0,0 +1,270 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + for i in range(rows): + mystr = '' + r = i + if rows == 1: + r = rin + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + #print(f'vi{sxf}{xfrac_new}({slr}),r={r}={mystr}') + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + print(f'rj_dict={rj_dict}') + print(sorted(rj_dict.items())) + for rj, pairs in sorted(rj_dict.items()): + print(rj, pairs) + for rj, pairs in sorted(rj_dict.items()): + readable = [(idx, str(frac)) for idx, frac in pairs] + print(rj, readable) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07d/xi.py b/example/figure/1d/weno/interplate/xi/07d/xi.py new file mode 100644 index 00000000..a9380aa1 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07d/xi.py @@ -0,0 +1,264 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + for i in range(rows): + mystr = '' + r = i + if rows == 1: + r = rin + for j in range(cols): + absv,ss = coef_toabsstring(alist[i][j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = ishift - r + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'vi{sxf}{xfrac_new}({slr}),{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07e/xi.py b/example/figure/1d/weno/interplate/xi/07e/xi.py new file mode 100644 index 00000000..3d47d9e5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07e/xi.py @@ -0,0 +1,267 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + for j in range(cols): + absv,ss = coef_toabsstring(row[j]) + if j == 0 and ss == '+': + ss = ' ' + tt = f"{absv:>{widths[j]-1}}" + rj = jstart + j + var_rj = id_tostring(rj) + mystr += ss + tt + f"*v[i{var_rj}]" + print(f'{vstr},{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07f/xi.py b/example/figure/1d/weno/interplate/xi/07f/xi.py new file mode 100644 index 00000000..19a6ecb5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07f/xi.py @@ -0,0 +1,274 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + var_rj_list = cal_iplus_index(jstart,cols) + for j in range(cols): + absv,ss = coef_toabsstring(row[j]) + if j == 0 and ss == '+': + ss = ' ' + ss = f"{ss}{absv:>{widths[j]-1}}" + mystr += ss + f"*v[i{var_rj_list[j]}]" + print(f'{vstr},{r}={mystr}') + print() + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07g/xi.py b/example/figure/1d/weno/interplate/xi/07g/xi.py new file mode 100644 index 00000000..0e186ef4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07g/xi.py @@ -0,0 +1,326 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac, shift, row_index): + """ + 构建左边标签,如 'vi+1/2(-),0' + x_frac: 分数部分(如 1/2),建议传入字符串 '1/2' + shift: 整体偏移 + row_index: 当前行号(用于 ,0, ,1, ,2) + """ + total_offset = x_frac + shift + sign_char = '+' if total_offset >= 0 else '' + # 注意:x_frac 是字符串,如 '1/2',所以 total_offset 需为字符串 + # 为简化,假设 x_frac 是字符串(如 '1/2'),shift 是整数 + if shift == 0: + offset_str = f"+{x_frac}" if total_offset >= 0 else f"{x_frac}" + else: + # 更健壮的做法:让调用者传入完整偏移字符串 + offset_str = f"{sign_char}{total_offset}" # 需要重新设计此处逻辑 + + # 简化:假设 x_frac 是字符串(如 '1/2'),整体偏移用 shift 控制 + # 实际中,建议将 x_frac 设计为字符串,如 "1/2" + lhs_base = f"vi+{x_frac}" if shift == 0 else f"vi{total_offset}" + lr_sign = '-' if x_frac.startswith('-') or (isinstance(x_frac, str) and x_frac[0].isdigit()) else '+' + # 但根据你原始逻辑:当 xfrac < 0 时 slr='+', 否则 '-' + # 所以更准确的是: + lr_sign = '-' if x_frac >= 0 else '+' + return f"vi{'+' if total_offset >= 0 else ''}{total_offset}({lr_sign})" + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + var_rj_list = cal_iplus_index(jstart,cols) + for j in range(cols): + ss = format_signed_coef(row[j],j==0,widths[j]) + ioffset_str = build_variable_index_string(jstart + j) + print(f'ioffset_str={ioffset_str}') + mystr += ss + f"*v{ioffset_str}" + print(f'{vstr},{r}={mystr}') + print() + + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07h/xi.py b/example/figure/1d/weno/interplate/xi/07h/xi.py new file mode 100644 index 00000000..cc42d5cd --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07h/xi.py @@ -0,0 +1,338 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def print_coef_formula(alist,xfrac,ishift=0,rin=0): + rows, cols = alist.shape + #print(f'rows,cols={rows},{cols}') + + widths = np.empty(cols, dtype=int) + + for j in range(cols): + w = 0 + for i in range(rows): + absv,ss = coef_toabsstring(alist[i][j]) + ww = len( absv ) + 1 + w = max(w, ww) + widths[j] = w + + #print(f'widths={widths}') + + sxf = '' + xfrac_new = xfrac + ishift + if xfrac_new >= 0: + sxf='+' + slr='-' + if xfrac < 0: + slr='+' + vstr = f'vi{sxf}{xfrac_new}({slr})' + print(f'vstr={vstr}') + lhs_label = build_lhs_label(xfrac, ishift) + print(f'lhs_label={lhs_label}') + for i in range(rows): + r = i + row = alist[i] + if rows == 1: + r = rin + mystr = '' + jstart = ishift - r + ioffset_strs = build_variable_indices(jstart, cols) + for j in range(cols): + ss = format_signed_coef(row[j],j==0,widths[j]) + mystr += ss + f"*v{ioffset_strs[j]}" + print(f'{vstr},{r}={mystr}') + print() + + # 示例 1: x_frac = 1/2, shift = 0 + print(build_lhs_label(Fraction(1, 2), 0)) + # 输出: vi+1/2(-) + + # 示例 2: x_frac = -1/2, shift = 0 + print(build_lhs_label(Fraction(-1, 2), 0)) + # 输出: vi-1/2(+) + + # 示例 3: x_frac = 1/2, shift = -1 + print(build_lhs_label(Fraction(1, 2), -1)) + # total_offset = -1 + 1/2 = -1/2 → "vi-1/2(-)" + # 注意:方向仍由 x_frac=1/2≥0 → '-' + # 输出: vi-1/2(-) + + # 示例 4: x_frac = -3/2, shift = 2 + print(build_lhs_label(Fraction(-3, 2), 2)) + # total_offset = 2 - 3/2 = 1/2 → "vi+1/2" + # direction: x_frac < 0 → '+' + # 输出: vi+1/2(+) + + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07i/xi.py b/example/figure/1d/weno/interplate/xi/07i/xi.py new file mode 100644 index 00000000..3555e169 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07i/xi.py @@ -0,0 +1,318 @@ +import numpy as np +from fractions import Fraction + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + #if f.denominator == 1: + # s = f"{f.numerator}" + #else: + # s = f"{f.numerator}/{f.denominator}" + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def print_coef_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + terms = [] + jstart = ishift - r + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + """ + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + rows_ref,cols_ref=1,5 + rows,cols=3,3 + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + -2 [(2, Fraction(1, 3))] + -1 [(1, Fraction(-1, 6)), (2, Fraction(-7, 6))] + 0 [(0, Fraction(1, 3)), (1, Fraction(5, 6)), (2, Fraction(11, 6))] + 1 [(0, Fraction(5, 6)), (1, Fraction(1, 3))] + 2 [(0, Fraction(-1, 6))] + -2 [(2, 1/3)] + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + #print(f'rj_dict={rj_dict}') + #print(sorted(rj_dict.items())) + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + + + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_coef_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_coef_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_coef_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_coef_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_coef_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_coef_formula(mymat3R,-xfrac) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(mymat3L) + +printhhh(row_matrixL, mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/07j/xi.py b/example/figure/1d/weno/interplate/xi/07j/xi.py new file mode 100644 index 00000000..c6806185 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/07j/xi.py @@ -0,0 +1,374 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat, rbase, xfrac): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + + print_matrix_fraction(row_matrix) + print_matrix_fraction(mymat) + + print(f'rbase={rbase}') + + print_stencil_formula(row_matrix,xfrac,0,rbase) + + print_stencil_formula(mymat,xfrac) + + """ + rows_ref,cols_ref=1,5 + rows,cols=3,3 + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + rbase=2 + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + rj_set={0, 1, 2, -2, -1} + sorted_rj_set=[-2, -1, 0, 1, 2] + -2 [(2, 1/3)] + -1 [(1, -1/6), (2, -7/6)] + 0 [(0, 1/3), (1, 5/6), (2, 11/6)] + 1 [(0, 5/6), (1, 1/3)] + 2 [(0, -1/6)] + vi+1/2(-)=d[0]*( 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2]) + +d[1]*(-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1]) + +d[2]*( 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ]) + -2 [(2, 1/3)] 表示下标-2也就是v[i-2]对应的系数为d[2]*1/3 + 这个系数应该和vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + 里面的v[i-2]系数一致,也就是d[2]*1/3v[i-2]=1/30v[i-2],从而求出d[2],同理可以求出d[0], + 最后求出d[1],我的想法是一步步搞清楚到底该怎么做,在思维不清晰之前先使用代码将各项表达式对应打印出来, + 第一步-2 [(2, 1/3)]已经做到了,但是不直观。第二步应该将其变化为 + v[i-2]: [(d[2]*1/3)=1/30] + v[i-1]: [(d[1]*(-1/6)+d[2]*( -7/6)=-13/60],以此类推。你有什么建议,并给出代码以便于进一步讨论。 + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + +def build_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def testfun(mymat): + sub_stencils = mymat + target_dict = { + -2: Fraction(1, 30), + -1: Fraction(-13, 60), + 0: Fraction(47, 60), + 1: Fraction(9, 20), + 2: Fraction(-1, 20) + } + print_weno_equations(sub_stencils, target_dict) + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_stencil_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_stencil_formula(mymat3R,-xfrac) + +#printhhh(row_matrixL, mymat3L, r, xfrac) + +testfun(mymat3L) diff --git a/example/figure/1d/weno/interplate/xi/08/xi.py b/example/figure/1d/weno/interplate/xi/08/xi.py new file mode 100644 index 00000000..4277ea57 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08/xi.py @@ -0,0 +1,392 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat, rbase, xfrac): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + + print_matrix_fraction(row_matrix) + print_matrix_fraction(mymat) + + print(f'rbase={rbase}') + + print_stencil_formula(row_matrix,xfrac,0,rbase) + + print_stencil_formula(mymat,xfrac) + + """ + rows_ref,cols_ref=1,5 + rows,cols=3,3 + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + rbase=2 + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + rj_set={0, 1, 2, -2, -1} + sorted_rj_set=[-2, -1, 0, 1, 2] + -2 [(2, 1/3)] + -1 [(1, -1/6), (2, -7/6)] + 0 [(0, 1/3), (1, 5/6), (2, 11/6)] + 1 [(0, 5/6), (1, 1/3)] + 2 [(0, -1/6)] + vi+1/2(-)=d[0]*( 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2]) + +d[1]*(-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1]) + +d[2]*( 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ]) + -2 [(2, 1/3)] 表示下标-2也就是v[i-2]对应的系数为d[2]*1/3 + 这个系数应该和vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + 里面的v[i-2]系数一致,也就是d[2]*1/3v[i-2]=1/30v[i-2],从而求出d[2],同理可以求出d[0], + 最后求出d[1],我的想法是一步步搞清楚到底该怎么做,在思维不清晰之前先使用代码将各项表达式对应打印出来, + 第一步-2 [(2, 1/3)]已经做到了,但是不直观。第二步应该将其变化为 + v[i-2]: [(d[2]*1/3)=1/30] + v[i-1]: [(d[1]*(-1/6)+d[2]*( -7/6)=-13/60],以此类推。你有什么建议,并给出代码以便于进一步讨论。 + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + +def build_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + offset_map[k].append(coef) + return offset_map + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_offset_map(sub_stencils) + print(f'offset_map={offset_map}') + all_offsets = sorted(target_dict.keys()) + print(f'all_offsets={all_offsets}') + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def testfun(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = { + -2: Fraction(1, 30), + -1: Fraction(-13, 60), + 0: Fraction(47, 60), + 1: Fraction(9, 20), + 2: Fraction(-1, 20) + } + + offset_map = build_offset_map_r(row_matrixL,2) + print(f'offset_map={offset_map}') + print(f'target_dict={target_dict}') + + print_weno_equations(sub_stencils, target_dict) + + +# i-2 i-1, i i+1 i+2 +# vi+1/2(-),r=sum crl*vi-r+l,l=1,kk-1; +# kk=5 +# r=0: -r+l=0,1,2,3,4:i,i+1,i+2,i+3,i+4 +# r=1: -r+l=-1,0,1,2,3:i-1,i,i+1,i+2,i+3 +# r=2: -r+l=-2,-1,0,1,2:i-2,i-1,i,i+1,i+2 + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +print_matrix_fraction(mymat5L) +print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixL) +print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +print_matrix_fraction(mymat5R) +print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +print_matrix_fraction(row_matrixR) +print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print_stencil_formula(mymat3L,xfrac) + +print_matrix_fraction(mymat3R) +print_stencil_formula(mymat3R,-xfrac) + +#printhhh(row_matrixL, mymat3L, r, xfrac) + +testfun(row_matrixL, mymat3L, r) diff --git a/example/figure/1d/weno/interplate/xi/08a/xi.py b/example/figure/1d/weno/interplate/xi/08a/xi.py new file mode 100644 index 00000000..d128adb4 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08a/xi.py @@ -0,0 +1,440 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + + +def printhhh(row_matrix, mymat, rbase, xfrac): + rows_ref, cols_ref = row_matrix.shape + print(f'rows_ref,cols_ref={rows_ref},{cols_ref}') + rows, cols = mymat.shape + print(f'rows,cols={rows},{cols}') + + print_matrix_fraction(row_matrix) + print_matrix_fraction(mymat) + + print(f'rbase={rbase}') + + print_stencil_formula(row_matrix,xfrac,0,rbase) + + print_stencil_formula(mymat,xfrac) + + """ + rows_ref,cols_ref=1,5 + rows,cols=3,3 + [ 1/30, -13/60, 47/60, 9/20, -1/20 ] + [ 1/3, 5/6, -1/6 ] + [ -1/6, 5/6, 1/3 ] + [ 1/3, -7/6, 11/6 ] + rbase=2 + vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + + r=0:0 1 2 + r=1:-1 0 1 + r=2:-2 -1 0 + rj_set={0, 1, 2, -2, -1} + sorted_rj_set=[-2, -1, 0, 1, 2] + -2 [(2, 1/3)] + -1 [(1, -1/6), (2, -7/6)] + 0 [(0, 1/3), (1, 5/6), (2, 11/6)] + 1 [(0, 5/6), (1, 1/3)] + 2 [(0, -1/6)] + vi+1/2(-)=d[0]*( 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2]) + +d[1]*(-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1]) + +d[2]*( 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ]) + -2 [(2, 1/3)] 表示下标-2也就是v[i-2]对应的系数为d[2]*1/3 + 这个系数应该和vi+1/2(-),2= 1/30*v[i-2]-13/60*v[i-1]+47/60*v[i ]+9/20*v[i+1]-1/20*v[i+2] + 里面的v[i-2]系数一致,也就是d[2]*1/3v[i-2]=1/30v[i-2],从而求出d[2],同理可以求出d[0], + 最后求出d[1],我的想法是一步步搞清楚到底该怎么做,在思维不清晰之前先使用代码将各项表达式对应打印出来, + 第一步-2 [(2, 1/3)]已经做到了,但是不直观。第二步应该将其变化为 + v[i-2]: [(d[2]*1/3)=1/30] + v[i-1]: [(d[1]*(-1/6)+d[2]*( -7/6)=-13/60],以此类推。你有什么建议,并给出代码以便于进一步讨论。 + """ + rj_set = set() + rj_dict = {} + for i in range(rows): + r = i + print(f'r={r}',end=':') + for j in range(cols): + rj = - r + j + rj_set.add(rj) + print(f'{rj}',end=' ') + rj_dict.setdefault(rj, []).append((r,mymat[i][j])) + print() + print(f'rj_set={rj_set}') + sorted_rj_set = sorted(rj_set) + print(f'sorted_rj_set={sorted_rj_set}') + + for rj, pairs in sorted(rj_dict.items()): + # 将每个元组格式化为 (idx, 分数) 字符串 + pair_str = ", ".join(f"({idx}, {frac})" for idx, frac in pairs) + print(f"{rj} [{pair_str}]") + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + #offset_map[k].append(coef) + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def testfun(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,2) + print_weno_equations(sub_stencils, target_dict) + + my_row_matrix = np.array([[ + Fraction(1,30), Fraction(-13,60), Fraction(47,60), Fraction(9,20), Fraction(-1,20) + ]], dtype=object) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-2) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +#print_matrix_fraction(mymat5L) +#print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixL) +#print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +#print_matrix_fraction(mymat5R) +#print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixR) +#print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +#print_matrix_fraction(mymat3L) +#print_stencil_formula(mymat3L,xfrac) + +#print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3R,-xfrac) + +testfun(row_matrixL, mymat3L, r) +testfun(row_matrixR, mymat3R, r) diff --git a/example/figure/1d/weno/interplate/xi/08b/xi.py b/example/figure/1d/weno/interplate/xi/08b/xi.py new file mode 100644 index 00000000..17a24160 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08b/xi.py @@ -0,0 +1,506 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + #offset_map[k].append(coef) + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,r) + print_weno_equations(sub_stencils, target_dict) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-r) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +#print_matrix_fraction(mymat5L) +#print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixL) +#print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +#print_matrix_fraction(mymat5R) +#print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixR) +#print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3L,xfrac) + +#print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3R,-xfrac) + +compute_weno_linear_weights(row_matrixL, mymat3L, r) +compute_weno_linear_weights(row_matrixR, mymat3R, r) + +mymat3L = generate_left_stencils(3) +mymat3R = generate_right_stencils(3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) diff --git a/example/figure/1d/weno/interplate/xi/08c/xi.py b/example/figure/1d/weno/interplate/xi/08c/xi.py new file mode 100644 index 00000000..6a6baea5 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08c/xi.py @@ -0,0 +1,502 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'sub_stencils={sub_stencils}') + rows, cols = sub_stencils.shape + print(f'rows,cols={rows},{cols}') + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,r) + print_weno_equations(sub_stencils, target_dict) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-r) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5 = 5 +mymat5L = generate_reconstruction_stencils(k5, xfrac) +mymat5R = generate_reconstruction_stencils(k5, -xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +row_matrixR = mymat5R[r, :].reshape(1, -1) + +print_matrix_fraction(mymat5L) +print_matrix_fraction(mymat5R) + +print_matrix_fraction(row_matrixL) +print_matrix_fraction(row_matrixR) + +row_matL = compute_stencil_coefficients_for_point(r, k5, xfrac) +row_matR = compute_stencil_coefficients_for_point(r, k5, -xfrac) +print_matrix_fraction(row_matL) +print_matrix_fraction(row_matR) + + +k3=3 +mymat3L = generate_left_stencils(k3) +mymat3R = generate_right_stencils(k3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) +print_stencil_formula(mymat3L,xfrac) + +compute_weno_linear_weights(row_matrixL, mymat3L, r) +compute_weno_linear_weights(row_matrixR, mymat3R, r) + diff --git a/example/figure/1d/weno/interplate/xi/08d/xi.py b/example/figure/1d/weno/interplate/xi/08d/xi.py new file mode 100644 index 00000000..cd09a0fc --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08d/xi.py @@ -0,0 +1,507 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for i in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[i, j] + #offset_map[k].append(coef) + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row, base_offset=-2): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat, r): + sub_stencils = mymat + target_dict = build_substencil_offset_map_r(row_matrix,r) + print(f'target_dict={target_dict}') + print_weno_equations(sub_stencils, target_dict) + + # Build target map + target_map = build_target_offset_map(row_matrix[0], base_offset=-r) + + # Solve + weights = solve_weno_weights(mymat, target_map) + + +xfrac = Fraction(1,2) +k5=5 +mymat5L = calc_coef_formula(k5, xfrac) +#print_matrix_fraction(mymat5L) +#print_stencil_formula(mymat5L,xfrac) + +r=2 +row_matrixL = mymat5L[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixL) +#print_stencil_formula(row_matrixL,xfrac,0,r) + +mymat5R = calc_coef_formula(k5, -xfrac) +#print_matrix_fraction(mymat5R) +#print_stencil_formula(mymat5R,-xfrac) + +row_matrixR = mymat5R[r, :].reshape(1, -1) +#print_matrix_fraction(row_matrixR) +#print_stencil_formula(row_matrixR,-xfrac,0,r) + +k3=3 +mymat3L = calc_coef_formula(k3, xfrac) +mymat3R = calc_coef_formula(k3, -xfrac) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3L,xfrac) + +#print_matrix_fraction(mymat3R) +#print_stencil_formula(mymat3R,-xfrac) + +compute_weno_linear_weights(row_matrixL, mymat3L, r) +compute_weno_linear_weights(row_matrixR, mymat3R, r) + +mymat3L = generate_left_stencils(3) +mymat3R = generate_right_stencils(3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) diff --git a/example/figure/1d/weno/interplate/xi/08e/xi.py b/example/figure/1d/weno/interplate/xi/08e/xi.py new file mode 100644 index 00000000..28b9cd1c --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08e/xi.py @@ -0,0 +1,516 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print(f'target_dict={target_dict}') + print_weno_equations(sub_stencils, target_dict) + + #print(f'target_dict={target_dict}') + + # Solve + weights = solve_weno_weights(mymat, target_dict) + + +xfrac = Fraction(1,2) +k5 = 5 + +row_matL = compute_optimal_reconstruction_stencil(k5, xfrac) +row_matR = compute_optimal_reconstruction_stencil(k5, -xfrac) + +print_matrix_fraction(row_matL) +print_matrix_fraction(row_matR) + +k3=3 +mymat3L = generate_left_stencils(k3) +mymat3R = generate_right_stencils(k3) + +print_matrix_fraction(mymat3L) +print() +print_matrix_fraction(mymat3R) + +compute_weno_linear_weights(row_matL, mymat3L) +compute_weno_linear_weights(row_matR, mymat3R) + diff --git a/example/figure/1d/weno/interplate/xi/08f/xi.py b/example/figure/1d/weno/interplate/xi/08f/xi.py new file mode 100644 index 00000000..c1f87b55 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08f/xi.py @@ -0,0 +1,510 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + print("WENO linear system (for weights d[0], d[1], d[2]):\n") + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + #print(f'target_dict={target_dict}') + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymat3L = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymat3L) + + mymat3R = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymat3R) + +compute_weno_linear_weights_new(3) + diff --git a/example/figure/1d/weno/interplate/xi/08g/xi.py b/example/figure/1d/weno/interplate/xi/08g/xi.py new file mode 100644 index 00000000..ad125717 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/08g/xi.py @@ -0,0 +1,520 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + #print(f'sub_stencils={sub_stencils}') + #print(f'all_offsets={all_offsets}') + #print(f'target_dict={target_dict}') + + rows, cols = sub_stencils.shape + #print(f'rows, cols={rows},{cols}') + + #print("WENO linear system (for weights d[0], d[1], d[2]):\n") + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + #print(f'target_dict={target_dict}') + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +compute_weno_linear_weights_new(3) + diff --git a/example/figure/1d/weno/interplate/xi/09/xi.py b/example/figure/1d/weno/interplate/xi/09/xi.py new file mode 100644 index 00000000..32ecfb33 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/09/xi.py @@ -0,0 +1,521 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_reconstruction_stencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_reconstruction_stencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_reconstruction_stencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + #print(f'sub_stencils={sub_stencils}') + #print(f'all_offsets={all_offsets}') + #print(f'target_dict={target_dict}') + + rows, cols = sub_stencils.shape + #print(f'rows, cols={rows},{cols}') + + #print("WENO linear system (for weights d[0], d[1], d[2]):\n") + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +compute_weno_linear_weights_new(1) +compute_weno_linear_weights_new(2) +compute_weno_linear_weights_new(3) + + diff --git a/example/figure/1d/weno/interplate/xi/09a/xi.py b/example/figure/1d/weno/interplate/xi/09a/xi.py new file mode 100644 index 00000000..8d5b2480 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/09a/xi.py @@ -0,0 +1,569 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_substencil_offset_map_r(sub_stencils,r): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + print(f'r={r}') + print(f'type(sub_stencils)={type(sub_stencils)}') + print(f'sub_stencils={sub_stencils}') + print(f'sub_stencils.shape={sub_stencils.shape}') + cols = sub_stencils.shape[0] + print(f'cols={cols}') + offset_map = defaultdict(list) + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[j] + offset_map[k] = coef + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + #print(f'sub_stencils={sub_stencils}') + #print(f'all_offsets={all_offsets}') + #print(f'target_dict={target_dict}') + + rows, cols = sub_stencils.shape + #print(f'rows, cols={rows},{cols}') + + #print("WENO linear system (for weights d[0], d[1], d[2]):\n") + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + + +# WENO3 (2 substencils → 3rd-order) +demo_weno_linear_weights(weno_r=2) + +# WENO5 (3 substencils → 5th-order) +demo_weno_linear_weights(weno_r=3) + +# WENO7 (4 substencils → 7th-order) +demo_weno_linear_weights(weno_r=4) + + diff --git a/example/figure/1d/weno/interplate/xi/09b/xi.py b/example/figure/1d/weno/interplate/xi/09b/xi.py new file mode 100644 index 00000000..030be928 --- /dev/null +++ b/example/figure/1d/weno/interplate/xi/09b/xi.py @@ -0,0 +1,542 @@ +import numpy as np +from fractions import Fraction +from collections import defaultdict + +def inverse_matrix(matrix): + # 将矩阵元素转换为浮点数以计算逆矩阵 + matrix_float = matrix.astype(float) + inverse = np.linalg.inv(matrix_float) + # 将逆矩阵元素转换为分数 + inverse_fraction = [[Fraction(inverse[i, j]).limit_denominator() for j in range(len(inverse))] for i in range(len(inverse))] + return inverse_fraction + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def integral_xi(x, j): + return (x ** (j + 1)) / (j + 1) + +def compute_coef(x,k): + y = [] + for j in range(k): + var = x ** j + y.append(var) + return y + +def id_tostring(rj): + mystr = str(rj) + if rj == 0: + mystr = ' ' + if rj > 0: + mystr = '+' + str(rj) + return mystr + +def coef_tostring(coef,i): + mystr = str(coef) + if coef >= 0: + if i == 0: + mystr = ' ' + mystr + else: + mystr = '+' + mystr + return mystr + +def coef_toabsstring(coef): + abs_str = str(abs(coef)) + s = '+' + if coef < 0: + s = '-' + return abs_str, s + +def cal_polynomial_matrix(r, kk): + arrays_list = [] + for m in range(kk): + j = -r + m + xia = Fraction(j) - Fraction(1,2) + xib = Fraction(j) + Fraction(1,2) + a_list = [] + for i in range(kk): + val = integral_xi(xib, i) - integral_xi(xia, i) + a_list.append(val) + arrays_list.append(a_list) + matrix = np.vstack(arrays_list) + return matrix + +def cal_polynomial_coefficients(r, kk, xfrac): + matrix = cal_polynomial_matrix(r, kk) + inverse = inverse_matrix(matrix) + xv = compute_coef(xfrac, kk) + yv = np.dot(xv, inverse) + return yv + +def calc_coef_formula(kk, xfrac): + rows_list = [] + for r in range(kk): + #-r+l + ym = cal_polynomial_coefficients(r, kk, xfrac) + rows_list.append(ym) + + return np.vstack(rows_list) + + +def build_moment_matrix(template_index: int, stencil_width: int) -> np.ndarray: + r""" + Build the moment matrix M for a given substencil, where + + M @ poly_coeffs = cell_averages + + The substencil corresponding to `template_index = r` uses the cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + with $k = \text{stencil\_width}$. Each cell $I_j$ is the interval $[j - 1/2, j + 1/2]$. + + The matrix entry M[m, i] is the integral of the monomial $\xi^i$ over the m-th cell + in the substencil (i.e., over $I_{j_m}$ where $j_m = i - r + m$): + + $$ + M[m, i] = \int_{j_m - 1/2}^{j_m + 1/2} \xi^i \, d\xi + $$ + + Parameters + ---------- + template_index : int + Index of the substencil (r = 0, 1, ..., k-1). Larger values shift the stencil left. + stencil_width : int + Number of cells in the substencil (k). + + Returns + ------- + M : np.ndarray of shape (k, k) + Moment matrix with exact fractional entries. + """ + rows = [] + for m in range(stencil_width): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -template_index + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point( + template_index: int, + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + r""" + Compute the reconstruction coefficients for a single substencil used to approximate + the point value at `x_point` (e.g., $x = i + 1/2$) from cell averages. + + The substencil corresponding to `template_index = r` (where $r = 0, 1, ..., k-1$) + uses the following $k = \text{stencil\_width}$ consecutive cells: + + $$ + I_{i - r},\ I_{i - r + 1},\ \dots,\ I_{i - r + k - 1} + $$ + + For example, when `stencil_width = 3` and reconstructing $v_{i+1/2}^-$: + - `template_index = 0` → cells [i, i+1, i+2] (rightmost) + - `template_index = 1` → cells [i-1, i, i+1] (middle) + - `template_index = 2` → cells [i-2, i-1, i ] (leftmost) + + The returned coefficients `c[0], c[1], ..., c[k-1]` satisfy: + $$ + p(x_{\text{point}}) = \sum_{j=0}^{k-1} c[j] \cdot \bar{v}_{i - r + j} + $$ + where $p(\cdot)$ is the unique polynomial of degree ≤ k−1 that matches the + cell averages over the substencil. + + Parameters + ---------- + template_index : int + Index of the substencil (0 ≤ template_index < stencil_width). + Larger values shift the stencil further to the left. + stencil_width : int + Number of cells in the substencil (order of accuracy = stencil_width). + x_point : Fraction + Relative coordinate where the point value is reconstructed, + e.g., Fraction(1, 2) for $i + 1/2$. + + Returns + ------- + coefficients : np.ndarray of shape (stencil_width,) + Reconstruction coefficients for the cell averages in the substencil, + ordered from leftmost to rightmost cell in the stencil. + """ + + M = build_moment_matrix(template_index, stencil_width) + M_inv = inverse_matrix(M) + monomials = np.array([x_point ** i for i in range(stencil_width)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def compute_optimal_reconstruction_stencil( + stencil_width: int, + x_point: Fraction +) -> np.ndarray: + """ + Compute the optimal (high-order) reconstruction stencil centered at cell i, + using `stencil_width` consecutive cells symmetric around i. + + The stencil covers cells: [i - (k-1)//2, ..., i, ..., i + (k-1)//2] + and reconstructs the point value at x = i + x_point. + + Example: + k=5, x_point=1/2 → cells [i-2, i-1, i, i+1, i+2] + Returns coefficients [c_{-2}, c_{-1}, c_0, c_1, c_2] + """ + if stencil_width % 2 == 0: + raise ValueError("Optimal stencil requires odd stencil_width for symmetry.") + + r = stencil_width // 2 + + coefficients = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + return coefficients + +def generate_weno_substencils(stencil_width: int, x_point: Fraction) -> np.ndarray: + """ + Generate all k = stencil_width substencils for reconstructing a point value at x_point. + + The returned matrix has shape (k, k), where: + - Row r corresponds to the substencil that uses cells: + [I_{i - r}, I_{i - r + 1}, ..., I_{i - r + k - 1}] + which is the r-th candidate stencil counting from the RIGHTMOST (r=0) + to the LEFTMOST (r=k-1) stencil. + + For example, when k=3 and reconstructing v_{i+1/2}^-: + r=0 → cells [i, i+1, i+2] (rightmost) + r=1 → cells [i-1, i, i+1] (middle) + r=2 → cells [i-2, i-1, i ] (leftmost) + """ + + stencils = [] + for r in range(stencil_width): + # r = 0 → rightmost stencil + # r = stencil_width-1 → leftmost stencil + + coef = compute_stencil_coefficients_for_point(r, stencil_width, x_point) + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(stencil_width, offset) + +def generate_right_stencils(stencil_width: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(stencil_width, -offset) + +def cal_iplus_index(jstart,cols): + var_rj_list=[] + for j in range(cols): + rj = jstart + j + var_rj = id_tostring(rj) + var_rj_list.append(var_rj) + return var_rj_list + +def format_signed_coef(coef,isFirstElement,width): + """ + vi+1/2(-),0= 1/3*v[i ]+5/6*v[i+1]- 1/6*v[i+2] + vi+1/2(-),1=-1/6*v[i-1]+5/6*v[i ]+ 1/3*v[i+1] + vi+1/2(-),2= 1/3*v[i-2]-7/6*v[i-1]+11/6*v[i ] + """ + abscoef,sign = coef_toabsstring( coef ) + if isFirstElement and sign == '+': + sign = ' ' + signed_coef_str = f"{sign}{abscoef:>{width-1}}" + return signed_coef_str + +def get_sign_and_abs_str(coef): + """将系数拆分为符号字符('+', '-', ' ')和绝对值字符串。""" + if coef >= 0: + return '+', str(coef) + else: + return '-', str(-coef) + +def compute_column_widths(coef_matrix): + """计算每列系数显示所需的最大宽度(含符号位)""" + rows, cols = coef_matrix.shape + widths = np.empty(cols, dtype=int) + for j in range(cols): + max_width = 0 + for i in range(rows): + _, abs_str = get_sign_and_abs_str(coef_matrix[i, j]) + width = len(abs_str) + 1 # +1 for sign or space + max_width = max(max_width, width) + widths[j] = max_width + return widths + +def build_variable_index_string(offset): + """将偏移量转为 '[i+2]', '[i-1]', '[i ]' 等字符串""" + if offset == 0: + return "[i ]" + elif offset > 0: + return f"[i+{offset}]" + else: + return f"[i{offset}]" # offset already includes minus, e.g., -2 → [i-2] + +def build_variable_indices(start_offset, num_cols): + """生成每列对应的变量索引字符串列表""" + return [build_variable_index_string(start_offset + j) for j in range(num_cols)] + +def build_lhs_label(x_frac: Fraction, shift: int = 0) -> str: + # 1. 计算总偏移 = shift + x_frac + total_offset = shift + x_frac + + # 2. 格式化总偏移字符串(带符号) + if total_offset >= 0: + offset_str = f"+{total_offset}" # e.g., +1/2, +3/2 + else: + offset_str = f"{total_offset}" # e.g., -1/2(Fraction 会自动带负号) + + # 3. 确定方向标志:由原始 x_frac 的符号决定(非 total_offset!) + direction = '-' if x_frac >= 0 else '+' + + # 4. 拼接 + return f"vi{offset_str}({direction})" + +def format_stencil_row(row, jstart, cols, widths): + terms = [] + ioffset_strs = build_variable_indices(jstart, cols) + + for j in range(cols): + term_str = format_signed_coef(row[j],j==0,widths[j]) + terms.append(f"{term_str}*v{ioffset_strs[j]}") + + rhs_label = ''.join(terms) + return rhs_label + +def print_stencil_formula(coef_matrix,xfrac,ishift=0,base_row=0): + rows, cols = coef_matrix.shape + widths = compute_column_widths(coef_matrix) + + lhs_label = build_lhs_label(xfrac, ishift) + + for i in range(rows): + r = base_row if rows == 1 else i + row = coef_matrix[i] + jstart = ishift - r + rhs_label = format_stencil_row(row, jstart, cols, widths) + print(f'{lhs_label},{r}={rhs_label}') + + print() + +def build_substencil_offset_map(sub_stencils): + """为每个空间偏移 k,记录 (模板索引 r, 系数)""" + rows, cols = sub_stencils.shape + offset_map = defaultdict(list) + for r in range(rows): + for j in range(cols): + k = j - r # spatial offset: v[i + k] + coef = sub_stencils[r, j] + offset_map[k].append((r, coef)) + return offset_map + +def build_target_offset_map(target_row): + """ + target_row: 1D array like [1/30, -13/60, 47/60, 9/20, -1/20] + assumes it corresponds to offsets [-2, -1, 0, 1, 2] + """ + n = len(target_row) + base_offset = - (n//2) + offsets = list(range(base_offset, base_offset + n)) # [-2,-1,0,1,2] + return {k: target_row[i] for i, k in enumerate(offsets)} + +def build_linear_system(sub_stencils, target_offset_map): + """ + Build A x = b for WENO weights. + + Returns: + A: np.ndarray of shape (num_equations, num_templates) + b: np.ndarray of shape (num_equations,) + offsets: list of spatial offsets (for labeling) + """ + sub_offset_map = build_substencil_offset_map(sub_stencils) + num_templates = sub_stencils.shape[0] + + # Get all spatial offsets that appear in target + offsets = sorted(target_offset_map.keys()) + + A = [] + b = [] + + for k in offsets: + row = [Fraction(0) for _ in range(num_templates)] + for r, coef in sub_offset_map.get(k, []): + row[r] = coef + A.append(row) + b.append(target_offset_map[k]) + + # Convert to float for numpy (or keep as Fraction for exact solve) + A_float = np.array([[float(x) for x in row] for row in A]) + b_float = np.array([float(x) for x in b]) + + return A_float, b_float, offsets + +def solve_weno_weights(sub_stencils, target_offset_map): + A, b, offsets = build_linear_system(sub_stencils, target_offset_map) + # Solve Ax = b in least-squares sense + x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None) + + print("Solved WENO weights:") + for i, wi in enumerate(x): + print(f"d[{i}] = {wi:.6f} ≈ {Fraction(wi).limit_denominator(100)}") + + # Verify residual + if len(residuals) > 0: + print(f"Residual norm: {np.sqrt(residuals[0]):.2e}") + else: + # Exact solution (rank-deficient or square) + residual = np.linalg.norm(A @ x - b) + print(f"Residual norm: {residual:.2e}") + + return x + +def print_weno_equations(sub_stencils, target_dict): + offset_map = build_substencil_offset_map(sub_stencils) + all_offsets = sorted(target_dict.keys()) + + rows, cols = sub_stencils.shape + + weights = ", ".join(f"d[{i}]" for i in range(rows)) + print(f"WENO linear system (for weights {weights}):\n") + + for k in all_offsets: + terms = [] + for r, coef in offset_map.get(k, []): + terms.append(f"d[{r}] * ({coef})") + + lhs = " + ".join(terms) if terms else "0" + rhs = target_dict[k] + print(f"v[i{k:+}] : {lhs} = {rhs}") + print() + +def compute_weno_linear_weights(row_matrix, mymat): + sub_stencils = mymat + + # Build target map + target_dict = build_target_offset_map(row_matrix) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(mymat, target_dict) + +def compute_weno_linear_weights_new(order): + xfrac = Fraction(1,2) + + k = order + kh = 2*k - 1 + + mymatL = generate_left_stencils(k) + row_matL = compute_optimal_reconstruction_stencil(kh, xfrac) + compute_weno_linear_weights(row_matL, mymatL) + + mymatR = generate_right_stencils(k) + row_matR = compute_optimal_reconstruction_stencil(kh, -xfrac) + compute_weno_linear_weights(row_matR, mymatR) + +def solve_weno_linear_weights(optimal_stencil: np.ndarray, sub_stencils: np.ndarray) -> np.ndarray: + """ + Solve for linear weights d such that: + optimal_stencil ≈ sum_j d[j] * sub_stencils[j] + + Prints the linear system and solved weights. + """ + + # Build target map + target_dict = build_target_offset_map(optimal_stencil) + print_weno_equations(sub_stencils, target_dict) + + # Solve + weights = solve_weno_weights(sub_stencils, target_dict) + return weights + +def demo_weno_linear_weights(weno_r: int): + """ + Demonstrate linear weight computation for WENO-r scheme. + + Parameters: + weno_r (int): Number of substencils (e.g., 3 for WENO5, 2 for WENO3) + """ + x_half = Fraction(1, 2) + global_stencil_width = 2 * weno_r - 1 # e.g., 5 for WENO3 + + # Left-biased (v_{i+1/2}^-) + substencils_L = generate_weno_substencils(stencil_width=weno_r, x_point=x_half) + optimal_L = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=x_half + ) + weights_L = solve_weno_linear_weights(optimal_L, substencils_L) + + # Right-biased (v_{i-1/2}^+) + substencils_R = generate_weno_substencils(stencil_width=weno_r, x_point=-x_half) + optimal_R = compute_optimal_reconstruction_stencil( + stencil_width=global_stencil_width, x_point=-x_half + ) + weights_R = solve_weno_linear_weights(optimal_R, substencils_R) + + return weights_L, weights_R + +if __name__ == "__main__": + maxk = 3 + for k in range(1,maxk+1): + print(f"\n=== WENO{2*k-1} ===") + demo_weno_linear_weights(weno_r=k) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01/matrix.py b/example/figure/1d/weno/matrix/01/matrix.py new file mode 100644 index 00000000..f50de4db --- /dev/null +++ b/example/figure/1d/weno/matrix/01/matrix.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +使用 SymPy 生成并操作下面的矩矩阵 + +M_{j,i} = ∫_{α_j}^{β_j} ξ^i dξ, +α_j = -r + j - 1/2, +β_j = -r + j + 1/2, +j,i = 0,…,k-1 +""" + +import sympy as sp + +def matrix_element(i, j, r): + """返回 M_{j,i} = (β_j^{i+1} - α_j^{i+1})/(i+1)""" + half = sp.Rational(1, 2) + alpha = -r + j - half + beta = -r + j + half + return sp.simplify((beta**(i+1) - alpha**(i+1)) / (i+1)) + +def moment_matrix(k_val, r_sym): + """生成 k×k 的矩矩阵 M""" + M = sp.zeros(k_val, k_val) + for j in range(k_val): + for i in range(k_val): + M[j, i] = matrix_element(i, j, r_sym) + return M + +# ------------------- 示例 ------------------- +if __name__ == "__main__": + # 符号参数 + r = sp.symbols('r', real=True) + + # 1) 生成 3×3 矩阵并打印 + M3 = moment_matrix(3, r) + print("3×3 矩矩阵 M (符号 r):") + sp.pprint(M3) + + # 2) 取 r = 2 得到数值矩阵 + M3_num = M3.subs(r, 2) + print("\n当 r = 2 时的数值矩阵:") + sp.pprint(M3_num) + + # 3) 计算行列式 + det_M3 = sp.simplify(M3.det()) + print("\n3×3 矩阵的行列式 (化简后):") + sp.pprint(det_M3) + + # 4) 生成 4×4 矩阵并查看行列式 + M4 = moment_matrix(4, r) + print("\n4×4 矩矩阵 M (符号 r):") + sp.pprint(M4) + det_M4 = sp.simplify(M4.det()) + print("\n4×4 矩阵的行列式 (化简后):") + sp.pprint(det_M4) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01a/matrix.py b/example/figure/1d/weno/matrix/01a/matrix.py new file mode 100644 index 00000000..671e8d50 --- /dev/null +++ b/example/figure/1d/weno/matrix/01a/matrix.py @@ -0,0 +1,28 @@ +import sympy as sp + +def moment_matrix_k3(r_val): + """生成 k=3 时的矩阵,并返回 LaTeX 字符串""" + k = 3 + r = sp.symbols('r') + half = sp.Rational(1, 2) + + # 创建矩阵 + M = sp.zeros(k, k) + for j in range(k): + for i in range(k): + alpha = -r + j - half + beta = -r + j + half + M[j, i] = (beta**(i+1) - alpha**(i+1)) / (i+1) + + # 代入具体的 r 值 + M_substituted = M.subs(r, r_val) + + # 转换为 LaTeX,使用简化模式 + latex_code = sp.latex(M_substituted, mat_str='matrix', mat_delim='[') + return latex_code + +# 生成 r = 0, 1, 2 的 LaTeX 代码 +for r in [0, 1, 2]: + latex = moment_matrix_k3(r) + print(f"\n%% 当 r = {r} 时的矩阵 (k=3):") + print(latex) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01b/matrix.py b/example/figure/1d/weno/matrix/01b/matrix.py new file mode 100644 index 00000000..fed93488 --- /dev/null +++ b/example/figure/1d/weno/matrix/01b/matrix.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +计算 c = φᵀ(½)·M⁻¹,其中 + M_{j,i}=∫_{α_j}^{β_j} ξ^i dξ, + α_j = -r + j - ½, β_j = -r + j + ½, + φᵀ(½) = [1, ½, …, (½)^{k-1}]. + +对 r = 0,1,2 计算对应的行向量 c,组合成矩阵 C 并输出 LaTeX。 +""" + +import sympy as sp + +def moment_matrix(k_val, r_sym): + """生成 k×k 的矩矩阵 M(符号形式)""" + half = sp.Rational(1, 2) + M = sp.zeros(k_val, k_val) + for j in range(k_val): + alpha = -r_sym + j - half + beta = -r_sym + j + half + for i in range(k_val): + M[j, i] = (beta**(i + 1) - alpha**(i + 1)) / (i + 1) + return M + +def compute_c_matrix(k_val): + """ + 计算 C 矩阵(3×k),其每一行对应 r = 0,1,2 时的 c = φᵀ(½)·M⁻¹ + """ + # 把 r 视为符号,后面再代入具体数值 + r = sp.symbols('r', real=True) + + # 基础的符号矩阵 M + M_sym = moment_matrix(k_val, r) + + # φᵀ(½) 行向量 + phi_T = sp.Matrix([ (sp.Rational(1, 2))**i for i in range(k_val) ]).T + + rows = [] + for r_val in [0, 1, 2]: + # 代入当前 r,得到数值矩阵并求逆 + M_num = M_sym.subs(r, r_val) + M_inv = M_num.inv() + # c = φᵀ(½)·M⁻¹ + c_row = phi_T * M_inv + rows.append(c_row) + + # 纵向堆叠成 3×k 矩阵 + C = sp.Matrix(rows) + return C + +def latex_matrix(mat, env='bmatrix'): + """将 sympy 矩阵转为 LaTeX 字符串""" + return sp.latex(mat, mat_str=env, mat_delim='[') + +# ------------------- 示例 ------------------- +if __name__ == "__main__": + # k = 2 + C2 = compute_c_matrix(2) + print("%% k = 2 时的 C 矩阵(LaTeX)") + print(latex_matrix(C2)) + + # k = 3 + C3 = compute_c_matrix(3) + print("\n%% k = 3 时的 C 矩阵(LaTeX)") + print(latex_matrix(C3)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/01c/matrix.py b/example/figure/1d/weno/matrix/01c/matrix.py new file mode 100644 index 00000000..dd0edeb1 --- /dev/null +++ b/example/figure/1d/weno/matrix/01c/matrix.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" +计算重构系数矩阵:coeff_matrix = φᵀ(ξ₀)·M⁻¹ +其中 M 为矩矩阵,ξ₀ 为指定位置(如 ±1/2) +""" + +import sympy as sp + +def moment_matrix(k_val, r_sym): + """生成 k×k 的矩矩阵 M(符号形式)""" + half = sp.Rational(1, 2) + M = sp.zeros(k_val, k_val) + for j in range(k_val): + alpha = -r_sym + j - half + beta = -r_sym + j + half + for i in range(k_val): + M[j, i] = (beta**(i + 1) - alpha**(i + 1)) / (i + 1) + return M + +def compute_coeff_matrix(k_val, xi0=sp.Rational(1, 2)): + """ + 计算系数矩阵(3×k),每行对应 r = 0,1,2 时的 φᵀ(ξ₀)·M⁻¹ + + 参数 + ---------- + k_val : int + 矩阵阶数 + xi0 : sympy.Rational + 求值位置(默认为 1/2) + """ + r = sp.symbols('r', real=True) + M_sym = moment_matrix(k_val, r) + phi_T = sp.Matrix([xi0**i for i in range(k_val)]).T # φᵀ(ξ₀) + #print(f"phi_T={phi_T}") + #print(sp.latex(phi_T, mat_str='matrix', mat_delim='[')) + + rows = [] + for r_val in [0, 1, 2]: + M_inv = M_sym.subs(r, r_val).inv() + c_row = phi_T * M_inv + rows.append(c_row) + + coeff_matrix = sp.Matrix(rows) # 推荐变量名 + return coeff_matrix + +def latex_matrix(mat, env='matrix'): + """将 sympy 矩阵转为 LaTeX 字符串""" + return sp.latex(mat, mat_str=env, mat_delim='[') + +# ------------------- 示例 ------------------- +if __name__ == "__main__": + # 默认 ξ₀ = 1/2 + print("%% 当 ξ₀ = 1/2 时:") + + C2 = compute_coeff_matrix(2) + print("\n%% k = 2 的系数矩阵:") + print(latex_matrix(C2)) + + C3 = compute_coeff_matrix(3) + print("\n%% k = 3 的系数矩阵:") + print(latex_matrix(C3)) + + # 示例:ξ₀ = -1/2 + print("\n\n%% 当 ξ₀ = -1/2 时:") + + C2_neg = compute_coeff_matrix(2, xi0=sp.Rational(-1, 2)) + print("\n%% k = 2 的系数矩阵:") + print(latex_matrix(C2_neg)) + + C3_neg = compute_coeff_matrix(3, xi0=sp.Rational(-1, 2)) + print("\n%% k = 3 的系数矩阵:") + print(latex_matrix(C3_neg)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02/matrix.py b/example/figure/1d/weno/matrix/02/matrix.py new file mode 100644 index 00000000..c736a777 --- /dev/null +++ b/example/figure/1d/weno/matrix/02/matrix.py @@ -0,0 +1,87 @@ +import sympy as sp + +def construct_system(k, r_symbolic=True, specific_r=None): + """ + 构造积分约束系统:∫_{α}^{β} p(r, φ) dφ = v̄_{i−r+j} + + 参数: + k (int): 多项式阶数 (p 是 k−1 次多项式) + r_symbolic (bool): 是否将 r 保留为符号(True)还是使用 specific_r(False) + specific_r (int): 若 r_symbolic=False,指定具体的 r 值 (0 ≤ r ≤ k−1) + + 返回: + A (Matrix): k×k 系数矩阵 A_{j,m} = ∫ φ^m dφ over [α(r,j), β(r,j)] + rhs_symbols (list): 右端符号 [v̄_{i−r}, v̄_{i−r+1}, ..., v̄_{i−r+k−1}] + phi (Symbol): 积分变量(内部用 phi,LaTeX 可替换) + r (Symbol or int): 使用的 r + """ + # 积分变量(内部用 phi,LaTeX 显示为 \phi) + phi = sp.Symbol('phi') + + if r_symbolic: + r = sp.Symbol('r', integer=True) + else: + if specific_r is None: + raise ValueError("specific_r must be provided if r_symbolic=False") + if not (0 <= specific_r <= k - 1): + raise ValueError(f"specific_r={specific_r} must be in [0, {k-1}]") + r = specific_r + + # 多项式系数 a_0(r), ..., a_{k-1}(r) + a = sp.symbols(f'a0:{k}', cls=sp.Function) + # 注意:这里 a0(r) 是函数形式,保留对 r 的依赖 + + # 构造多项式 p(r, phi) = Σ_{m=0}^{k-1} a_m(r) * phi^m + p = sum(a[m](r) * phi**m for m in range(k)) + + # 右端符号:v̄_{i - r + j} + v = sp.symbols(f'vbar0:{k}') # vbar0, vbar1, ..., vbar_{k-1} + rhs_symbols = [v[j] for j in range(k)] # 对应 j=0,...,k−1 + + # 构建系数矩阵 A:A[j, m] = ∫_{α}^{β} φ^m dφ + A = sp.zeros(k, k) + + for j in range(k): + # α(r,j) = -r + j - 1/2, β(r,j) = -r + j + 1/2 + alpha = -r + j - sp.Rational(1, 2) + beta = -r + j + sp.Rational(1, 2) + + for m in range(k): + # 积分 ∫ φ^m dφ = (β^{m+1} - α^{m+1}) / (m+1) + integral = (beta**(m + 1) - alpha**(m + 1)) / sp.Rational(m + 1) + A[j, m] = sp.simplify(integral) + + return A, rhs_symbols, phi, r, a + +# ----------------------------- +# 示例 1:通用表达式(k=3,r 为符号) +# ----------------------------- +print("=== 通用表达式 (k=3, r symbolic) ===") +A_gen, rhs_gen, phi, r_sym, a_sym = construct_system(k=3, r_symbolic=True) + +# 打印矩阵 A(LaTeX,用 \phi 替代 \xi) +# SymPy 默认用 phi,若原用 xi 则需替换,但这里直接定义为 phi +print("系数矩阵 A (k=3):") +sp.pprint(A_gen) +print("\nLaTeX (A):") +latex_A = sp.latex(A_gen) +latex_A_phi = latex_A.replace(r'\xi', r'\phi') # 虽然这里没用 xi,但保险起见 +print(latex_A_phi) + +# ----------------------------- +# 示例 2:具体 r=0, k=3 +# ----------------------------- +print("\n=== 具体情况 (k=3, r=0) ===") +A_r0, rhs_r0, phi, r_val, a_val = construct_system(k=3, r_symbolic=False, specific_r=0) +sp.pprint(A_r0) +print("\nLaTeX (A for r=0):") +print(sp.latex(A_r0)) + +# ----------------------------- +# 示例 3:保留接口以供将来代入 r 值 +# ----------------------------- +print("\n=== 从通用表达式代入 r=1 ===") +A_r1 = A_gen.subs(r_sym, 1) +sp.pprint(A_r1) +print("\nLaTeX (A for r=1 via subs):") +print(sp.latex(A_r1)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02a/matrix.py b/example/figure/1d/weno/matrix/02a/matrix.py new file mode 100644 index 00000000..ae9f2b34 --- /dev/null +++ b/example/figure/1d/weno/matrix/02a/matrix.py @@ -0,0 +1,89 @@ +import sympy as sp +from sympy import symbols, Function, latex +from sympy.printing.latex import LatexPrinter + +# ---- Step 1: 定义基础符号(可全局配置) ---- +k = sp.Symbol('k', integer=True, positive=True) +r, j, xi = sp.symbols('r j \\xi') # 注意:xi 命名为 \xi 以便 LaTeX + +# 多项式系数:a_m(r) 作为函数 +a = Function('a') +# 为简化,我们用 a(0)(r), a(1)(r), ... 表示 a_0(r), a_1(r), ... +# 但 LaTeX 中希望显示为 a_0(r),需自定义打印 + +# 右端符号:定义一个带下标的符号生成器 +def vbar(index_expr): + # 使用 Dummy symbol with name containing LaTeX + name = f"\\overline{{v}}_{{{sp.latex(index_expr)}}}" + return sp.Symbol(name) + +# ---- Step 2: 构造多项式 p(r, xi) ---- +def build_polynomial(order, use_r_in_coeff=True): + terms = [] + for m in range(order): + if use_r_in_coeff: + coeff = a(m)(r) # a_m(r) + else: + coeff = sp.Symbol(f'a_{m}') # a_m(无 r 依赖) + terms.append(coeff * xi**m) + return sum(terms) + +# ---- Step 3: 构造积分限 α, β ---- +alpha = -r + j - sp.Rational(1, 2) +beta = -r + j + sp.Rational(1, 2) + +# ---- Step 4: 构造各个表达式(保持符号) ---- +# 多项式(带 r 依赖) +p_r_xi = build_polynomial(k, use_r_in_coeff=True) + +# 积分等式(抽象) +integral_eq_abstract = sp.Eq( + sp.Integral(p_r_xi, (xi, alpha, beta)), + vbar(sp.Symbol('i') - r + j) +) + +# 积分等式(显式展开,带 a_m(r)) +p_expanded = p_r_xi +integral_eq_expanded = sp.Eq( + sp.Integral(p_expanded, (xi, alpha, beta)), + vbar(sp.Symbol('i') - r + j) +) + +# 积分等式(a_m 不带 r) +p_no_r = build_polynomial(k, use_r_in_coeff=False) +integral_eq_no_r = sp.Eq( + sp.Integral(p_no_r, (xi, alpha, beta)), + vbar(sp.Symbol('i') - r + j) +) + +# ---- Step 5: 生成 LaTeX 输出(array 环境) ---- +def generate_latex_array(*exprs, alpha_expr, beta_expr): + lines = [] + for expr in exprs: + lines.append(latex(expr)) + + # 添加 α, β 定义(j 范围) + j_range = r'j=0,1,\ldots,k-1' + alpha_line = f"{latex(alpha_expr)}={latex(alpha)},\\quad {j_range}" + beta_line = f"{latex(beta_expr)}={latex(beta)},\\quad {j_range}" + + full_latex = r"\begin{array}{l}" + "\n" + full_latex += " \\\\\n".join(lines + [alpha_line, beta_line]) + full_latex += "\n\\end{array}" + return full_latex + +# ---- 执行生成 ---- +# 定义 α(r,j), β(r,j) 作为符号(用于等式左边) +alpha_sym = sp.Symbol(r'\alpha(r,j)') +beta_sym = sp.Symbol(r'\beta(r,j)') + +latex_output = generate_latex_array( + sp.Eq(sp.Symbol('p(r,\\xi)'), p_r_xi), + sp.Eq(sp.Integral(sp.Symbol('p(r,\\xi)'), (xi, alpha_sym, beta_sym)), vbar(sp.Symbol('i') - r + j)), + integral_eq_expanded, + integral_eq_no_r, + alpha_expr=alpha_sym, + beta_expr=beta_sym +) + +print(latex_output) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02b/matrix.py b/example/figure/1d/weno/matrix/02b/matrix.py new file mode 100644 index 00000000..4e90596e --- /dev/null +++ b/example/figure/1d/weno/matrix/02b/matrix.py @@ -0,0 +1,89 @@ +import sympy as sp +from sympy import symbols, Function, latex, Rational + +# ---------- 配置 ---------- +# 当前仅用于 LaTeX 显示,不用于求和 +k = sp.Symbol('k', integer=True, positive=True) +r, j, xi = symbols('r j \\xi') +i_sym = symbols('i') # 用于 vbar 下标 + +# 多项式系数函数(用于具体 k 时) +a = Function('a') + +# ---------- 工具函数 ---------- +def vbar_expr(): + """返回 \overline{v}_{i - r + j} 的 LaTeX 表示""" + index = i_sym - r + j + return r"\overline{v}_{" + latex(index) + r"}" + +def polynomial_latex_with_cdots(use_r=True): + """生成 a_0 + a_1 \\xi + \\cdots + a_{k-1} \\xi^{k-1} 的 LaTeX""" + if use_r: + term0 = r"a_{0}(r)" + term1 = r"a_{1}(r) \xi" + term_last = r"a_{k-1}(r) \xi^{k-1}" + else: + term0 = r"a_{0}" + term1 = r"a_{1} \xi" + term_last = r"a_{k-1} \xi^{k-1}" + return f"{term0} + {term1} + \\cdots + {term_last}" + +def integral_latex_with_cdots(use_r=True): + """生成带省略号的积分表达式 LaTeX""" + poly_str = polynomial_latex_with_cdots(use_r) + alpha_str = latex(-r + j - Rational(1, 2)) + beta_str = latex(-r + j + Rational(1, 2)) + vbar_str = vbar_expr() + return f"\\displaystyle \\int_{{{alpha_str}}}^{{{beta_str}}} ({poly_str}) \\, d\\xi = {vbar_str}" + +# ---------- 构造具体 k 的表达式(用于将来矩阵生成) ---------- +def build_full_polynomial(k_val, use_r=True): + """当 k 是具体整数时,构建完整多项式(用于计算)""" + terms = [] + for m in range(k_val): + if use_r: + coeff = a(m)(r) + else: + coeff = sp.Symbol(f'a_{m}') + terms.append(coeff * xi**m) + return sum(terms) + +# ---------- 生成最终 LaTeX array ---------- +def generate_weno_latex_array(): + lines = [] + + # Line 1: p(r,xi) = a0(r) + a1(r) xi + ... + a_{k-1}(r) xi^{k-1} + p_def = r"p(r,\xi) = " + polynomial_latex_with_cdots(use_r=True) + lines.append(p_def) + + # Line 2: ∫ p(r,xi) dxi = vbar + alpha_sym = r"\alpha(r,j)" + beta_sym = r"\beta(r,j)" + vbar_str = vbar_expr() + lines.append(rf"\displaystyle\int_{{{alpha_sym}}}^{{{beta_sym}}} p(r,\xi) \, d\xi = {vbar_str}") + + # Line 3: 展开积分(带 a_m(r)) + lines.append(integral_latex_with_cdots(use_r=True)) + + # Line 4: 展开积分(不带 r) + lines.append(integral_latex_with_cdots(use_r=False)) + + # Line 5-6: α, β 定义 + alpha_def = latex(-r + j - Rational(1, 2)) + beta_def = latex(-r + j + Rational(1, 2)) + j_range = r"j=0,1,\ldots,k-1" + lines.append(rf"\alpha(r,j)={alpha_def},\quad {j_range}") + lines.append(rf"\beta(r,j)={beta_def},\quad {j_range}") + + # 组合成 array + latex_output = "\\begin{array}{l}\n" + " \\\\\n".join(lines) + "\n\\end{array}" + return latex_output + +# ---------- 主程序 ---------- +if __name__ == "__main__": + latex_code = generate_weno_latex_array() + print(latex_code) + + # 未来:当你需要 k=3 的具体系统时 + # p3 = build_full_polynomial(3, use_r=True) + # print("p3 =", p3) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02c/matrix.py b/example/figure/1d/weno/matrix/02c/matrix.py new file mode 100644 index 00000000..fbe89518 --- /dev/null +++ b/example/figure/1d/weno/matrix/02c/matrix.py @@ -0,0 +1,123 @@ +import sympy as sp +from sympy import symbols, Function, Rational, latex, Matrix, Integral + +# ---------- 全局符号 ---------- +k = sp.Symbol('k', integer=True, positive=True) +r, j, xi = symbols('r j \\xi') +i_sym = symbols('i') + +# ---------- 辅助函数 ---------- +def vbar(idx): + """生成 \overline{v}_{i - r + idx}""" + return sp.Symbol(r"\overline{v}_{" + latex(i_sym - r + idx) + r"}") + +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 1. 生成通用矩阵 M 的 LaTeX(带 \cdots) ---------- +def matrix_M_latex_generic(): + """生成 M = [ ∫ ξ^m dξ ]_{j,m=0}^{k-1} 的通用 LaTeX(含省略号)""" + rows = [] + # 第一行:j=0 + first_row = " & ".join([ + r"\int_{\alpha_{" + "0" + r"}}^{\beta_{" + "0" + r"}} d\xi", + r"\int_{\alpha_{" + "0" + r"}}^{\beta_{" + "0" + r"}} \xi^{1} d\xi", + r"\cdots", + r"\int_{\alpha_{" + "0" + r"}}^{\beta_{" + "0" + r"}} \xi^{k-1} d\xi" + ]) + rows.append(first_row) + + # 第二行(示例 j=1) + second_row = " & ".join([ + r"\int_{\alpha_{" + "1" + r"}}^{\beta_{" + "1" + r"}} d\xi", + r"\int_{\alpha_{" + "1" + r"}}^{\beta_{" + "1" + r"}} \xi^{1} d\xi", + r"\cdots", + r"\int_{\alpha_{" + "1" + r"}}^{\beta_{" + "1" + r"}} \xi^{k-1} d\xi" + ]) + rows.append(second_row) + + # 省略行 + rows.append(r"\vdots & \vdots & \ddots & \vdots") + + # 最后一行:j = k-1 + last_idx = "k-1" + last_row = " & ".join([ + r"\int_{\alpha_{" + last_idx + r"}}^{\beta_{" + last_idx + r"}} d\xi", + r"\int_{\alpha_{" + last_idx + r"}}^{\beta_{" + last_idx + r"}} \xi^{1} d\xi", + r"\cdots", + r"\int_{\alpha_{" + last_idx + r"}}^{\beta_{" + last_idx + r"}} \xi^{k-1} d\xi" + ]) + rows.append(last_row) + + matrix_body = " \\\\\n".join(rows) + return r"M = \begin{bmatrix}" + "\n" + matrix_body + "\n\\end{bmatrix}" + +# ---------- 2. 生成具体 M 矩阵(用于计算和 LaTeX) ---------- +def build_matrix_M(k_val, r_val=None): + """ + 构建具体 k 下的 M 矩阵。 + 若 r_val 为 None,则保留 r 为符号;否则代入具体 r。 + """ + M = sp.zeros(k_val, k_val) + for j_idx in range(k_val): + a_j = alpha(j_idx) + b_j = beta(j_idx) + if r_val is not None: + a_j = a_j.subs(r, r_val) + b_j = b_j.subs(r, r_val) + for m in range(k_val): + # ∫ ξ^m dξ from α to β + integral = (b_j**(m+1) - a_j**(m+1)) / Rational(m+1) + M[j_idx, m] = sp.simplify(integral) + return M + +# ---------- 3. 生成向量 LaTeX ---------- +def vector_a_latex(): + elements = [f"a_{{{i}}}" for i in range(k-1)] # 仅用于显示 + # 但 k 是符号,无法 range(k),所以用省略号 + return r"\mathbf{a} = \begin{bmatrix} a_0 \\ a_1 \\ \\vdots \\ a_{k-1} \\end{bmatrix}" + +def vector_v_latex(): + # 使用 vbar(j) for j=0,...,k-1 + first = r"\overline{v}_{" + latex(i_sym - r + 0) + r"}" + second = r"\overline{v}_{" + latex(i_sym - r + 1) + r"}" + last = r"\overline{v}_{" + latex(i_sym - r + (k - 1)) + r"}" + return f"\\mathbf{{v}} = \\begin{bmatrix} {first} \\\\ {second} \\\\ \\vdots \\\\ {last} \\end{{bmatrix}}" + +# ---------- 4. 生成 a = M^{-1} v 的公式 ---------- +def equation_system_latex(): + a_vec = r"\begin{bmatrix} a_0 \\ a_1 \\ \\vdots \\ a_{k-1} \\end{bmatrix}" + v_vec = r"\begin{bmatrix} \overline{v}_{" + latex(i_sym - r + 0) + r"} \\ \overline{v}_{" + latex(i_sym - r + 1) + r"} \\ \\vdots \\ \overline{v}_{" + latex(i_sym - r + (k - 1)) + r"} \\end{bmatrix}" + return f"{a_vec} = M^{{-1}} {v_vec}" + +# ---------- 主输出:完整 LaTeX 块 ---------- +def generate_extended_latex(): + lines = [] + + # 向量定义 + lines.append(r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}") + lines.append(r"\mathbf{v}=[\overline{v}_{" + latex(i_sym - r + 0) + r"},\overline{v}_{" + latex(i_sym - r + 1) + r"},\dots,\overline{v}_{" + latex(i_sym - r + (k - 1)) + r"}]^T") + + # 方程 + lines.append(r"\mathbf{a}=M^{-1}\mathbf{v}") + + # 展开方程 + #a_vec = r"\begin{bmatrix} a_0 \\ a_1 \\ \\vdots \\ a_{k-1} \\end{bmatrix}" + a_vec = r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix}" + #v_vec = r"\begin{bmatrix} \overline{v}_{" + latex(i_sym - r + 0) + r"} \\ \overline{v}_{" + latex(i_sym - r + 1) + r"} \\ \\vdots \\ \overline{v}_{" + latex(i_sym - r + (k - 1)) + r"} \\end{bmatrix}" + v_vec = r"\begin{bmatrix} \overline{v}_{" + latex(i_sym - r + 0) + r"} \\ \overline{v}_{" + latex(i_sym - r + 1) + r"} \\ \vdots \\ \overline{v}_{" + latex(i_sym - r + (k - 1)) + r"} \end{bmatrix}" + lines.append(f"{a_vec} = M^{{-1}} {v_vec}") + + # 矩阵 M(通用形式) + lines.append(matrix_M_latex_generic()) + + return "\\begin{array}{l}\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 示例:打印通用公式 ---------- +if __name__ == "__main__": + print(generate_extended_latex()) + + # 示例:打印 k=3, r=0 时的具体 M 矩阵 + print("\n\n=== Example: M for k=3, r=0 ===") + M3 = build_matrix_M(3, r_val=0) + print(latex(M3)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02d/matrix.py b/example/figure/1d/weno/matrix/02d/matrix.py new file mode 100644 index 00000000..e0a7c286 --- /dev/null +++ b/example/figure/1d/weno/matrix/02d/matrix.py @@ -0,0 +1,91 @@ +import sympy as sp +from sympy import symbols, Rational, Matrix, latex + +# ---------- 全局符号(保持一致性) ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols('\\xi') + +# ---------- 核心函数 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +def vbar(idx_expr): + """生成 \overline{v}_{expr} 的 SymPy 符号(用于计算)""" + # 使用带 LaTeX 名称的 Symbol,确保 latex() 输出正确 + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def build_v_vector(k_val): + """构建 v = [v_{i-r}, v_{i-r+1}, ..., v_{i-r+k-1}]^T""" + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_is_symbolic=True, specific_r=None): + """ + 构建 M_{j,m} = ∫_{α_j}^{β_j} ξ^m dξ, j,m = 0,...,k-1 + + 参数: + k_val (int): 多项式阶数 k + r_is_symbolic (bool): 是否保留 r 为符号 + specific_r (int): 若 r_is_symbolic=False,指定具体 r 值 + """ + if not r_is_symbolic and specific_r is None: + raise ValueError("specific_r must be provided if r_is_symbolic=False") + + M = sp.zeros(k_val, k_val) + r_use = r if r_is_symbolic else specific_r + + for j in range(k_val): + a_j = (-r_use + j - Rational(1, 2)) + b_j = (-r_use + j + Rational(1, 2)) + for m in range(k_val): + # ∫ ξ^m dξ = (b^{m+1} - a^{m+1}) / (m+1) + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = sp.simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + """ + 求解 a = M^{-1} v + + 返回: + a_vec: 系数向量 [a0, a1, ..., a_{k-1}]^T (SymPy Matrix) + M: 使用的 M 矩阵 + v: 使用的 v 向量 + """ + if r_val is None: + # 保留 r 为符号(第 1 层) + M = build_M_matrix(k_val, r_is_symbolic=True) + v = build_v_vector(k_val) + else: + # 固定 r(第 2 层) + M = build_M_matrix(k_val, r_is_symbolic=False, specific_r=r_val) + # 构建 v 向量(此时 r 已固定) + v = Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + + # 求逆并计算 a = M^{-1} v + M_inv = M.inv() # SymPy 会自动简化 + a_vec = M_inv * v + return a_vec, M, v + +# ---------- 工具:美化 LaTeX 输出 ---------- +def print_solution_for_k_r(k_val, r_val=None): + """打印 a = M^{-1} v 的完整 LaTeX(通用或具体)""" + a_vec, M, v = solve_coefficients(k_val, r_val) + + print(f"\n=== WENO Coefficients: k={k_val}" + (f", r={r_val}" if r_val is not None else "(r symbolic)") + " ===") + print("M =") + print(latex(M)) + print("\nv =") + print(latex(v)) + print("\na = M^{-1} v =") + print(latex(a_vec)) + return a_vec, M, v + +# ---------- 主程序:按需生成 ---------- +if __name__ == "__main__": + # 示例 1: k=3, r 保持符号(通用表达式) + print_solution_for_k_r(k_val=3, r_val=None) + + # 示例 2: k=3, r=0,1,2(具体表达式) + for r_example in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_example) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02e/matrix.py b/example/figure/1d/weno/matrix/02e/matrix.py new file mode 100644 index 00000000..782c9438 --- /dev/null +++ b/example/figure/1d/weno/matrix/02e/matrix.py @@ -0,0 +1,219 @@ +import sympy as sp +import re +from sympy import symbols, Function, Rational, Matrix, latex, simplify + +# ---------- 全局符号(保持一致性) ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- 辅助函数:vbar 符号 ---------- +def vbar(idx_expr): + """生成带 LaTeX 名称的 \overline{v}_{...} 符号""" + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +# ---------- 积分限 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 向量与矩阵构造 ---------- +def build_v_vector(k_val, r_val=None): + """构建 v = [v_{i - r + j}]_{j=0}^{k-1}""" + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + """构建 M_{j,m} = ∫_{α_j}^{β_j} ξ^m dξ""" + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + """求解 a = M^{-1} v""" + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX 输出:原始公式 ---------- +def generate_original_latex(): + """生成你最初要求的 LaTeX 公式块""" + k_sym = sp.Symbol('k') + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + r"\displaystyle\int_{\alpha(r,j)}^{\beta(r,j)} p(r,\xi) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0} + a_{1} \xi + a_{2} \xi^2 + \cdots + a_{k-1} \xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\alpha(r,j)=-r+j-1/2,\quad j=0,1,\cdots,k-1", + r"\beta(r,j)=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX 输出:扩展系统 ---------- +def generate_extended_latex(): + """生成 a = M^{-1} v 系统的 LaTeX""" + k_sym = sp.Symbol('k') + v0 = vbar(i_sym - r + 0) + v1 = vbar(i_sym - r + 1) + vk1 = vbar(i_sym - r + (k_sym - 1)) + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + # 矩阵 M(通用形式) + M_latex = r"M = \begin{bmatrix}" + "\n" + M_latex += r"\int_{\alpha_{0}}^{\beta_{0}}d\xi & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\int_{\alpha_{1}}^{\beta_{1}}d\xi & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\vdots & \vdots & \ddots & \vdots \\" + "\n" + M_latex += r"\int_{\alpha_{k-1}}^{\beta_{k-1}}d\xi & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{k-1}d\xi" + "\n" + M_latex += r"\end{bmatrix}" + + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 格式化输出:排序 + 可选 factored ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_indexOlddddd(index_expr): + if isinstance(index_expr, str): + return (0, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (float(offset), "") + else: + return (0, str(index_expr)) + except: + return (0, str(index_expr)) + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + if not expr.has(_is_vbar_symbol): + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed.append((term, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Term has multiple vbar: {term}") + v = v_syms[0] + coeff = simplify(term / v) + idx = _extract_index_from_vbar(v) + parsed.append((coeff, v, idx)) + + # 排序:常数项在前(或最后),vbar 项按 index 排 + v_terms = [p for p in parsed if p[1] is not None] + const_terms = [p for p in parsed if p[1] is None] + v_terms.sort(key=lambda x: _sort_key_from_index(x[2])) + all_terms = const_terms + v_terms # 或 v_terms + const_terms + + parts = [] + for item in all_terms: + if item[1] is None: + parts.append(latex(item[0])) + else: + coeff, v, _ = item + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + num = "" if abs(coeff.p) == 1 else str(abs(coeff.p)) + den = str(coeff.q) + if num == "": + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = f"{sign}\\frac{{{num} {latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + parts.append(s) + + # 合并 + result = parts[0] + for part in parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主输出函数 ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- 主程序 ---------- +if __name__ == "__main__": + # 1. 输出原始 LaTeX 公式 + print("=== Original LaTeX Block ===") + print(generate_original_latex()) + + # 2. 输出扩展系统 LaTeX + print("\n\n=== Extended System LaTeX ===") + print(generate_extended_latex()) + + # 3. 计算并格式化输出系数(k=3, r=0,1,2) + for r_example in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_example, factored=True) + # 取消注释可查看另一种格式 + # print_solution_for_k_r(k_val=3, r_val=r_example, factored=False) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02f/matrix.py b/example/figure/1d/weno/matrix/02f/matrix.py new file mode 100644 index 00000000..4c2b7774 --- /dev/null +++ b/example/figure/1d/weno/matrix/02f/matrix.py @@ -0,0 +1,190 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- 辅助函数 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 向量与矩阵构造 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX: 原始公式 ---------- +def generate_original_latex(): + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + r"\displaystyle\int_{\alpha(r,j)}^{\beta(r,j)} p(r,\xi) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\displaystyle \int_{\alpha(r,j)}^{\beta(r,j)} (a_{0} + a_{1} \xi + a_{2} \xi^2 + \cdots + a_{k-1} \xi^{k-1}) d\xi=\overline{v}_{i-r+j}", + r"\alpha(r,j)=-r+j-1/2,\quad j=0,1,\cdots,k-1", + r"\beta(r,j)=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX: 扩展系统 ---------- +def generate_extended_latex(): + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + M_latex = r"M = \begin{bmatrix}" + "\n" + M_latex += r"\int_{\alpha_{0}}^{\beta_{0}}d\xi & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{0}}^{\beta_{0}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\int_{\alpha_{1}}^{\beta_{1}}d\xi & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{1}}^{\beta_{1}}{\xi}^{k-1}d\xi \\" + "\n" + M_latex += r"\vdots & \vdots & \ddots & \vdots \\" + "\n" + M_latex += r"\int_{\alpha_{k-1}}^{\beta_{k-1}}d\xi & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{1}d\xi & \cdots & \int_{\alpha_{k-1}}^{\beta_{k-1}}{\xi}^{k-1}d\xi" + "\n" + M_latex += r"\end{bmatrix}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 排序与格式化 ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + + # 正确检测是否包含 vbar 符号 + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # 排序:vbar 项按 index 排序,常数项放前(WENO 中通常无常数项) + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + # 生成 LaTeX + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主输出函数 ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- 主程序 ---------- +if __name__ == "__main__": + # 1. 原始公式 + print("=== Original LaTeX Block ===") + print(generate_original_latex()) + + # 2. 扩展系统 + print("\n\n=== Extended System LaTeX ===") + print(generate_extended_latex()) + + # 3. 具体系数 (k=3, r=0,1,2) + for r_val in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_val, factored=True) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02g/matrix.py b/example/figure/1d/weno/matrix/02g/matrix.py new file mode 100644 index 00000000..2f69fcfa --- /dev/null +++ b/example/figure/1d/weno/matrix/02g/matrix.py @@ -0,0 +1,264 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- 积分限命名策略 ---------- +# 提供基名(不含下标),如 \alpha, \beta +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', # 用于原始公式 + 'long_right': r'\beta(r,j)', + # M 矩阵中:\alpha_j 表示第 j 个单元左边界 + 'short_left_template': r'\alpha_{{{j}}}', # 用于 M 矩阵:\alpha_0, \alpha_1, ... + 'short_right_template': r'\beta_{{{j}}}' +} + +# ---------- 辅助函数 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): return -r + j - Rational(1, 2) +def upper_limit(j): return -r + j + Rational(1, 2) + +# ---------- 向量与矩阵构造 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = lower_limit(j) + b_j = upper_limit(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX: 原始公式 ---------- +def generate_original_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX: 扩展系统 ---------- +def generate_extended_latexOld(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + base_l = boundary_names['short_base_left'] # e.g., \alpha + base_r = boundary_names['short_base_right'] # e.g., \beta + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + # 构建 M 矩阵 LaTeX:使用 \alpha_0, \alpha_1, \alpha_{k-1} + def make_row(j_str): + return " & ".join([ + rf"\int_{{{base_l}_{{{j_str}}}}}^{{{base_r}_{{{j_str}}}}} d\xi", + rf"\int_{{{base_l}_{{{j_str}}}}}^{{{base_r}_{{{j_str}}}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{base_l}_{{{j_str}}}}}^{{{base_r}_{{{j_str}}}}} \xi^{{k-1}} d\xi" + ]) + + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +def generate_extended_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left_template = boundary_names['short_left_template'] # e.g., r'\alpha_{{{j}}}' + right_template = boundary_names['short_right_template'] # e.g., r'\beta_{{{j}}}' + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + def make_row(j_str): + left = left_template.format(j=j_str) + right = right_template.format(j=j_str) + return " & ".join([ + rf"\int_{{{left}}}^{{{right}}} d\xi", + rf"\int_{{{left}}}^{{{right}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{left}}}^{{{right}}} \xi^{{k-1}} d\xi" + ]) + + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- 排序与格式化(无变动) ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主输出函数 ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- 示例:不同命名策略 ---------- +X_BOUNDARY_NAMES = { + 'long_left': r'x_{j-\frac{1}{2}}', + 'long_right': r'x_{j+\frac{1}{2}}', + # 对 M 矩阵,提供 j 的占位符,如 {j} - 1/2 + 'short_left_template': r'x_{{{j}-\frac{{1}}{{2}}}}', + 'short_right_template': r'x_{{{j}+\frac{{1}}{{2}}}}' +} + +# ---------- 主程序 ---------- +if __name__ == "__main__": + # 1. 默认命名 (\alpha, \beta) + print("=== Original LaTeX (default: \\alpha, \\beta) ===") + print(generate_original_latex()) + + print("\n\n=== Extended System (default) ===") + print(generate_extended_latex()) + + # 2. 使用 x_{j±1/2} 命名 + print("\n\n=== Original LaTeX (x_{j±1/2}) ===") + print(generate_original_latex(boundary_names=X_BOUNDARY_NAMES)) + + print("\n\n=== Extended System (x_{j±1/2}) ===") + print(generate_extended_latex(boundary_names=X_BOUNDARY_NAMES)) + + # 3. 系数输出 (不受命名影响) + for r_val in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_val, factored=True) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/02h/matrix.py b/example/figure/1d/weno/matrix/02h/matrix.py new file mode 100644 index 00000000..eff7f06a --- /dev/null +++ b/example/figure/1d/weno/matrix/02h/matrix.py @@ -0,0 +1,289 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- Global symbols ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- Boundary naming strategy ---------- +# Template-based naming to avoid double subscripts and allow flexible notation +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', # Used in original formulas + 'long_right': r'\beta(r,j)', + 'short_left_template': r'\alpha_{{{j}}}', # Template for M matrix: \alpha_{j} + 'short_right_template': r'\beta_{{{j}}}' +} + +# Alternative naming for cell-centered finite volume methods +X_BOUNDARY_NAMES = { + 'long_left': r'x_{j-\frac{1}{2}}', + 'long_right': r'x_{j+\frac{1}{2}}', + 'short_left_template': r'x_{{{j}-\frac{{1}}{{2}}}}', + 'short_right_template': r'x_{{{j}+\frac{{1}}{{2}}}}' +} + +# ---------- Helper functions ---------- +def vbar(idx_expr): + """Create a Symbol with LaTeX name \overline{v}_{idx_expr} for proper rendering.""" + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): + """Lower integration limit: α(r,j) = -r + j - 1/2""" + return -r + j - Rational(1, 2) + +def upper_limit(j): + """Upper integration limit: β(r,j) = -r + j + 1/2""" + return -r + j + Rational(1, 2) + +# ---------- Vector and matrix construction ---------- +def build_v_vector(k_val, r_val=None): + """Construct the right-hand side vector v = [v̄_{i-r+j}]_{j=0}^{k-1}.""" + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + """ + Construct the moment matrix M where M[j,m] = ∫_{α_j}^{β_j} ξ^m dξ. + + Parameters: + k_val (int): Polynomial order (degree k-1) + r_val (int, optional): Specific r value. If None, keep r symbolic. + """ + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is not None: + # Substitute specific r value into symbolic limits + a_j = lower_limit(j).subs(r, r_val) + b_j = upper_limit(j).subs(r, r_val) + else: + a_j = lower_limit(j) + b_j = upper_limit(j) + + for m in range(k_val): + # Analytical integration: ∫ ξ^m dξ = (β^{m+1} - α^{m+1}) / (m+1) + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + """Solve a = M^{-1} v for polynomial coefficients.""" + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- LaTeX generation: Original formulas ---------- +def generate_original_latex(boundary_names=None): + """ + Generate the original LaTeX block with integration constraints. + + Uses boundary_names['long_left'] and boundary_names['long_right'] + for integration limits. + """ + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- LaTeX generation: Extended system ---------- +def generate_extended_latex(boundary_names=None): + """ + Generate the extended system LaTeX including a = M^{-1}v and matrix M. + + Uses template-based naming for M matrix entries to avoid double subscripts. + Templates should contain {j} placeholder for row index. + """ + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + + left_template = boundary_names['short_left_template'] + right_template = boundary_names['short_right_template'] + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + def make_row(j_str): + """Generate a matrix row with proper boundary notation.""" + # Safe template substitution + left = left_template.format(j=j_str) + right = right_template.format(j=j_str) + return " & ".join([ + rf"\int_{{{left}}}^{{{right}}} d\xi", + rf"\int_{{{left}}}^{{{right}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{left}}}^{{{right}}} \xi^{{k-1}} d\xi" + ]) + + # Construct matrix with representative rows (0, 1, ..., k-1) + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- Expression formatting with sorted vbar terms ---------- +def _is_vbar_symbol(sym): + """Check if symbol represents \overline{v}_{...}.""" + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + """Extract the index expression from \overline{v}_{index} symbol name.""" + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + # Evaluate in context of global symbols i and r + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + """ + Generate sort key for vbar indices. + Priority: numeric offsets from i (i-1, i, i+1, ...) > symbolic > strings. + """ + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) # Sort by numeric offset + else: + return (0.5, str(offset)) # Symbolic offsets + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + """ + Format polynomial coefficient expression with vbar terms sorted by index. + + Parameters: + expr: SymPy expression for a coefficient + factored: If True, output as (1/24)*v̄; if False, output as v̄/24 + """ + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + + # Check if expression contains any vbar symbols + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + # Parse terms into (coefficient, vbar_symbol, index) tuples + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # Sort terms: vbar terms by index offset, constants first + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) # Constants first + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + # Generate LaTeX for each term + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + # Add parentheses for compound coefficients + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + # Fraction format: v̄/denominator + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + # Combine terms with proper sign handling + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + """Format coefficient vector as bmatrix with sorted terms.""" + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- Main output functions ---------- +def print_solution_for_k_r(k_val, r_val=None, factored=True): + """Print formatted coefficient vector for given k and r values.""" + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- Main execution ---------- +if __name__ == "__main__": + # 1. Default naming (\alpha, \beta) + print("=== Original LaTeX (default: \\alpha, \\beta) ===") + print(generate_original_latex()) + + print("\n\n=== Extended System (default) ===") + print(generate_extended_latex()) + + # 2. Cell-centered naming (x_{j±1/2}) + print("\n\n=== Original LaTeX (x_{j±1/2}) ===") + print(generate_original_latex(boundary_names=X_BOUNDARY_NAMES)) + + print("\n\n=== Extended System (x_{j±1/2}) ===") + print(generate_extended_latex(boundary_names=X_BOUNDARY_NAMES)) + + # 3. Coefficient solutions for k=3, r=0,1,2 + for r_val in [0, 1, 2]: + print_solution_for_k_r(k_val=3, r_val=r_val, factored=True) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/03/matrix.py b/example/figure/1d/weno/matrix/03/matrix.py new file mode 100644 index 00000000..05e388bb --- /dev/null +++ b/example/figure/1d/weno/matrix/03/matrix.py @@ -0,0 +1,352 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- Global symbols ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- Boundary naming strategy ---------- +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', + 'long_right': r'\beta(r,j)', + 'short_left_template': r'\alpha_{{{j}}}', + 'short_right_template': r'\beta_{{{j}}}' +} + +X_BOUNDARY_NAMES = { + 'long_left': r'x_{j-\frac{1}{2}}', + 'long_right': r'x_{j+\frac{1}{2}}', + 'short_left_template': r'x_{{{j}-\frac{{1}}{{2}}}}', + 'short_right_template': r'x_{{{j}+\frac{{1}}{{2}}}}' +} + +# ---------- Helper functions ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): + return -r + j - Rational(1, 2) + +def upper_limit(j): + return -r + j + Rational(1, 2) + +# ---------- Vector and matrix construction ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is not None: + a_j = lower_limit(j).subs(r, r_val) + b_j = upper_limit(j).subs(r, r_val) + else: + a_j = lower_limit(j) + b_j = upper_limit(j) + + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- NEW: Smoothness indicator Sr computation ---------- +def compute_Sr(k_val, r_val=None): + """ + Compute the WENO smoothness indicator Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (d^l p/dξ^l)^2 dξ + + Parameters: + k_val (int): Polynomial order (degree k-1) + r_val (int, optional): Specific r value. If None, keep r symbolic. + + Returns: + Sr_expr: Symbolic expression for Sr in terms of vbar values + p_expr: The polynomial p(r,ξ) + a_vec: Coefficient vector [a0, a1, ..., a_{k-1}] + """ + # Step 1: Get coefficients a = [a0, a1, ..., a_{k-1}] + a_vec, M, v = solve_coefficients(k_val, r_val) + + # Step 2: Construct polynomial p(r,ξ) = sum_{m=0}^{k-1} a_m * ξ^m + p_expr = sum(a_vec[m] * xi**m for m in range(k_val)) + + # Step 3: Compute Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (p^{(l)}(ξ))^2 dξ + Sr_expr = 0 + for l in range(1, k_val): # l = 1, 2, ..., k-1 + # Compute l-th derivative + p_l_deriv = sp.diff(p_expr, xi, l) + # Square it + p_l_squared = p_l_deriv**2 + # Integrate over [-1/2, 1/2] + integral_l = sp.integrate(p_l_squared, (xi, -Rational(1, 2), Rational(1, 2))) + Sr_expr += simplify(integral_l) + + # Final simplification + Sr_expr = simplify(Sr_expr) + return Sr_expr, p_expr, a_vec + +def format_Sr_expression(Sr_expr, factored=True): + """ + Format Sr expression with vbar terms sorted by index (similar to coefficient formatting). + """ + if Sr_expr.is_Number: + return latex(Sr_expr) + Sr_expr = simplify(Sr_expr) + + # Reuse the existing vbar detection and sorting logic + has_vbar = any(_is_vbar_symbol(s) for s in Sr_expr.free_symbols) + if not has_vbar: + return latex(Sr_expr) + + terms = Sr_expr.as_ordered_terms() if Sr_expr.is_Add else [Sr_expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + # Sr may have products of vbar terms (like v_i * v_{i+1}) + # For now, handle single vbar terms (which is the case for k=3) + # For higher k, we might have cross terms + if len(v_syms) == 1: + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + else: + # Handle cross terms or complex expressions + parsed_terms.append((term, None, None)) + + # Sort single vbar terms, keep others as-is + single_vbar_terms = [] + other_terms = [] + for item in parsed_terms: + if item[1] is not None: + single_vbar_terms.append(item) + else: + other_terms.append(item) + + # Sort single vbar terms + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + single_vbar_terms.sort(key=term_sort_key) + all_terms = other_terms + single_vbar_terms + + # Generate LaTeX (simplified for Sr - usually no factoring needed) + latex_parts = [] + for coeff, v, _ in all_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + # For Sr, we typically want expanded form + latex_parts.append(latex(coeff * v)) + + # Combine terms + if not latex_parts: + return "0" + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +# ---------- Existing functions (unchanged) ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +def generate_original_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +def generate_extended_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left_template = boundary_names['short_left_template'] + right_template = boundary_names['short_right_template'] + + lines = [ + r"\mathbf{a}=[a_{0},a_{1},\dots,a_{k-1}]^T,\quad \mathbf{\phi}(\xi) = [\xi^0, \xi^1, \dots, \xi^{k-1}]^{T}", + r"\mathbf{v}=[\overline{v}_{i - r},\overline{v}_{i - r + 1},\dots,\overline{v}_{i + k - r - 1}]^T", + r"\mathbf{a}=M^{-1}\mathbf{v}", + r"\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{k-1} \end{bmatrix} = M^{-1} \begin{bmatrix} \overline{v}_{i - r} \\ \overline{v}_{i - r + 1} \\ \vdots \\ \overline{v}_{i + k - r - 1} \end{bmatrix}" + ] + + def make_row(j_str): + left = left_template.format(j=j_str) + right = right_template.format(j=j_str) + return " & ".join([ + rf"\int_{{{left}}}^{{{right}}} d\xi", + rf"\int_{{{left}}}^{{{right}}} \xi^{{1}} d\xi", + r"\cdots", + rf"\int_{{{left}}}^{{{right}}} \xi^{{k-1}} d\xi" + ]) + + M_body = " \\\\\n".join([ + make_row("0"), + make_row("1"), + r"\vdots & \vdots & \ddots & \vdots", + make_row("k-1") + ]) + M_latex = f"M = \\begin{{bmatrix}}\n{M_body}\n\\end{{bmatrix}}" + lines.append(M_latex) + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +def print_solution_for_k_r(k_val, r_val=None, factored=True): + a_vec, M, v = solve_coefficients(k_val, r_val) + desc = f"k={k_val}" + (f", r={r_val}" if r_val is not None else " (r symbolic)") + print(f"\n=== WENO Coefficients: {desc} ===") + print(latex_a_vector_formatted(a_vec, factored=factored)) + return a_vec, M, v + +# ---------- Main execution with Sr computation ---------- +if __name__ == "__main__": + k_val = 3 + + # Print original and extended systems + print("=== Original LaTeX (default) ===") + print(generate_original_latex()) + + print("\n\n=== Extended System (default) ===") + print(generate_extended_latex()) + + # Compute coefficients and Sr for r = 0, 1, 2 + for r_val in [0, 1, 2]: + print(f"\n" + "="*60) + print(f"=== k={k_val}, r={r_val} ===") + + # Coefficients + a_vec, M, v = solve_coefficients(k_val, r_val) + print("\nCoefficients a = M^{-1} v:") + print(latex_a_vector_formatted(a_vec, factored=True)) + + # Smoothness indicator Sr + Sr_expr, p_expr, a_vec_sr = compute_Sr(k_val, r_val) + print(f"\nSmoothness indicator S_{r_val}:") + print(format_Sr_expression(Sr_expr)) + + # Optional: Show the polynomial + print(f"\nPolynomial p({r_val},ξ):") + print(latex(p_expr)) + + # Also show symbolic Sr (r as symbol) for completeness + print(f"\n" + "="*60) + print(f"=== Symbolic Sr (k={k_val}, r symbolic) ===") + Sr_sym, p_sym, a_sym = compute_Sr(k_val, r_val=None) + print("S_r =") + print(format_Sr_expression(Sr_sym)) \ No newline at end of file diff --git a/example/figure/1d/weno/matrix/03a/matrix.py b/example/figure/1d/weno/matrix/03a/matrix.py new file mode 100644 index 00000000..ef2b909b --- /dev/null +++ b/example/figure/1d/weno/matrix/03a/matrix.py @@ -0,0 +1,254 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- Global symbols ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- Boundary naming strategy ---------- +DEFAULT_BOUNDARY_NAMES = { + 'long_left': r'\alpha(r,j)', + 'long_right': r'\beta(r,j)', + 'short_left_template': r'\alpha_{{{j}}}', + 'short_right_template': r'\beta_{{{j}}}' +} + +# ---------- Helper functions ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +def lower_limit(j): + return -r + j - Rational(1, 2) + +def upper_limit(j): + return -r + j + Rational(1, 2) + +# ---------- Vector and matrix construction ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is not None: + a_j = lower_limit(j).subs(r, r_val) + b_j = upper_limit(j).subs(r, r_val) + else: + a_j = lower_limit(j) + b_j = upper_limit(j) + + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- Enhanced: Smoothness indicator Sr computation ---------- +def compute_Sr(k_val, r_val=None): + """ + Compute the WENO smoothness indicator Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (d^l p/dξ^l)^2 dξ + + This works for any k >= 2 and handles cross terms correctly. + """ + # Get coefficients a = [a0, a1, ..., a_{k-1}] + a_vec, M, v = solve_coefficients(k_val, r_val) + + # Construct polynomial p(r,ξ) = sum_{m=0}^{k-1} a_m * ξ^m + p_expr = sum(a_vec[m] * xi**m for m in range(k_val)) + + # Compute Sr = sum_{l=1}^{k-1} ∫_{-1/2}^{1/2} (p^{(l)}(ξ))^2 dξ + Sr_expr = 0 + for l in range(1, k_val): # l = 1, 2, ..., k-1 + p_l_deriv = sp.diff(p_expr, xi, l) + p_l_squared = p_l_deriv**2 + integral_l = sp.integrate(p_l_squared, (xi, -Rational(1, 2), Rational(1, 2))) + Sr_expr += simplify(integral_l) + + Sr_expr = simplify(Sr_expr) + return Sr_expr, p_expr, a_vec + +def format_Sr_expression(Sr_expr): + """ + Format Sr expression properly, handling both single terms and cross terms. + + For WENO smoothness indicators, the standard format is expanded form + showing all quadratic terms explicitly. + """ + if Sr_expr.is_Number: + return latex(Sr_expr) + + Sr_expr = simplify(Sr_expr) + + # For Sr, we want to expand everything to show the quadratic form + # This is the standard way smoothness indicators are presented in literature + expanded_expr = sp.expand(Sr_expr) + + # If it's just a number after expansion + if expanded_expr.is_Number: + return latex(expanded_expr) + + # Handle the general case by converting directly to LaTeX + # The expanded form will show all cross terms properly + return latex(expanded_expr) + +# ---------- Utility function to get vbar symbols from expression ---------- +def get_vbar_symbols(expr): + """Extract all vbar symbols from an expression.""" + return [s for s in expr.free_symbols if _is_vbar_symbol(s)] + +# ---------- Existing helper functions ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + return simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except: + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (0.5, str(offset)) + except: + return (1, str(index_expr)) + +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Unexpected term: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + parsed_terms.sort(key=term_sort_key) + + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +def generate_original_latex(boundary_names=None): + if boundary_names is None: + boundary_names = DEFAULT_BOUNDARY_NAMES + left = boundary_names['long_left'] + right = boundary_names['long_right'] + lines = [ + r"p(r,\xi) = a_{0}(r)+a_{1}(r)\xi +a_{2}(r)\xi^2+\cdots+a_{k-1}(r)\xi^{k-1}", + rf"\displaystyle\int_{{{left}}}^{{{right}}} p(r,\xi) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}}(r)+a_{{1}}(r)\xi +\cdots+a_{{k-1}}(r)\xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"\displaystyle \int_{{{left}}}^{{{right}}} (a_{{0}} + a_{{1}} \xi + \cdots + a_{{k-1}} \xi^{{k-1}}) d\xi=\overline{{v}}_{{i-r+j}}", + rf"{left}=-r+j-1/2,\quad j=0,1,\cdots,k-1", + rf"{right}=-r+j+1/2,\quad j=0,1,\cdots,k-1" + ] + return r"\begin{array}{l}" + "\n" + " \\\\\n".join(lines) + "\n\\end{array}" + +# ---------- Main execution for both k=3 and k=4 ---------- +if __name__ == "__main__": + # Test both k=3 and k=4 + for k_val in [3, 4]: + print(f"\n{'='*80}") + print(f"COMPUTING FOR k = {k_val}") + print(f"{'='*80}") + + print("\n=== Original LaTeX ===") + print(generate_original_latex()) + + # Compute for r = 0, 1, ..., k-1 + for r_val in range(k_val): + print(f"\n" + "-"*60) + print(f"r = {r_val} (k = {k_val})") + + # Coefficients + a_vec, M, v = solve_coefficients(k_val, r_val) + print("\nCoefficients:") + print(latex_a_vector_formatted(a_vec, factored=True)) + + # Smoothness indicator + Sr_expr, p_expr, _ = compute_Sr(k_val, r_val) + print(f"\nSmoothness indicator S_{r_val}:") + print(format_Sr_expression(Sr_expr)) + + # Show the stencil size + v_symbols = get_vbar_symbols(Sr_expr) + if v_symbols: + print(f"\nStencil involves {len(v_symbols)} points: {[s.name for s in v_symbols]}") + + # Show symbolic version for k=4 + print(f"\n{'='*80}") + print("SYMBOLIC S_r FOR k=4 (r as symbol)") + print(f"{'='*80}") + Sr_sym, p_sym, _ = compute_Sr(4, r_val=None) + print("S_r =") + print(format_Sr_expression(Sr_sym)) \ No newline at end of file diff --git a/example/figure/1d/weno/smoothness/01/polynomial_operations.py b/example/figure/1d/weno/smoothness/01/polynomial_operations.py new file mode 100644 index 00000000..8b8799f9 --- /dev/null +++ b/example/figure/1d/weno/smoothness/01/polynomial_operations.py @@ -0,0 +1,913 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + #demo_smoothness_indicator(1) + #demo_smoothness_indicator(2) + #demo_smoothness_indicator(3) + demo_smoothness_indicator(4) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/01/test_sorting.py b/example/figure/1d/weno/some_help_code/01/test_sorting.py new file mode 100644 index 00000000..a5a531fd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/01/test_sorting.py @@ -0,0 +1,55 @@ +import sympy as sp +import re + +i_sym, r = sp.symbols('i r', integer=True) + +def _extract_index_from_vbar_name(name): + match = re.search(r'\\overline\{v\}_\{(.+)\}', name) + if not match: + return name + index_latex = match.group(1) + try: + # 注意:这里必须传入所有可能符号! + return sp.simplify(eval(index_latex, {'i': i_sym, 'r': r})) + except Exception as e: + print(f"Eval failed for '{index_latex}': {e}") + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + return (1, index_expr) # 字符串放后面 + try: + offset = sp.simplify(index_expr - i_sym) + if offset.is_number: + return (0, float(offset)) + else: + return (1, str(index_expr)) + except: + return (1, str(index_expr)) + +# 测试用例 +test_names = [ + r"\overline{v}_{i - 2}", + r"\overline{v}_{i + 1}", + r"\overline{v}_{i}", + r"\overline{v}_{i - 1}", + r"\overline{v}_{i + 2}", +] + +print("Test 1: Pure i + const") +indices = [_extract_index_from_vbar_name(name) for name in test_names] +print("Indices:", indices) +sorted_names = [name for _, name in sorted(zip(indices, test_names), key=lambda x: _sort_key_from_index(x[0]))] +print("Sorted:", sorted_names) + +# Test with r substituted (like r=0) +print("\nTest 2: After r=0 substitution (should be i, i+1, i+2)") +names_r0 = [ + r"\overline{v}_{i}", # j=0 + r"\overline{v}_{i + 1}", # j=1 + r"\overline{v}_{i + 2}", # j=2 +] +indices2 = [_extract_index_from_vbar_name(name) for name in names_r0] +print("Indices:", indices2) +sorted_names2 = [name for _, name in sorted(zip(indices2, names_r0), key=lambda x: _sort_key_from_index(x[0]))] +print("Sorted:", sorted_names2) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py b/example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py new file mode 100644 index 00000000..d5b5b18b --- /dev/null +++ b/example/figure/1d/weno/some_help_code/01a/weno_debug_sort.py @@ -0,0 +1,170 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- vbar 构造 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +# ---------- 积分限 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 矩阵与向量 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- 排序辅助函数 ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + # 使用全局 i_sym, r + index_expr = eval(index_latex, {'i': i_sym, 'r': r}) + return simplify(index_expr) + except Exception as e: + print(f"[EXTRACT FAILED] name={v_symbol.name}, index_latex={index_latex}, error={e}") + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + print(f" [SORT KEY] string fallback: {index_expr}") + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + key = (0, float(offset)) + print(f" [SORT KEY] {index_expr} → offset={offset} → key={key}") + return key + else: + key = (0.5, str(offset)) + print(f" [SORT KEY] non-numeric offset: {offset} → key={key}") + return key + except Exception as e: + key = (1, str(index_expr)) + print(f" [SORT KEY] exception: {e} → key={key}") + return key + +# ---------- 格式化表达式(带详细调试) ---------- +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + if not expr.has(_is_vbar_symbol): + return latex(expr) + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Term has multiple vbar: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # === DEBUG: 打印解析结果 === + print(f"\n[DEBUG] Expression: {expr}") + for coeff, v, idx in parsed_terms: + if v is None: + print(f" CONSTANT: {coeff}") + else: + print(f" TERM: coeff={coeff}, v={v.name}, index_expr={idx} (type={type(idx)})") + + # === 排序 === + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) # 常数项最先 + return _sort_key_from_index(idx) + + print("[DEBUG] Sorting terms...") + parsed_terms.sort(key=term_sort_key) + + # === 生成 LaTeX === + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + # 合并 + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主程序 ---------- +if __name__ == "__main__": + print("=== Testing k=3, r=0 ===") + a_vec, M, v = solve_coefficients(k_val=3, r_val=0) + print("\nRaw a_vec from SymPy:") + for i, ai in enumerate(a_vec): + print(f"a{i} = {ai}") + + print("\nFormatted output (factored=True):") + formatted = latex_a_vector_formatted(a_vec, factored=True) + print(formatted) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py b/example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py new file mode 100644 index 00000000..b168820c --- /dev/null +++ b/example/figure/1d/weno/some_help_code/01b/weno_debug_sort.py @@ -0,0 +1,173 @@ +import sympy as sp +import re +from sympy import symbols, Rational, Matrix, latex, simplify + +# ---------- 全局符号 ---------- +r, i_sym = symbols('r i', integer=True) +xi = symbols(r'\xi') + +# ---------- vbar 构造 ---------- +def vbar(idx_expr): + name = r"\overline{v}_{" + latex(idx_expr) + r"}" + return sp.Symbol(name) + +# ---------- 积分限 ---------- +def alpha(j): return -r + j - Rational(1, 2) +def beta(j): return -r + j + Rational(1, 2) + +# ---------- 矩阵与向量 ---------- +def build_v_vector(k_val, r_val=None): + if r_val is None: + return Matrix([vbar(i_sym - r + j) for j in range(k_val)]) + else: + return Matrix([vbar(i_sym - r_val + j) for j in range(k_val)]) + +def build_M_matrix(k_val, r_val=None): + M = sp.zeros(k_val, k_val) + for j in range(k_val): + if r_val is None: + a_j = alpha(j) + b_j = beta(j) + else: + a_j = (-r_val + j - Rational(1, 2)) + b_j = (-r_val + j + Rational(1, 2)) + for m in range(k_val): + integral = (b_j**(m + 1) - a_j**(m + 1)) / Rational(m + 1) + M[j, m] = simplify(integral) + return M + +def solve_coefficients(k_val, r_val=None): + M = build_M_matrix(k_val, r_val) + v = build_v_vector(k_val, r_val) + a_vec = M.inv() * v + return a_vec, M, v + +# ---------- 排序辅助函数 ---------- +def _is_vbar_symbol(sym): + return isinstance(sym, sp.Symbol) and sym.name.startswith(r"\overline{v}_{") + +def _extract_index_from_vbar(v_symbol): + match = re.search(r'\\overline\{v\}_\{(.+)\}', v_symbol.name) + if not match: + return v_symbol.name + index_latex = match.group(1) + try: + # 使用全局 i_sym, r + index_expr = eval(index_latex, {'i': i_sym, 'r': r}) + return simplify(index_expr) + except Exception as e: + print(f"[EXTRACT FAILED] name={v_symbol.name}, index_latex={index_latex}, error={e}") + return index_latex + +def _sort_key_from_index(index_expr): + if isinstance(index_expr, str): + print(f" [SORT KEY] string fallback: {index_expr}") + return (1, index_expr) + try: + offset = simplify(index_expr - i_sym) + if offset.is_number: + key = (0, float(offset)) + print(f" [SORT KEY] {index_expr} → offset={offset} → key={key}") + return key + else: + key = (0.5, str(offset)) + print(f" [SORT KEY] non-numeric offset: {offset} → key={key}") + return key + except Exception as e: + key = (1, str(index_expr)) + print(f" [SORT KEY] exception: {e} → key={key}") + return key + +# ---------- 格式化表达式(带详细调试) ---------- +def format_a_expression(expr, factored=True): + if expr.is_Number: + return latex(expr) + expr = simplify(expr) + + # === FIX: 正确检测 vbar 符号 === + has_vbar = any(_is_vbar_symbol(s) for s in expr.free_symbols) + if not has_vbar: + return latex(expr) + # ============================== + + terms = expr.as_ordered_terms() if expr.is_Add else [expr] + parsed_terms = [] + for term in terms: + v_syms = [s for s in term.free_symbols if _is_vbar_symbol(s)] + if not v_syms: + parsed_terms.append((term, None, None)) + else: + if len(v_syms) != 1: + raise ValueError(f"Term has multiple vbar: {term}") + v = v_syms[0] + coeff = simplify(term / v) + index_expr = _extract_index_from_vbar(v) + parsed_terms.append((coeff, v, index_expr)) + + # === DEBUG: 打印解析结果 === + print(f"\n[DEBUG] Expression: {expr}") + for coeff, v, idx in parsed_terms: + if v is None: + print(f" CONSTANT: {coeff}") + else: + print(f" TERM: coeff={coeff}, v={v.name}, index_expr={idx} (type={type(idx)})") + + # === 排序 === + def term_sort_key(item): + _, v, idx = item + if v is None: + return (-1, 0) + return _sort_key_from_index(idx) + + print("[DEBUG] Sorting terms...") + parsed_terms.sort(key=term_sort_key) + + # === 生成 LaTeX === + latex_parts = [] + for coeff, v, _ in parsed_terms: + if v is None: + latex_parts.append(latex(coeff)) + else: + if factored: + if coeff == 1: + s = latex(v) + elif coeff == -1: + s = "-" + latex(v) + else: + c_latex = latex(coeff) + if any(op in c_latex for op in ['+', '-']) and not c_latex.startswith('-'): + c_latex = f"({c_latex})" + s = f"{c_latex} {latex(v)}" + else: + if coeff.is_Rational and abs(coeff.p) == 1: + sign = "-" if coeff.p < 0 else "" + den = str(coeff.q) + s = f"{sign}\\frac{{{latex(v)}}}{{{den}}}" + else: + s = latex(coeff * v) + latex_parts.append(s) + + result = latex_parts[0] + for part in latex_parts[1:]: + if part.startswith("-"): + result += " " + part + else: + result += " + " + part + return result + +def latex_a_vector_formatted(a_vec, factored=True): + entries = [format_a_expression(a_vec[i], factored=factored) for i in range(len(a_vec))] + body = " \\\\ ".join(entries) + return f"\\begin{{bmatrix}} {body} \\end{{bmatrix}}" + +# ---------- 主程序 ---------- +if __name__ == "__main__": + print("=== Testing k=3, r=0 ===") + a_vec, M, v = solve_coefficients(k_val=3, r_val=0) + print("\nRaw a_vec from SymPy:") + for i, ai in enumerate(a_vec): + print(f"a{i} = {ai}") + + print("\nFormatted output (factored=True):") + formatted = latex_a_vector_formatted(a_vec, factored=True) + print(formatted) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02/testprj.py b/example/figure/1d/weno/some_help_code/02/testprj.py new file mode 100644 index 00000000..9ba11f09 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02/testprj.py @@ -0,0 +1,31 @@ +import sympy as sp + +# 定义符号变量 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 定义三个beta表达式 +beta0 = (13/sp.Integer(12) * (v(i) - 2*v(i+1) + v(i+2))**2 + + sp.Rational(1,4) * (3*v(i) - 4*v(i+1) + v(i+2))**2) + +beta1 = (13/sp.Integer(12) * (v(i-1) - 2*v(i) + v(i+1))**2 + + sp.Rational(1,4) * (v(i-1) - v(i+1))**2) + +beta2 = (13/sp.Integer(12) * (v(i-2) - 2*v(i-1) + v(i))**2 + + sp.Rational(1,4) * (v(i-2) - 4*v(i-1) + 3*v(i))**2) + +# 展开表达式 +beta0_expanded = sp.expand(beta0) +beta1_expanded = sp.expand(beta1) +beta2_expanded = sp.expand(beta2) + +print(f"beta0_expanded={beta0_expanded}") +print(f"beta1_expanded={beta1_expanded}") +print(f"beta2_expanded={beta2_expanded}") + +print("β0 的展开式:") +print(sp.latex(beta0_expanded)) +print("\nβ1 的展开式:") +print(sp.latex(beta1_expanded)) +print("\nβ2 的展开式:") +print(sp.latex(beta2_expanded)) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02a/testprj.py b/example/figure/1d/weno/some_help_code/02a/testprj.py new file mode 100644 index 00000000..1343b890 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02a/testprj.py @@ -0,0 +1,60 @@ +import sympy as sp + +# 1. 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 2. 原始公式(目标) +beta0_original = (13/sp.Integer(12) * (v(i) - 2*v(i+1) + v(i+2))**2 + + sp.Rational(1,4) * (3*v(i) - 4*v(i+1) + v(i+2))**2) + +# 3. 展开式(已知条件) +beta0_expanded = sp.expand(beta0_original) +print("已知展开式:") +print(beta0_expanded) +print("\n" + "-"*80 + "\n") + +# 4. 逆向求解:假设未知线性表达式 +a, b = sp.symbols('a b') # 系数 +c1, c2, c3 = sp.symbols('c1 c2 c3') # 第一个线性表达式的系数 +d1, d2, d3 = sp.symbols('d1 d2 d3') # 第二个线性表达式的系数 + +# 设未知形式: a*(c1*v[i] + c2*v[i+1] + c3*v[i+2])**2 + b*(d1*v[i] + d2*v[i+1] + d3*v[i+2])**2 +unknown_form = a*(c1*v(i) + c2*v(i+1) + c3*v(i+2))**2 + b*(d1*v(i) + d2*v(i+1) + d3*v(i+2))**2 +unknown_expanded = sp.expand(unknown_form) + +# 5. 建立方程:比较同类项系数 +variables = [v(i)**2, v(i+1)**2, v(i+2)**2, + v(i)*v(i+1), v(i)*v(i+2), v(i+1)*v(i+2)] + +equations = [] +for var in variables: + # 获取两边系数并建立等式 + coeff_original = sp.expand(beta0_expanded).coeff(var) + coeff_unknown = unknown_expanded.coeff(var) + equations.append(sp.Eq(coeff_unknown, coeff_original)) + +print("建立的方程组:") +for eq in equations: + print(f" {eq}") + +# 6. 求解(我们知道应该有多个解,添加约束条件) +# 添加约束:系数为有理数,且第二个表达式首项系数为1(消除缩放 ambiguity) +constraints = [sp.Eq(d1, 1), sp.Eq(a, sp.Rational(13,12)), sp.Eq(b, sp.Rational(1,4))] +# 实际上更简单的方法是:直接匹配我们的预期模式 + +print("\n" + "-"*80 + "\n") +print("逆向求解结果:") + +# 更直接的方法:通过模式匹配求解 +print("β0 的平方和分解:") +# 提取 v(i), v(i+1), v(i+2) 的系数 +L1 = v(i) - 2*v(i+1) + v(i+2) +L2 = 3*v(i) - 4*v(i+1) + v(i+2) + +print(f" 第一项: {sp.Rational(13,12)} × ({L1})²") +print(f" 第二项: {sp.Rational(1,4)} × ({L2})²") + +# 验证展开是否一致 +recovered = sp.expand(sp.Rational(13,12)*L1**2 + sp.Rational(1,4)*L2**2) +print(f"\n 验证 - 重建的展开式与原始一致: {recovered == beta0_expanded}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02aa/testprj.py b/example/figure/1d/weno/some_help_code/02aa/testprj.py new file mode 100644 index 00000000..033bc20e --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02aa/testprj.py @@ -0,0 +1,76 @@ +import sympy as sp + +# 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 定义四个连续点的函数值 +v0 = v(i) +v1 = v(i+1) +v2 = v(i+2) +v3 = v(i+3) + +# 定义三个线性表达式 +L1 = v0 - 3*v1 + 3*v2 - v3 # 三阶差分 +L2 = 2*v0 - 5*v1 + 4*v2 - v3 # WENO专用模板 +L3 = 43*v0 - 69*v1 + 33*v2 - 7*v3 # 高阶修正项 + +# 定义完整的β₀表达式 +beta0 = (sp.Rational(1043,960) * L1**2 + + sp.Rational(13,12) * L2**2 + + sp.Rational(1,288) * L3 * L1 + + sp.Rational(1,576) * L3**2) + +print("="*80) +print("β₀ 原始表达式结构") +print("="*80) +print(f"项1: 1043/960 × ({L1})²") +print(f"项2: 13/12 × ({L2})²") +print(f"项3: 1/288 × ({L3}) × ({L1})") +print(f"项4: 1/576 × ({L3})²") +print("\n" + "="*80) +print("完全展开式") +print("="*80) + +# 完全展开 +beta0_expanded = sp.expand(beta0) + +# 输出LaTeX公式 +latex_output = sp.latex(beta0_expanded) +print(f"LaTeX公式:") +print(f"\\[") +print(f"\\beta_0 = {latex_output}") +print(f"\\]") + +# 为了更清晰地显示,按变量分组 +print("\n" + "="*80) +print("按变量分组的展开式") +print("="*80) + +# 收集同类项 +terms = { + 'v0²': beta0_expanded.coeff(v0**2), + 'v1²': beta0_expanded.coeff(v1**2), + 'v2²': beta0_expanded.coeff(v2**2), + 'v3²': beta0_expanded.coeff(v3**2), + 'v0v1': beta0_expanded.coeff(v0*v1), + 'v0v2': beta0_expanded.coeff(v0*v2), + 'v0v3': beta0_expanded.coeff(v0*v3), + 'v1v2': beta0_expanded.coeff(v1*v2), + 'v1v3': beta0_expanded.coeff(v1*v3), + 'v2v3': beta0_expanded.coeff(v2*v3), +} + +for term, coeff in terms.items(): + if coeff != 0: + print(f"{term}: {coeff} = {float(coeff):.6f}") + +# 输出完整的数学表达式 +print("\n" + "="*80) +print("完整的数学表达式") +print("="*80) +print("\\beta_0 = ") +for term, coeff in terms.items(): + if coeff != 0: + sign = "+" if coeff > 0 else "" + print(f" {sign}{coeff} \\cdot {term.replace('v', 'v_{i+').replace('²', '}^2').replace('v0', 'v_i').replace('v1', 'v_{i+1}').replace('v2', 'v_{i+2}').replace('v3', 'v_{i+3}')}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02b/testprj.py b/example/figure/1d/weno/some_help_code/02b/testprj.py new file mode 100644 index 00000000..f4ae070b --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02b/testprj.py @@ -0,0 +1,296 @@ +import sympy as sp + +# 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') + +# 输入:已知的展开式(不给出原始表达式) +beta0_expanded = (10*v(i)**2/3 - 31*v(i)*v(i+1)/3 + 11*v(i)*v(i+2)/3 + + 25*v(i+1)**2/3 - 19*v(i+1)*v(i+2)/3 + 4*v(i+2)**2/3) + +beta1_expanded = (13*v(i)**2/3 - 13*v(i)*v(i-1)/3 - 13*v(i)*v(i+1)/3 + + 4*v(i-1)**2/3 + 5*v(i-1)*v(i+1)/3 + 4*v(i+1)**2/3) + +beta2_expanded = (10*v(i)**2/3 + 11*v(i)*v(i-2)/3 - 31*v(i)*v(i-1)/3 + + 4*v(i-2)**2/3 - 19*v(i-2)*v(i-1)/3 + 25*v(i-1)**2/3) + +def solve_sos_decomposition(expanded_expr, variables): + """ + 求解形如 c1*L1^2 + c2*L2^2 的SOS分解 + + 参数: + expanded_expr: 展开后的表达式 + variables: 变量列表,如 [v(i), v(i+1), v(i+2)] + + 返回: + 分解结果列表 [(c1, L1), (c2, L2)] + """ + print(f"\n{'='*80}") + print(f"求解变量的SOS分解: {variables}") + print(f"{'='*80}") + + # 1. 构造对称矩阵Q,使得 expr = v^T * Q * v + n = len(variables) + Q = sp.zeros(n, n) + + print("\n步骤1: 构造二次型矩阵 Q") + print("-" * 40) + + # 对角线元素 + for i in range(n): + coeff = expanded_expr.coeff(variables[i]**2) + Q[i, i] = coeff + print(f"Q[{i},{i}] = {variables[i]}² 的系数 = {coeff}") + + # 非对角线元素(表达式中 2*x*y 的系数对应 Q[i,j] + Q[j,i] = 2*Q[i,j]) + for i in range(n): + for j in range(i+1, n): + coeff = expanded_expr.coeff(variables[i]*variables[j]) + # 由于表达式中是 var_i*var_j,矩阵中对应 2*Q[i,j] + Q[i, j] = coeff / 2 + Q[j, i] = coeff / 2 + print(f"Q[{i},{j}] = Q[{j},{i}] = {variables[i]}*{variables[j]} 系数/2 = {coeff}/2 = {coeff/2}") + + print(f"\n得到的对称矩阵 Q:") + sp.pprint(Q) + + # 2. 特征值分解(理论上有两个非零特征值) + print("\n步骤2: 矩阵的特征值分解") + print("-" * 40) + eigenvals = Q.eigenvals() + print("特征值及其重数:") + for val, mult in eigenvals.items(): + print(f" λ = {sp.nsimplify(val)} (重数: {mult})") + + # 3. 寻找秩-1分解 + # 理论上 Q = c1*w1*w1^T + c2*w2*w2^T + print("\n步骤3: 求解秩-1分解") + print("-" * 40) + + # 方法:通过比较法直接求解 + # 设未知线性表达式: L1 = a1*var0 + a2*var1 + a3*var2 + # 设未知线性表达式: L2 = b1*var0 + b2*var1 + b3*var2 + # 则 Q = c1*[a1,a2,a3]^T*[a1,a2,a3] + c2*[b1,b2,b3]^T*[b1,b2,b3] + + # 未知系数 + c1, c2 = sp.symbols('c1 c2') + a1, a2, a3 = sp.symbols('a1 a2 a3') + b1, b2, b3 = sp.symbols('b1 b2 b3') + + # 构造秩-1矩阵 + w1 = sp.Matrix([a1, a2, a3]) + w2 = sp.Matrix([b1, b2, b3]) + R1 = c1 * (w1 * w1.T) + R2 = c2 * (w2 * w2.T) + R = R1 + R2 + + print("假设分解形式: Q = c1*[a1,a2,a3]ᵀ[a1,a2,a3] + c2*[b1,b2,b3]ᵀ[b1,b2,b3]") + + # 4. 比较矩阵元素,建立方程组 + equations = [] + for i_idx in range(n): + for j_idx in range(n): + equations.append(sp.Eq(R[i_idx, j_idx], Q[i_idx, j_idx])) + + print(f"\n建立 {len(equations)} 个方程:") + for idx, eq in enumerate(equations[:6]): # 显示前6个 + print(f" 方程 {idx+1}: {eq}") + if len(equations) > 6: + print(f" ... 还有 {len(equations)-6} 个方程") + + # 5. 添加约束条件求解 + # 约束1: 系数是有理数 + # 约束2: 消除缩放歧义(令某些系数为特定值) + # 约束3: c1, c2 应为正数(因为是平方和) + + print("\n步骤4: 求解方程组(添加约束消除缩放歧义)") + print("-" * 40) + print("添加约束: b1=1(固定第一个系数)") + + # 实际求解时使用数值方法先找到近似解 + equations_constrained = equations + [sp.Eq(b1, 1)] + + # 使用 nsolve 寻找数值解(需要提供初始值) + print("\n使用数值求解作为初始猜测...") + try: + # 初始猜测 + initial_guess = { + a1: 1, a2: -2, a3: 1, + b1: 1, b2: -2, b3: 1, + c1: 1, c2: 1 + } + + # 选择9个独立方程求解9个未知数 + sol = sp.nsolve(equations_constrained[:9], + [a1, a2, a3, b1, b2, b3, c1, c2], + [1, -2, 1, 1, -2, 1, 1, 1], + tol=1e-14, maxsteps=100) + + print(f"数值解: {sol}") + + # 根据数值解模式,推断符号解 + # 观察数值解的模式,手动构造符号解 + + except Exception as e: + print(f"数值求解遇到错误: {e}") + print("切换到符号模式匹配...") + + # 6. 使用启发式方法:寻找整数/有理数解 + # 观察矩阵Q的结构,尝试匹配模式 + + print("\n步骤5: 启发式模式匹配") + print("-" * 40) + + # 对于beta0,观察系数模式 + # Q = [[10/3, -31/6, 11/6], + # [-31/6, 25/3, -19/6], + # [11/6, -19/6, 4/3]] + + # 尝试寻找形如 (1, -2, 1) 的模式 + # 这是差分算子的典型模式 + + test_vector1 = sp.Matrix([1, -2, 1]) # 二阶差分 + test_vector2 = sp.Matrix([1, -1, 0]) # 一阶差分 + test_vector3 = sp.Matrix([3, -4, 1]) # WENO的特定组合 + + # 测试哪个向量能匹配 + print("测试候选向量:") + + candidates = [ + ("[1, -2, 1] (二阶差分)", sp.Matrix([1, -2, 1])), + ("[1, -1, 0]", sp.Matrix([1, -1, 0])), + ("[3, -4, 1] (WENO专用)", sp.Matrix([3, -4, 1])), + ] + + for name, vec in candidates: + # 计算 rank(Q - λ*vv^T) + vvt = vec * vec.T + print(f"\n 测试向量 {name}:") + sp.pprint(vec.T) + + # 尝试用最小二乘拟合求解λ + # 对于矩阵的每个非零元素位置 + ratios = [] + for ii in range(n): + for jj in range(n): + if vvt[ii, jj] != 0: + ratios.append(Q[ii, jj] / vvt[ii, jj]) + + if ratios: + avg_ratio = sum(ratios) / len(ratios) + print(f" 平均比例系数: {sp.nsimplify(avg_ratio)}") + + # 验证剩余矩阵 + residual = Q - sp.nsimplify(avg_ratio) * vvt + print(f" 剩余矩阵的秩: {residual.rank()}") + + if residual.rank() == 1: + print(f" ✓ 成功!剩余矩阵秩为1,可以继续分解") + # 寻找第二个向量 + # 剩余矩阵应为 c2 * w2 * w2^T + for name2, vec2 in candidates: + if name2 != name: + vvt2 = vec2 * vec2.T + ratios2 = [] + for ii in range(n): + for jj in range(n): + if vvt2[ii, jj] != 0 and residual[ii, jj] != 0: + ratios2.append(residual[ii, jj] / vvt2[ii, jj]) + if ratios2: + avg_ratio2 = sum(ratios2) / len(ratios2) + residual2 = residual - sp.nsimplify(avg_ratio2) * vvt2 + if residual2.norm() < 1e-10: + print(f" 第二个向量 {name2}, 系数: {sp.nsimplify(avg_ratio2)}") + return [(sp.nsimplify(avg_ratio), vec), (sp.nsimplify(avg_ratio2), vec2)] + + # 如果没有找到完美匹配,使用特征值方法 + print("\n 使用特征值分解方法...") + eigenvecs = Q.eigenvects() + + sos_decomp = [] + for val_mult in eigenvecs: + val = sp.nsimplify(val_mult[0]) + mult = val_mult[1] + if abs(val) > 1e-10: + # 获取特征向量 + vec = val_mult[2][0] # 第一个特征向量 + # 归一化 + vec_simplified = sp.nsimplify(vec / sp.sqrt(vec.dot(vec))) + sos_decomp.append((val, vec_simplified)) + + return sos_decomp + +def format_sos_result(decomp, variables): + """格式化SOS分解结果为可读形式""" + if not decomp: + return "未找到分解" + + result = [] + for idx, (coeff, vec) in enumerate(decomp): + # 构建线性表达式 + terms = [] + for var_idx, var in enumerate(variables): + if vec[var_idx] != 0: + term = sp.nsimplify(vec[var_idx]) * var + terms.append(str(term)) + + linear_expr = " + ".join(terms).replace("+ -", "- ") + result.append(f" 项{idx+1}: {sp.nsimplify(coeff)} × ({linear_expr})²") + + return "\n".join(result) + +# 对每个beta进行分解 +print("\n" + "="*80) +print("SOS分解逆向求解") +print("="*80) + +# β0分解 +vars0 = [v(i), v(i+1), v(i+2)] +decomp0 = solve_sos_decomposition(beta0_expanded, vars0) +print("\n最终结果:") +print(format_sos_result(decomp0, vars0)) + +# β1分解 +vars1 = [v(i-1), v(i), v(i+1)] +decomp1 = solve_sos_decomposition(beta1_expanded, vars1) +print("\nβ1的SOS分解:") +print(format_sos_result(decomp1, vars1)) + +# β2分解 +vars2 = [v(i-2), v(i-1), v(i)] +decomp2 = solve_sos_decomposition(beta2_expanded, vars2) +print("\nβ2的SOS分解:") +print(format_sos_result(decomp2, vars2)) + +# 验证分解 +print("\n="*80) +print("验证分解正确性") +print("="*80) + +def verify_decomposition(original_expanded, decomp, variables): + """验证分解是否正确重构原式""" + if not decomp: + return False + + reconstructed = 0 + for coeff, vec in decomp: + # 构造线性表达式 + linear = sum(sp.nsimplify(vec[idx]) * var for idx, var in enumerate(variables)) + reconstructed += sp.nsimplify(coeff) * linear**2 + + reconstructed = sp.expand(reconstructed) + diff = sp.simplify(reconstructed - original_expanded) + return diff == 0 + +# 验证每个分解 +for idx, (decomp, vars_list, original) in enumerate([ + (decomp0, vars0, beta0_expanded), + (decomp1, vars1, beta1_expanded), + (decomp2, vars2, beta2_expanded) +], 0): + print(f"\nβ{idx} 验证:") + is_correct = verify_decomposition(original, decomp, vars_list) + print(f" 分解正确: {is_correct}") + + if is_correct: + print(f" 重建表达式: {sp.expand(sum(sp.nsimplify(coeff)*sum(sp.nsimplify(vec[idx])*var for idx, var in enumerate(vars_list))**2 for coeff, vec in decomp))}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02c/testprj.py b/example/figure/1d/weno/some_help_code/02c/testprj.py new file mode 100644 index 00000000..fc71a375 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02c/testprj.py @@ -0,0 +1,21 @@ +from sympy import symbols, Rational, expand, latex + +# 定义符号变量 +v_im2, v_im1, v_i, v_ip1, v_ip2 = symbols('v_{i-2} v_{i-1} v_i v_{i+1} v_{i+2}') + +# β0 的表达式 +beta0_expr = Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2 +beta0_expanded = expand(beta0_expr) + +# β1 的表达式 +beta1_expr = Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2 +beta1_expanded = expand(beta1_expr) + +# β2 的表达式 +beta2_expr = Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2 +beta2_expanded = expand(beta2_expr) + +# 输出 LaTeX 公式 +print("β0 = " + latex(beta0_expanded)) +print("β1 = " + latex(beta1_expanded)) +print("β2 = " + latex(beta2_expanded)) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02d/formulas.json b/example/figure/1d/weno/some_help_code/02d/formulas.json new file mode 100644 index 00000000..9033ca52 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02d/formulas.json @@ -0,0 +1,5 @@ +{ + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" +} \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02d/testprj.py b/example/figure/1d/weno/some_help_code/02d/testprj.py new file mode 100644 index 00000000..5fb4a72f --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02d/testprj.py @@ -0,0 +1,33 @@ +import json +from sympy import symbols, Rational, expand, latex, sympify + +# 定义符号变量(假设输入公式中使用这些符号) +v_im2, v_im1, v_i, v_ip1, v_ip2 = symbols('v_{i-2} v_{i-1} v_i v_{i+1} v_{i+2}') + +def read_and_expand_formulas(file_path): + """ + 从JSON文件中读取公式表达式,展开并输出LaTeX。 + + 输入文件格式:JSON对象,键为"beta0", "beta1", "beta2",值为SymPy兼容的字符串表达式。 + 示例文件内容: + { + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" + } + """ + with open(file_path, 'r') as f: + formulas = json.load(f) + + results = {} + for name, expr_str in formulas.items(): + expr = sympify(expr_str) + expanded = expand(expr) + results[name] = latex(expanded) + + # 输出LaTeX公式 + for name, latex_str in results.items(): + print(f"{name} = " + latex_str) + +# 使用示例:替换为实际文件路径 +read_and_expand_formulas('formulas.json') \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02e/formulas.json b/example/figure/1d/weno/some_help_code/02e/formulas.json new file mode 100644 index 00000000..9033ca52 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02e/formulas.json @@ -0,0 +1,5 @@ +{ + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" +} \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/02e/testprj.py b/example/figure/1d/weno/some_help_code/02e/testprj.py new file mode 100644 index 00000000..49c91fa5 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/02e/testprj.py @@ -0,0 +1,48 @@ +import json +from sympy import symbols, Rational, expand, latex, sympify + +# 定义符号变量(使用LaTeX下标名称) +v_im2, v_im1, v_i, v_ip1, v_ip2 = symbols('v_{i-2} v_{i-1} v_i v_{i+1} v_{i+2}') + +def read_and_expand_formulas(file_path): + """ + 从JSON文件中读取公式表达式,展开并输出指定的LaTeX格式。 + + 输入文件格式:JSON对象,键为"beta0", "beta1", "beta2",值为SymPy兼容的字符串表达式。 + 示例文件内容: + { + "beta0": "Rational(13, 12) * (v_i - 2 * v_ip1 + v_ip2)**2 + Rational(1, 4) * (3 * v_i - 4 * v_ip1 + v_ip2)**2", + "beta1": "Rational(13, 12) * (v_im1 - 2 * v_i + v_ip1)**2 + Rational(1, 4) * (v_im1 - v_ip1)**2", + "beta2": "Rational(13, 12) * (v_im2 - 2 * v_im1 + v_i)**2 + Rational(1, 4) * (v_im2 - 4 * v_im1 + 3 * v_i)**2" + } + """ + with open(file_path, 'r') as f: + formulas = json.load(f) + + # 定义局部变量字典,用于sympify解析表达式 + locals_dict = { + 'v_im2': v_im2, + 'v_im1': v_im1, + 'v_i': v_i, + 'v_ip1': v_ip1, + 'v_ip2': v_ip2 + } + + results = {} + for name, expr_str in formulas.items(): + expr = sympify(expr_str, locals=locals_dict) + expanded = expand(expr) + results[name] = latex(expanded) + + # 输出指定的LaTeX数组格式 + output = r'\begin{array}{l}' + '\n' + for i, name in enumerate(['beta0', 'beta1', 'beta2']): + output += f'β{name[4:]} = {results[name]}' + if i < 2: + output += r'\\' + output += '\n' + output += r'\end{array}' + print(output) + +# 使用示例:替换为实际文件路径 +read_and_expand_formulas('formulas.json') \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03/testprj.py b/example/figure/1d/weno/some_help_code/03/testprj.py new file mode 100644 index 00000000..80d7d197 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03/testprj.py @@ -0,0 +1,87 @@ +import sympy as sp + +class LatexParser: + def __init__(self): + self.symbols = {} + + def register_symbols(self, symbol_list): + """预定义符号""" + for sym in symbol_list: + self.symbols[sym] = sp.symbols(sym) + + def parse(self, latex_str, use_backup=False): + """ + 解析 LaTeX 字符串 + use_backup: 如果主要方法失败,是否使用备选方案 + """ + # 方法1: 尝试 sympy 的 parse_latex + try: + expr = parse_latex(latex_str) + return expr, "sympy_parse_latex" + except: + if not use_backup: + raise + + # 方法2: 转换为 sympy 友好格式 + try: + sympy_friendly = self._latex_to_sympy_friendly(latex_str) + expr = sp.sympify(sympy_friendly, locals=self.symbols) + return expr, "converted_sympify" + except: + pass + + # 方法3: 尝试 latex2sympy2 (如果安装) + try: + from latex2sympy2 import latex2sympy + expr = latex2sympy(latex_str) + return expr, "latex2sympy2" + except ImportError: + print("警告: 未安装 latex2sympy2") + except Exception: + pass + + raise ValueError(f"无法解析 LaTeX: {latex_str}") + + def _latex_to_sympy_friendly(self, latex_str): + """内部转换方法""" + # 简化的转换逻辑 + import re + + replacements = [ + (r'\\frac{(.*?)}{(.*?)}', r'(\1)/(\2)'), + (r'\\sqrt{(.*?)}', r'sqrt(\1)'), + (r'\^', r'**'), + (r'\\cdot', r'*'), + (r'\\times', r'*'), + (r'\\div', r'/'), + ] + + result = latex_str + for pattern, replacement in replacements: + result = re.sub(pattern, replacement, result) + + # 移除括号 + result = result.replace('{', '(').replace('}', ')') + + return result + +# 使用示例 +parser = LatexParser() +parser.register_symbols(['x', 'y', 'z', 'alpha', 'beta']) + +test_cases = [ + (r"x^2 + y^2", "简单幂运算"), + (r"\frac{x + 1}{y - 2}", "分数"), + (r"\sin(\alpha) \cdot \cos(\beta)", "三角函数"), +] + +for latex_str, description in test_cases: + try: + expr, method = parser.parse(latex_str, use_backup=True) + print(f"{description}:") + print(f" LaTeX: {latex_str}") + print(f" 方法: {method}") + print(f" 结果: {expr}") + print() + except Exception as e: + print(f"{description} 解析失败: {e}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03a/testprj.py b/example/figure/1d/weno/some_help_code/03a/testprj.py new file mode 100644 index 00000000..6555a1ed --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03a/testprj.py @@ -0,0 +1,108 @@ +import sympy as sp +import numpy as np + +# 定义变量 +v = sp.symbols('v0:6') # v0, v1, v2, v3, v4, v5 +# 为了对应原表达式,设定索引映射: +# v[i] -> v2 +# v[i+1] -> v3 +# v[i+2] -> v4 +# v[i-1] -> v1 +# v[i-2] -> v0 +# 我们用通用符号,之后替换 + +i = 2 # 中间索引为2,则 v[i]=v2, v[i+1]=v3, v[i+2]=v4, v[i-1]=v1, v[i-2]=v0 + +# 定义原表达式 +beta0_expr = 10*v[2]**2/3 - 31*v[2]*v[3]/3 + 11*v[2]*v[4]/3 + 25*v[3]**2/3 - 19*v[3]*v[4]/3 + 4*v[4]**2/3 +beta1_expr = 13*v[2]**2/3 - 13*v[2]*v[1]/3 - 13*v[2]*v[3]/3 + 4*v[1]**2/3 + 5*v[1]*v[3]/3 + 4*v[3]**2/3 +beta2_expr = 10*v[2]**2/3 + 11*v[2]*v[0]/3 - 31*v[2]*v[1]/3 + 4*v[0]**2/3 - 19*v[0]*v[1]/3 + 25*v[1]**2/3 + +# 将二次型转换为矩阵形式 +def quad_form_to_matrix(expr, var_list): + """返回二次型 expr 关于变量 var_list 的对称矩阵 A""" + n = len(var_list) + A = sp.zeros(n, n) + for i in range(n): + for j in range(i, n): + coeff = expr.coeff(var_list[i]*var_list[j]) + if i == j: + A[i,j] = coeff + else: + # 交叉项系数,二次型中 x_i x_j 系数对应矩阵 (i,j) 和 (j,i) 各一半 + A[i,j] = coeff/2 + A[j,i] = coeff/2 + return A + +# 对 beta0 +var0 = [v[2], v[3], v[4]] +A0 = quad_form_to_matrix(beta0_expr, var0) +print("A0 =", A0) + +# 对 beta1 +var1 = [v[1], v[2], v[3]] +A1 = quad_form_to_matrix(beta1_expr, var1) +print("A1 =", A1) + +# 对 beta2 +var2 = [v[0], v[1], v[2]] +A2 = quad_form_to_matrix(beta2_expr, var2) +print("A2 =", A2) + +# 特征值分解找平方和 +def sos_from_matrix(A, vars): + """将半正定矩阵A分解为 sum_i (linear_form)^2""" + # 对称矩阵的特征值分解 + A_np = np.array(A, dtype=float) + eigvals, eigvecs = np.linalg.eigh(A_np) + # 只取正特征值 + sos_terms = [] + for val, vec in zip(eigvals, eigvecs.T): + if abs(val) > 1e-10: + linear = sum(float(c)*x for c, x in zip(vec, vars)) + sos_terms.append((val, linear)) + return sos_terms + +print("\n--- Beta0 SOS ---") +sos0 = sos_from_matrix(A0, var0) +for val, lin in sos0: + print(f"{val:.6f} * ({lin})^2") + +print("\n--- Beta1 SOS ---") +sos1 = sos_from_matrix(A1, var1) +for val, lin in sos1: + print(f"{val:.6f} * ({lin})^2") + +print("\n--- Beta2 SOS ---") +sos2 = sos_from_matrix(A2, var2) +for val, lin in sos2: + print(f"{val:.6f} * ({lin})^2") + +# 也可以尝试用符号 Cholesky 分解(但要求正定,这里可能半正定) +# 用 sympy 的 LDL 分解(对半正定有效) +def sos_ldl(A, vars): + """LDL^T 分解得到平方和""" + L, D = A.LDLdecomposition() + # A = L * D * L.T + # 那么 x^T A x = (sqrt(D) L^T x)^T (sqrt(D) L^T x) + n = A.rows + terms = [] + for j in range(n): + if D[j, j] != 0: + linear = sum(L[i, j]*vars[i] for i in range(n)) + terms.append((D[j, j], linear)) + return terms + +print("\n--- 使用 LDL 分解 ---") +print("Beta0:") +terms0 = sos_ldl(A0, var0) +for coeff, lin in terms0: + print(f"{coeff} * ({lin})^2") +print("Beta1:") +terms1 = sos_ldl(A1, var1) +for coeff, lin in terms1: + print(f"{coeff} * ({lin})^2") +print("Beta2:") +terms2 = sos_ldl(A2, var2) +for coeff, lin in terms2: + print(f"{coeff} * ({lin})^2") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03b/testprj.py b/example/figure/1d/weno/some_help_code/03b/testprj.py new file mode 100644 index 00000000..a397b9f9 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03b/testprj.py @@ -0,0 +1,220 @@ +import sympy as sp +import numpy as np + +# 定义变量 +v = sp.symbols('v0:6') # v0, v1, v2, v3, v4, v5 +# 索引映射: +# v[i] -> v2 +# v[i+1] -> v3 +# v[i+2] -> v4 +# v[i-1] -> v1 +# v[i-2] -> v0 + +# 定义原表达式 +beta0_expr = 10*v[2]**2/3 - 31*v[2]*v[3]/3 + 11*v[2]*v[4]/3 + 25*v[3]**2/3 - 19*v[3]*v[4]/3 + 4*v[4]**2/3 +beta1_expr = 13*v[2]**2/3 - 13*v[2]*v[1]/3 - 13*v[2]*v[3]/3 + 4*v[1]**2/3 + 5*v[1]*v[3]/3 + 4*v[3]**2/3 +beta2_expr = 10*v[2]**2/3 + 11*v[2]*v[0]/3 - 31*v[2]*v[1]/3 + 4*v[0]**2/3 - 19*v[0]*v[1]/3 + 25*v[1]**2/3 + +# 将二次型转换为矩阵形式 +def quad_form_to_matrix(expr, var_list): + """返回二次型 expr 关于变量 var_list 的对称矩阵 A""" + n = len(var_list) + A = sp.zeros(n, n) + for i in range(n): + for j in range(i, n): + coeff = expr.coeff(var_list[i]*var_list[j]) + if i == j: + A[i,j] = coeff + else: + # 交叉项系数,二次型中 x_i x_j 系数对应矩阵 (i,j) 和 (j,i) 各一半 + A[i,j] = coeff/2 + A[j,i] = coeff/2 + return A + +# 对 beta0 +var0 = [v[2], v[3], v[4]] +A0 = quad_form_to_matrix(beta0_expr, var0) +print("A0 =", A0) + +# 对 beta1 +var1 = [v[1], v[2], v[3]] +A1 = quad_form_to_matrix(beta1_expr, var1) +print("\nA1 =", A1) + +# 对 beta2 +var2 = [v[0], v[1], v[2]] +A2 = quad_form_to_matrix(beta2_expr, var2) +print("\nA2 =", A2) + +# 方法1:使用正交对角化(特征值分解)得到有理数形式 +def sos_rational_from_matrix(A, vars): + """将半正定矩阵A分解为有理数形式的平方和""" + # 计算特征值和特征向量(符号计算) + eig_data = A.eigenvects() + + sos_terms = [] + for eigval, multiplicity, eigvecs in eig_data: + if eigval > 0: # 只取正特征值 + for vec in eigvecs: + # 将特征向量化为最简整数形式 + vec = sp.Matrix(vec) + # 找到最小公倍数使得所有系数为整数 + denoms = [sp.fraction(sp.simplify(c))[1] for c in vec if c != 0] + lcm_denom = 1 + for d in denoms: + if d != 1: + lcm_denom = sp.lcm(lcm_denom, d) + + # 乘以最小公倍数得到整数系数 + vec_int = vec * lcm_denom + # 提取系数的最大公约数 + coeffs = [vec_int[i] for i in range(vec_int.rows)] + gcd_coeff = abs(sp.gcd(coeffs)) + if gcd_coeff > 1: + vec_int = vec_int / gcd_coeff + + # 构造线性组合 + linear_expr = sum(vec_int[i] * vars[i] for i in range(len(vars))) + + # 计算系数 + # 验证:vec^T A vec = eigval * vec^T vec + # 但我们需要的是 eigval * (vec^T x)^2 / (vec^T vec) + vec_norm2 = sum(c**2 for c in vec) + coefficient = eigval / vec_norm2 * (lcm_denom / gcd_coeff)**2 + + # 简化系数为分数形式 + coefficient = sp.nsimplify(coefficient) + + sos_terms.append((coefficient, linear_expr)) + + return sos_terms + +# 方法2:使用完成平方的方法 +def complete_square(expr, vars): + """使用完成平方的方法得到SOS表示""" + # 这是一个启发式方法,尝试常见的线性组合模式 + # 对于WENO格式,通常的形式是 a*(p*v[i] + q*v[i+1] + r*v[i+2])^2 + b*(s*v[i] + t*v[i+1] + u*v[i+2])^2 + + # 对于beta0,我们知道形式应该是: + # c1*(v[i] - 2*v[i+1] + v[i+2])^2 + c2*(3*v[i] - 4*v[i+1] + v[i+2])^2 + # 让我们用符号求解系数 + + if vars == var0: # beta0 + # 尝试形式: c1*(a*v2 + b*v3 + c*v4)^2 + c2*(d*v2 + e*v3 + f*v4)^2 + c1, c2, a, b, c, d, e, f = sp.symbols('c1 c2 a b c d e f') + target = c1*(a*v[2] + b*v[3] + c*v[4])**2 + c2*(d*v[2] + e*v[3] + f*v[4])**2 + + # 展开并比较系数 + target_expanded = sp.expand(target) + + # 收集系数方程 + coeff_eqs = [] + # v2^2 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]**2), beta0_expr.coeff(v[2]**2))) + # v3^2 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[3]**2), beta0_expr.coeff(v[3]**2))) + # v4^2 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[4]**2), beta0_expr.coeff(v[4]**2))) + # v2*v3 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]*v[3]), beta0_expr.coeff(v[2]*v[3]))) + # v2*v4 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]*v[4]), beta0_expr.coeff(v[2]*v[4]))) + # v3*v4 系数 + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[3]*v[4]), beta0_expr.coeff(v[3]*v[4]))) + + # 添加归一化约束以减少自由度 + # 让第一个线性组合的系数为简单整数,比如 (1, -2, 1) + coeff_eqs.append(sp.Eq(a, 1)) + coeff_eqs.append(sp.Eq(b, -2)) + coeff_eqs.append(sp.Eq(c, 1)) + + # 求解 + sol = sp.nsolve(coeff_eqs, [c1, c2, a, b, c, d, e, f], [13/12, 1/4, 1, -2, 1, 3, -4, 1]) + + return sol + + elif vars == var1: # beta1 + # 形式: c1*(v1 - 2*v2 + v3)^2 + c2*(v1 - v3)^2 + c1, c2 = sp.symbols('c1 c2') + target = c1*(v[1] - 2*v[2] + v[3])**2 + c2*(v[1] - v[3])**2 + target_expanded = sp.expand(target) + + # 收集系数方程 + coeff_eqs = [] + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]**2), beta1_expr.coeff(v[1]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]**2), beta1_expr.coeff(v[2]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[3]**2), beta1_expr.coeff(v[3]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]*v[2]), beta1_expr.coeff(v[1]*v[2]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]*v[3]), beta1_expr.coeff(v[1]*v[3]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]*v[3]), beta1_expr.coeff(v[2]*v[3]))) + + # 求解 + sol = sp.solve(coeff_eqs, [c1, c2]) + return sol + + elif vars == var2: # beta2 + # 形式: c1*(v0 - 2*v1 + v2)^2 + c2*(v0 - 4*v1 + 3*v2)^2 + c1, c2 = sp.symbols('c1 c2') + target = c1*(v[0] - 2*v[1] + v[2])**2 + c2*(v[0] - 4*v[1] + 3*v[2])**2 + target_expanded = sp.expand(target) + + # 收集系数方程 + coeff_eqs = [] + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[0]**2), beta2_expr.coeff(v[0]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]**2), beta2_expr.coeff(v[1]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[2]**2), beta2_expr.coeff(v[2]**2))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[0]*v[1]), beta2_expr.coeff(v[0]*v[1]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[0]*v[2]), beta2_expr.coeff(v[0]*v[2]))) + coeff_eqs.append(sp.Eq(target_expanded.coeff(v[1]*v[2]), beta2_expr.coeff(v[1]*v[2]))) + + # 求解 + sol = sp.solve(coeff_eqs, [c1, c2]) + return sol + +print("\n=== 使用完成平方的方法 ===") +print("\n--- Beta0 ---") +sol0 = complete_square(beta0_expr, var0) +if sol0: + print("系数解:", sol0) + # 重构表达式 + if isinstance(sol0, list): + c1_val, c2_val = sol0[0][0], sol0[0][1] + beta0_reconstructed = c1_val*(v[2] - 2*v[3] + v[4])**2 + c2_val*(3*v[2] - 4*v[3] + v[4])**2 + print("重构的Beta0:", beta0_reconstructed) + print("是否等于原式:", sp.simplify(beta0_expr - beta0_reconstructed) == 0) + +print("\n--- Beta1 ---") +sol1 = complete_square(beta1_expr, var1) +if sol1: + print("系数解:", sol1) + # 重构表达式 + c1_val, c2_val = list(sol1.values())[0] + beta1_reconstructed = c1_val*(v[1] - 2*v[2] + v[3])**2 + c2_val*(v[1] - v[3])**2 + print("重构的Beta1:", beta1_reconstructed) + print("是否等于原式:", sp.simplify(beta1_expr - beta1_reconstructed) == 0) + +print("\n--- Beta2 ---") +sol2 = complete_square(beta2_expr, var2) +if sol2: + print("系数解:", sol2) + # 重构表达式 + c1_val, c2_val = list(sol2.values())[0] + beta2_reconstructed = c1_val*(v[0] - 2*v[1] + v[2])**2 + c2_val*(v[0] - 4*v[1] + 3*v[2])**2 + print("重构的Beta2:", beta2_reconstructed) + print("是否等于原式:", sp.simplify(beta2_expr - beta2_reconstructed) == 0) + +print("\n=== 总结 ===") +print("Beta0 = 13/12 * (v[i] - 2v[i+1] + v[i+2])^2 + 1/4 * (3v[i] - 4v[i+1] + v[i+2])^2") +print("Beta1 = 13/12 * (v[i-1] - 2v[i] + v[i+1])^2 + 1/4 * (v[i-1] - v[i+1])^2") +print("Beta2 = 13/12 * (v[i-2] - 2v[i-1] + v[i])^2 + 1/4 * (v[i-2] - 4v[i-1] + 3v[i])^2") + +# 验证 +print("\n=== 验证 ===") +beta0_target = sp.Rational(13,12)*(v[2] - 2*v[3] + v[4])**2 + sp.Rational(1,4)*(3*v[2] - 4*v[3] + v[4])**2 +print("Beta0 匹配:", sp.simplify(beta0_expr - beta0_target) == 0) + +beta1_target = sp.Rational(13,12)*(v[1] - 2*v[2] + v[3])**2 + sp.Rational(1,4)*(v[1] - v[3])**2 +print("Beta1 匹配:", sp.simplify(beta1_expr - beta1_target) == 0) + +beta2_target = sp.Rational(13,12)*(v[0] - 2*v[1] + v[2])**2 + sp.Rational(1,4)*(v[0] - 4*v[1] + 3*v[2])**2 +print("Beta2 匹配:", sp.simplify(beta2_expr - beta2_target) == 0) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/03c/testprj.py b/example/figure/1d/weno/some_help_code/03c/testprj.py new file mode 100644 index 00000000..c6785337 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/03c/testprj.py @@ -0,0 +1,105 @@ +from sympy import symbols, Rational, expand, simplify, nsimplify +from sympy.core.numbers import Float + +# 1. 定义符号变量 +v2, v3, v4 = symbols('v2 v3 v4', real=True) + +# 2. 配置参数(控制有理数简洁性) +DECIMAL_PRECISION = 8 # 保留8位小数(可调整,平衡精度和简洁性) +#MAX_DENOMINATOR = 1000000 # 有理数分母最大值(避免超大数字) +MAX_DENOMINATOR = 1000 + +# 3. 原始系数 +# 第一个表达式:0.255002 * (0.64295988*v2 + 0.11435482*v3 - 0.75731471*v4)^2(截断后) +coeff1_out = 0.255002 +coeff1_in = [0.642959882291173, 0.114354823786141, -0.757314706077309] + +# 第二个表达式:12.744998 * (-0.50325864*v2 + 0.80844891*v3 - 0.30519027*v4)^2(截断后) +coeff2_out = 12.744998 +coeff2_in = [-0.503258637711058, 0.808448910533936, -0.305190272822878] + +def float_to_simple_rational(num): + """ + 将浮点数转换为简洁的有理数: + 1. 截断浮点精度,消除噪声 + 2. 限制分母最大值,避免虚假大数字 + """ + # 步骤1:截断浮点数精度(保留DECIMAL_PRECISION位小数) + #truncated = round(num, DECIMAL_PRECISION) + truncated = num + # 步骤2:转换为有理数,限制分母最大值 + return Rational(truncated).limit_denominator(MAX_DENOMINATOR) + +def normalize_linear_coeffs_simple(coeffs): + """ + 优化版:将线性系数转为最小整数(基于简洁有理数) + """ + # 步骤1:转简洁有理数 + rat_coeffs = [float_to_simple_rational(c) for c in coeffs] + + # 步骤2:提取公分母,转为整数 + denoms = [c.denominator for c in rat_coeffs] + lcm_denom = 1 + for d in denoms: + lcm_denom = lcm_denom * d // gcd(lcm_denom, d) + int_coeffs = [c * lcm_denom for c in rat_coeffs] + + # 步骤3:提取最大公约数,化为最小整数 + abs_ints = [abs(int(c)) for c in int_coeffs if c != 0] + gcd_int = abs_ints[0] if abs_ints else 1 + for num in abs_ints[1:]: + gcd_int = gcd(gcd_int, num) + + # 最小整数系数 + 提取的公因子 + min_coeffs = [int(c / gcd_int) for c in int_coeffs] # 确保是整数类型 + factor_out = Rational(gcd_int, lcm_denom) + + return min_coeffs, factor_out + +# 辅助函数:GCD和LCM +def gcd(a, b): + while b: + a, b = b, a % b + return a + +# ------------------------ 处理第一个表达式 ------------------------ +min_coeffs1, factor1 = normalize_linear_coeffs_simple(coeff1_in) +rat_out1 = float_to_simple_rational(coeff1_out) +linear1 = min_coeffs1[0]*v2 + min_coeffs1[1]*v3 + min_coeffs1[2]*v4 +expr1 = rat_out1 * (factor1 **2) * (linear1** 2) +expr1_simplified = simplify(expr1) + +# ------------------------ 处理第二个表达式 ------------------------ +min_coeffs2, factor2 = normalize_linear_coeffs_simple(coeff2_in) +rat_out2 = float_to_simple_rational(coeff2_out) +linear2 = min_coeffs2[0]*v2 + min_coeffs2[1]*v3 + min_coeffs2[2]*v4 +expr2 = rat_out2 * (factor2 **2) * (linear2** 2) +expr2_simplified = simplify(expr2) + +# ------------------------ 输出结果 ------------------------ +print(f"配置:保留{DECIMAL_PRECISION}位小数,分母最大为{MAX_DENOMINATOR}") +print("\n=== 第一个表达式处理结果 ===") +print(f"平方内最小整数线性组合:{linear1}") +print(f"平方外最简有理系数:{expr1_simplified.coeff(linear1**2)}") +print(f"完整等价表达式:{expr1_simplified}") + +print("\n=== 第二个表达式处理结果 ===") +print(f"平方内最小整数线性组合:{linear2}") +print(f"平方外最简有理系数:{expr2_simplified.coeff(linear2**2)}") +print(f"完整等价表达式:{expr2_simplified}") + +# ------------------------ 等价性验证 ------------------------ +def verify_equivalence(expr_sym, coeff_out, coeff_in, v_vals=[1,1,1]): + """验证符号表达式与原始浮点表达式的数值等价性""" + # 原始浮点值 + linear_float = sum(c*v for c, v in zip(coeff_in, v_vals)) + float_val = coeff_out * (linear_float **2) + # 符号表达式值 + sym_val = expr_sym.subs({v2:v_vals[0], v3:v_vals[1], v4:v_vals[2]}).evalf() + # 误差阈值(根据截断精度调整) + error = abs(float_val - sym_val) + return error < 10**(-DECIMAL_PRECISION) + +print("\n=== 等价性验证 ===") +print(f"第一个表达式(v2=1,v3=1,v4=1):误差={verify_equivalence(expr1_simplified, coeff1_out, coeff1_in, [1,1,1])}") +print(f"第二个表达式(v2=1,v3=1,v4=1):误差={verify_equivalence(expr2_simplified, coeff2_out, coeff2_in, [1,1,1])}") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04/testprj.py b/example/figure/1d/weno/some_help_code/04/testprj.py new file mode 100644 index 00000000..fcc083dd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04/testprj.py @@ -0,0 +1,41 @@ +from sympy import symbols, Rational, simplify, pprint, expand + +# 定义符号(v0 = v(i), v1 = v(i+1), v2 = v(i+2)) +v0, v1, v2 = symbols('v0 v1 v2') + +# beta0 的展开形式 +beta0 = (Rational(10,3)*v0**2 - Rational(31,3)*v0*v1 + Rational(11,3)*v0*v2 + + Rational(25,3)*v1**2 - Rational(19,3)*v1*v2 + Rational(4,3)*v2**2) + +def complete_the_square(poly, var): + """ + 对多项式poly相对于变量var进行配方。 + 返回:(平方项, 剩余多项式) + """ + # 提取系数:A (var^2), D (var^1), C (常数项) + A = poly.coeff(var, 2) + D = poly.coeff(var, 1) + C = poly - A * var**2 - D * var + + if A == 0: + return 0, poly # 无二次项 + + # 移位 delta = -D / (2 A) + delta = -D / (2 * A) + # 平方项 + square_part = A * (var + delta)**2 + # 剩余 + remaining = C - (D**2) / (4 * A) + + return simplify(square_part), simplify(remaining) + +# 应用:选择 var = v1 (v(i+1),系数最大) +var = v1 +square_part, remaining = complete_the_square(beta0, var) + +print("beta0 的 SOS 分解:") +print("平方项 1:") +pprint(square_part) +print("\n剩余(平方项 2):") +pprint(remaining) +print("\n完整形式:beta0 = 平方项1 + 剩余") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04a/testprj.py b/example/figure/1d/weno/some_help_code/04a/testprj.py new file mode 100644 index 00000000..2f34fe9c --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04a/testprj.py @@ -0,0 +1,58 @@ +from sympy import symbols, Rational, simplify, expand, latex, factor + +# 定义符号(v0 = v(i), v1 = v(i+1), v2 = v(i+2)) +v0, v1, v2 = symbols('v0 v1 v2') + +# beta0 的展开形式 +beta0 = (Rational(10,3)*v0**2 - Rational(31,3)*v0*v1 + Rational(11,3)*v0*v2 + + Rational(25,3)*v1**2 - Rational(19,3)*v1*v2 + Rational(4,3)*v2**2) + +def complete_the_square(poly, var): + """ + 对多项式 poly 相对于变量 var 进行配方。 + 返回:(平方项, 剩余多项式) + """ + A = poly.coeff(var, 2) + D = poly.coeff(var, 1) + C = poly - A * var**2 - D * var + + if A == 0: + return 0, poly + + # delta = D / (2 * A) + delta = D / (2 * A) + square_part = A * (var + delta)**2 + remaining = C - (D**2) / (4 * A) + + return simplify(square_part), simplify(remaining) + +# 应用:选择 var = v1 (系数最大) +var = v1 +square_part, remaining = complete_the_square(beta0, var) + +# 简化剩余为平方形式 +remaining_squared = factor(remaining) + +# 修正:SOS 使用简化平方 +sos = square_part + remaining_squared + +# 验证 +expanded_sos = expand(sos) +difference = simplify(beta0 - expanded_sos) +print("验证:展开 SOS 与原始差值 =", difference) # 应为 0 + +# LaTeX 输出 +print("\n原始公式 LaTeX:") +print(latex(beta0)) +print("\n平方项 LaTeX:") +print(latex(square_part)) +print("\n剩余项 LaTeX (简化平方):") +print(latex(remaining_squared)) +print("\n完整 SOS LaTeX:") +print(latex(sos)) + +# 目标形式验证 +target = Rational(13,12) * (v0 - 2*v1 + v2)**2 + Rational(1,4) * (3*v0 - 4*v1 + v2)**2 +target_expanded = expand(target) +target_diff = simplify(beta0 - target_expanded) +print("\n目标形式与原始差值 =", target_diff) # 应为 0 \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04b/testprj.py b/example/figure/1d/weno/some_help_code/04b/testprj.py new file mode 100644 index 00000000..7730abbd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04b/testprj.py @@ -0,0 +1,97 @@ +from sympy import Rational +from itertools import product +from math import gcd as math_gcd # 更快整数 gcd + +# beta0 的对称矩阵(二次型 A,off-diag 是跨项系数的一半) +A_beta = [ + [Rational(10,3), Rational(-31,6), Rational(11,6)], + [Rational(-31,6), Rational(25,3), Rational(-19,6)], + [Rational(11,6), Rational(-19,6), Rational(4,3)] +] + +def gcd_list(lst): + """计算列表整数的最大公约数(处理 0)。""" + g = 0 + for x in lst: + g = math_gcd(g, abs(x)) + return g + +def normalize_vector(vec): + """标准化向量:gcd=1,第一非零系数正。""" + g = gcd_list(vec) + if g == 0: + return tuple(vec) + vec = tuple(int(x) // g for x in vec) + # 翻转符号使第一非零正 + for i in range(len(vec)): + if vec[i] != 0: + if vec[i] < 0: + vec = tuple(-int(x) for x in vec) + break + return vec + +def check_sos_2_terms(l1, l2, A): + """检查是否为有效 2 平方 SOS:解 lambda 并验证所有系数。""" + a, b, c = l1 + d, e, f = l2 + p = a**2 + q = d**2 + r = b**2 + s = e**2 + det = p * s - q * r + if det == 0: + return None + # Cramer 法则解 lambda1, lambda2 (基于 v0² 和 v1² 系数) + lambda1 = (A[0][0] * s - A[1][1] * q) / det + lambda2 = (A[1][1] * p - A[0][0] * r) / det + # 预测所有系数 + pred_00 = lambda1 * p + lambda2 * q + pred_11 = lambda1 * r + lambda2 * s + pred_22 = lambda1 * c**2 + lambda2 * f**2 + pred_01 = lambda1 * a * b + lambda2 * d * e + pred_02 = lambda1 * a * c + lambda2 * d * f + pred_12 = lambda1 * b * c + lambda2 * e * f + # 验证(对称矩阵,只查上三角) + if (pred_00 == A[0][0] and pred_11 == A[1][1] and pred_22 == A[2][2] and + pred_01 == A[0][1] and pred_02 == A[0][2] and pred_12 == A[1][2] and + lambda1 > 0 and lambda2 > 0): + return (l1, lambda1, l2, lambda2) + return None + +def find_all_sos_2(A, max_c=4): # 默认 max_c=4 以快速找到目标形式 + """枚举所有 2 平方 SOS,小系数版本。""" + print(f"Starting search with max_c={max_c}... Total iterations: {(2*max_c + 1)**6}") + sos_list = [] + processed = set() # 去重已标准化对 + total = (2 * max_c + 1) ** 6 + count = 0 + for coeffs in product(range(-max_c, max_c + 1), repeat=6): + count += 1 + if count % 50000 == 0: # 每 5 万迭代打印进度 + print(f"Progress: {count}/{total} ({count / total * 100:.1f}%) | Processed pairs: {len(processed)}") + l1_raw = coeffs[0:3] + l2_raw = coeffs[3:6] + if all(x == 0 for x in l1_raw) or all(x == 0 for x in l2_raw): # 跳过零向量 + continue + l1 = normalize_vector(l1_raw) + l2 = normalize_vector(l2_raw) + pair = tuple(sorted([l1, l2])) # 排序避免 (l1,l2) 和 (l2,l1) 重复 + if pair in processed: + continue + processed.add(pair) + res = check_sos_2_terms(l1, l2, A) + if res: + sos_list.append(res) + print(f"Found one: {res}") # 立即打印发现 + print(f"Search complete. Found {len(sos_list)} representations.") + return sos_list + +# 运行 2 平方搜索(从小 max_c 开始) +if __name__ == "__main__": + sos_2 = find_all_sos_2(A_beta, max_c=4) # 改成 6 如果想找更多 + print("\n=== 找到的 2 平方 SOS 表示(3 项线性形式 + 2 平方) ===") + for i, (l1, lam1, l2, lam2) in enumerate(sos_2, 1): + print(f"{i}. \\beta_0 = {lam1} ({l1[0]} v_0 + {l1[1]} v_1 + {l1[2]} v_2)^2 + {lam2} ({l2[0]} v_0 + {l2[1]} v_1 + {l2[2]} v_2)^2") + + print("\n注: 3 平方 SOS 可能无小系数严格正 lambda 表示(因秩 2),可通过添加第三个 0 lambda 退化到 2 平方。") + print("若需 3 平方扩展,增大 max_c 但计算量 O(max_c^9),建议 max_c=2。") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04c/testprj.py b/example/figure/1d/weno/some_help_code/04c/testprj.py new file mode 100644 index 00000000..798282fd --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04c/testprj.py @@ -0,0 +1,108 @@ +from sympy import Rational +from itertools import product +from math import gcd as math_gcd + +# beta0 的对称矩阵(二次型 A,off-diag 是跨项系数的一半) +A_beta = [ + [Rational(10,3), Rational(-31,6), Rational(11,6)], + [Rational(-31,6), Rational(25,3), Rational(-19,6)], + [Rational(11,6), Rational(-19,6), Rational(4,3)] +] + +def gcd_list(lst): + """计算列表整数的最大公约数(处理 0)。""" + g = 0 + for x in lst: + g = math_gcd(g, abs(x)) + return g + +def normalize_vector(vec): + """标准化向量:gcd=1,第一非零系数正(忽略零系数)。""" + non_zero = [x for x in vec if x != 0] + if not non_zero: + return tuple(vec) + g = gcd_list(non_zero) + vec_norm = tuple(int(x) // g if x != 0 else 0 for x in vec) + # 翻转符号使第一非零正 + for i in range(len(vec_norm)): + if vec_norm[i] != 0: + if vec_norm[i] < 0: + vec_norm = tuple(-int(x) if x != 0 else 0 for x in vec_norm) + break + return vec_norm + +def check_sos_2_terms(l1, l2, A): + """检查是否为有效 2 平方 SOS:解 lambda 并验证所有系数。""" + a, b, c = l1 + d, e, f = l2 + p = a**2 + q = d**2 + r = b**2 + s = e**2 + det = p * s - q * r + if det == 0: + return None + # Cramer 法则解 lambda1, lambda2 (基于 v0² 和 v1² 系数) + lambda1 = (A[0][0] * s - A[1][1] * q) / det + lambda2 = (A[1][1] * p - A[0][0] * r) / det + # 预测剩余系数 + pred_22 = lambda1 * c**2 + lambda2 * f**2 + pred_01 = lambda1 * a * b + lambda2 * d * e + pred_02 = lambda1 * a * c + lambda2 * d * f + pred_12 = lambda1 * b * c + lambda2 * e * f + # 验证(对称矩阵,只查上三角) + if (pred_22 == A[2][2] and pred_01 == A[0][1] and pred_02 == A[0][2] and pred_12 == A[1][2] and + lambda1 > 0 and lambda2 > 0): + return (l1, lambda1, l2, lambda2) + return None + +def find_all_sos_2(A, max_c=4): + """枚举所有 2 平方 SOS(包括稀疏支持集,如 3+2, 2+2)。""" + print(f"Starting search with max_c={max_c}... Total iterations: {(2*max_c + 1)**6}") + sos_list = [] + processed = set() # 去重已标准化对 + total = (2 * max_c + 1) ** 6 + count = 0 + for coeffs in product(range(-max_c, max_c + 1), repeat=6): + count += 1 + if count % 50000 == 0: # 每 5 万迭代打印进度 + print(f"Progress: {count}/{total} ({count / total * 100:.1f}%) | Processed pairs: {len(processed)}") + l1_raw = coeffs[0:3] + l2_raw = coeffs[3:6] + if all(x == 0 for x in l1_raw) or all(x == 0 for x in l2_raw): # 跳过零向量 + continue + l1 = normalize_vector(l1_raw) + l2 = normalize_vector(l2_raw) + pair = tuple(sorted([l1, l2])) # 排序避免 (l1,l2) 和 (l2,l1) 重复 + if pair in processed: + continue + processed.add(pair) + res = check_sos_2_terms(l1, l2, A) + if res: + sos_list.append(res) + print(f"Found one: {res}") # 立即打印发现 + print(f"Search complete. Found {len(sos_list)} representations.") + return sos_list + +def classify_supports(sos_list): + """分类找到的形式按支持集大小 (e.g., 3+3, 3+2)。""" + from collections import defaultdict + groups = defaultdict(list) + for l1, lam1, l2, lam2 in sos_list: + supp1 = sum(1 for x in l1 if x != 0) + supp2 = sum(1 for x in l2 if x != 0) + key = f"{max(supp1, supp2)}+{min(supp1, supp2)}" # 排序如 3+2 + groups[key].append((l1, lam1, l2, lam2)) + return groups + +# 运行搜索与分类 +if __name__ == "__main__": + sos_2 = find_all_sos_2(A_beta, max_c=4) + print("\n=== 所有找到的 2 平方 SOS 表示(按支持集分类) ===") + groups = classify_supports(sos_2) + for combo, forms in sorted(groups.items()): + print(f"\n{combo} 组合 ({len(forms)} 个):") + for i, (l1, lam1, l2, lam2) in enumerate(forms, 1): + print(f" {i}. \\beta_0 = {lam1} ({l1[0]} v_0 + {l1[1]} v_1 + {l1[2]} v_2)^2 + {lam2} ({l2[0]} v_0 + {l2[1]} v_1 + {l2[2]} v_2)^2") + + print("\n注: 无找到 3+2 或 2+2(max_c=4 下);增大 max_c=6 可能发现更多稀疏形式,但计算 ~10x 更长。") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/04d/testprj.py b/example/figure/1d/weno/some_help_code/04d/testprj.py new file mode 100644 index 00000000..2a5113d6 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/04d/testprj.py @@ -0,0 +1,119 @@ +from sympy import Rational +from itertools import product +from math import gcd as math_gcd +from collections import defaultdict # 添加以防 + +# 修正矩阵(off-diagonal 非 0) +A_beta0 = [ + [Rational(10,3), Rational(-31,6), Rational(11,6)], + [Rational(-31,6), Rational(25,3), Rational(-19,6)], + [Rational(11,6), Rational(-19,6), Rational(4,3)] +] + +A_beta1 = [ + [Rational(4,3), Rational(-13,6), Rational(5,6)], + [Rational(-13,6), Rational(13,3), Rational(-13,6)], + [Rational(5,6), Rational(-13,6), Rational(4,3)] +] + +A_beta2 = [ + [Rational(4,3), Rational(-19,6), Rational(11,6)], + [Rational(-19,6), Rational(25,3), Rational(-31,6)], + [Rational(11,6), Rational(-31,6), Rational(10,3)] +] + +ALL_BETAS = {'beta0': A_beta0, 'beta1': A_beta1, 'beta2': A_beta2} + +def gcd_list(lst): + g = 0 + for x in lst: + g = math_gcd(g, abs(x)) + return g + +def normalize_vector(vec): + non_zero = [x for x in vec if x != 0] + if not non_zero: + return tuple(vec) + g = gcd_list(non_zero) + vec_norm = tuple(int(x) // g if x != 0 else 0 for x in vec) + for i in range(len(vec_norm)): + if vec_norm[i] != 0: + if vec_norm[i] < 0: + vec_norm = tuple(-int(x) if x != 0 else 0 for x in vec_norm) + break + return vec_norm + +def check_sos_2_terms(l1, l2, A): + a, b, c = l1 + d, e, f = l2 + p = a**2 + q = d**2 + r = b**2 + s = e**2 + det = p * s - q * r + if det == 0: + return None + lambda1 = (A[0][0] * s - A[1][1] * q) / det + lambda2 = (A[1][1] * p - A[0][0] * r) / det + pred_22 = lambda1 * c**2 + lambda2 * f**2 + pred_01 = lambda1 * a * b + lambda2 * d * e + pred_02 = lambda1 * a * c + lambda2 * d * f + pred_12 = lambda1 * b * c + lambda2 * e * f + if (pred_22 == A[2][2] and pred_01 == A[0][1] and pred_02 == A[0][2] and pred_12 == A[1][2] and + lambda1 > 0 and lambda2 > 0): + return (l1, lambda1, l2, lambda2) + return None + +def find_all_sos_2(A, max_c=4, name='beta'): + print(f"\n--- Searching for {name} with max_c={max_c}... ---") + print(f"Total iterations: {(2*max_c + 1)**6}") + sos_list = [] + processed = set() + total = (2 * max_c + 1) ** 6 + count = 0 + for coeffs in product(range(-max_c, max_c + 1), repeat=6): + count += 1 + if count % 50000 == 0: + print(f"Progress: {count}/{total} ({count / total * 100:.1f}%) | Processed pairs: {len(processed)}") + l1_raw = coeffs[0:3] + l2_raw = coeffs[3:6] + if all(x == 0 for x in l1_raw) or all(x == 0 for x in l2_raw): + continue + l1 = normalize_vector(l1_raw) + l2 = normalize_vector(l2_raw) + pair = tuple(sorted([l1, l2])) + if pair in processed: + continue + processed.add(pair) + res = check_sos_2_terms(l1, l2, A) + if res: + sos_list.append(res) + print(f"Found one for {name}: {res}") + print(f"Search for {name} complete. Found {len(sos_list)} representations.") + return sos_list + +def classify_supports(sos_list): + groups = defaultdict(list) + for l1, lam1, l2, lam2 in sos_list: + supp1 = sum(1 for x in l1 if x != 0) + supp2 = sum(1 for x in l2 if x != 0) + key = f"{max(supp1, supp2)}+{min(supp1, supp2)}" + groups[key].append((l1, lam1, l2, lam2)) + return groups + +if __name__ == "__main__": + max_c = 4 # 可增大到 6 + all_results = {} + for name, A in ALL_BETAS.items(): + sos = find_all_sos_2(A, max_c, name) + all_results[name] = classify_supports(sos) + + print("\n=== 所有 β 的 SOS 表示(按支持集分类) ===") + for name, groups in all_results.items(): + print(f"\n{name.upper()}:") + for combo, forms in sorted(groups.items()): + print(f" {combo} 组合 ({len(forms)} 个):") + for i, (l1, lam1, l2, lam2) in enumerate(forms, 1): + print(f" {i}. \\beta_{name} = {lam1} ({l1[0]} v_0 + {l1[1]} v_1 + {l1[2]} v_2)^2 + {lam2} ({l2[0]} v_0 + {l2[1]} v_1 + {l2[2]} v_2)^2") + + print(f"\n注: max_c={max_c} 下结果;若无稀疏形式,增大 max_c。总运行时间 ~2min。") \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/05/testprj.py b/example/figure/1d/weno/some_help_code/05/testprj.py new file mode 100644 index 00000000..0fb19a71 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/05/testprj.py @@ -0,0 +1,106 @@ +import sympy as sp + +# 定义符号 +i = sp.symbols('i', integer=True) +v = sp.Function('v') +# 定义四个连续点的函数值 +v_i = v(i) +v_ip1 = v(i + 1) +v_ip2 = v(i + 2) +v_ip3 = v(i + 3) + +# 定义展开式(按照给定的系数) +beta0_expanded = ( + sp.Rational(2107, 240) * v_i**2 + - sp.Rational(1567, 40) * v_i * v_ip1 + + sp.Rational(3521, 120) * v_i * v_ip2 + - sp.Rational(309, 40) * v_i * v_ip3 + + sp.Rational(11003, 240) * v_ip1**2 + - sp.Rational(8623, 120) * v_ip1 * v_ip2 + + sp.Rational(2321, 120) * v_ip1 * v_ip3 + + sp.Rational(7043, 240) * v_ip2**2 + - sp.Rational(647, 40) * v_ip2 * v_ip3 + + sp.Rational(547, 240) * v_ip3**2 +) + +# 变量列表(为了方便提取系数) +vars_list = [v_i, v_ip1, v_ip2, v_ip3] + +def complete_the_square(poly, var, vars_list): + """ + 对多项式 poly 相对于变量 var 进行配方。 + 返回:(平方项, 剩余多项式) + """ + # 提取 A (var^2 系数), D (var^1 系数), C (无 var 项) + A = poly.coeff(var, 2) + D = poly.coeff(var, 1) + C = poly.as_poly(vars_list).as_expr() - A * var**2 - D * var # 剩余部分(简化版) + + if A == 0: + return 0, poly + + # delta = D / (2 * A) # 注意:标准配方是 -D/(2A),但根据二次形式 A x^2 + B x + C,delta = -B/(2A) + # 修正:D 是 B,这里 delta = -D / (2*A) + delta = -D / (2 * A) + square_part = A * (var + delta)**2 + remaining = C - D**2 / (4 * A) + + return sp.simplify(square_part), sp.simplify(remaining) + +def successive_completion(poly, vars_list, max_steps=10): + """ + 逐次配方法:反复配方直到剩余为常数或简单形式。 + 选择当前二次系数绝对值最大的变量配方。 + 返回:SOS 列表(平方项们) + """ + current_poly = poly + sos_terms = [] + steps = 0 + + while steps < max_steps: + # 找当前二次系数最大的变量 + max_coeff = 0 + best_var = None + for var in vars_list: + coeff = current_poly.coeff(var, 2) + if abs(coeff) > max_coeff: + max_coeff = abs(coeff) + best_var = var + + if max_coeff == 0: + # 无二次项,剩余为线性/常数(理想 SOS 为 0) + break + + # 配方 + square_part, remaining = complete_the_square(current_poly, best_var, vars_list) + sos_terms.append(square_part) + current_poly = remaining + steps += 1 + print(f"Step {steps}: Distributed {best_var}, square: {square_part}, remaining degree: {sp.degree(current_poly)}") + + if current_poly != 0: + sos_terms.append(current_poly) # 剩余作为最后一项(可能需进一步因子) + + return sos_terms + +# 运行逐次配方法 +print("Original beta0_expanded:") +sp.pprint(beta0_expanded) +print("\nDegree:", sp.degree(beta0_expanded)) + +sos_terms = successive_completion(beta0_expanded, vars_list) + +print("\n=== SOS 分解(逐次配方) ===") +sos = sum(sos_terms) +sp.pprint(sos) +print("\n完整 SOS 形式:") +sp.pprint(sp.expand(sos)) + +# 验证:展开 SOS 与原始差值 +difference = sp.simplify(sp.expand(sos) - beta0_expanded) +print("\n验证差值(应为 0):", difference) + +# 如果剩余复杂,可尝试因子化或手动调整 +for term in sos_terms: + print("\nTerm:") + sp.pprint(sp.factor(term)) \ No newline at end of file diff --git a/example/figure/1d/weno/some_help_code/05a/testprj.py b/example/figure/1d/weno/some_help_code/05a/testprj.py new file mode 100644 index 00000000..4ff8ebf8 --- /dev/null +++ b/example/figure/1d/weno/some_help_code/05a/testprj.py @@ -0,0 +1,76 @@ +import sympy as sp + +# 作为符号处理(避免 Function 问题) +v_i, v_ip1, v_ip2, v_ip3 = sp.symbols('v_i v_{i+1} v_{i+2} v_{i+3}') + +beta0_expanded = ( + sp.Rational(2107, 240) * v_i**2 - sp.Rational(1567, 40) * v_i * v_ip1 + sp.Rational(3521, 120) * v_i * v_ip2 - sp.Rational(309, 40) * v_i * v_ip3 + + sp.Rational(11003, 240) * v_ip1**2 - sp.Rational(8623, 120) * v_ip1 * v_ip2 + sp.Rational(2321, 120) * v_ip1 * v_ip3 + + sp.Rational(7043, 240) * v_ip2**2 - sp.Rational(647, 40) * v_ip2 * v_ip3 + sp.Rational(547, 240) * v_ip3**2 +) + +def complete_the_square(poly, var): + poly_exp = sp.expand(poly) + A = poly_exp.coeff(var, 2) + B = poly_exp.coeff(var, 1) + C = poly_exp - A * var**2 - B * var + + if A == 0: + return 0, poly + + delta = -B / (2 * A) + square_part = A * (var + delta)**2 + remaining = C - B**2 / (4 * A) + + return sp.simplify(square_part), sp.simplify(remaining) + +def successive_completion(poly, order): + current_poly = poly + sos_terms = [] + + for var in order: + A = sp.expand(current_poly).coeff(var, 2) + if A == 0: + break + square_part, remaining = complete_the_square(current_poly, var) + sos_terms.append(square_part) + current_poly = remaining + + if current_poly != 0: + sos_terms.append(current_poly) + + return sos_terms + +# 尝试顺序直到 diff = 0 +orders = [ + [v_ip1, v_ip2, v_i, v_ip3], + [v_ip2, v_ip1, v_i, v_ip3], + [v_ip2, v_ip1, v_ip3, v_i], + [v_ip1, v_ip3, v_ip2, v_i] +] + +best_sos = None +for idx, order in enumerate(orders, 1): + print(f"--- Order {idx}: {order} ---") + sos_terms = successive_completion(beta0_expanded, order) + sos = sum(sos_terms) + diff = sp.simplify(sp.expand(sos) - beta0_expanded) + print("Diff:", diff) + if diff == 0: + best_sos = sos + print("Success! LaTeX SOS:") + print(sp.latex(best_sos)) + break + +if best_sos is None: + print("No exact SOS found with these orders. Try larger steps or SDP.") + +# PSD 检查 +A = sp.Matrix([ + [sp.Rational(2107,240), sp.Rational(-1567,80), sp.Rational(3521,240), sp.Rational(-309,80)], + [sp.Rational(-1567,80), sp.Rational(11003,240), sp.Rational(-8623,240), sp.Rational(2321,240)], + [sp.Rational(3521,240), sp.Rational(-8623,240), sp.Rational(7043,240), sp.Rational(-647,80)], + [sp.Rational(-309,80), sp.Rational(2321,240), sp.Rational(-647,80), sp.Rational(547,240)] +]) +eigs_num = [float(e) for e in A.applyfunc(sp.N).eigenvals().values()] +print("Numerical eigenvalues (all >=0 for PSD):", eigs_num) \ No newline at end of file diff --git a/example/figure/1d/weno/wenoinfo/01/wenoinfo.py b/example/figure/1d/weno/wenoinfo/01/wenoinfo.py new file mode 100644 index 00000000..2f4a50cd --- /dev/null +++ b/example/figure/1d/weno/wenoinfo/01/wenoinfo.py @@ -0,0 +1,1016 @@ +from fractions import Fraction +from collections import Counter, defaultdict +from typing import List, Tuple, Dict +import numpy as np +import math +from math import gcd +from functools import reduce + +# 类型别名 +Term = Tuple[int, List[int]] # (系数, 符号下标列表) +Expression = List[Term] # Term 的列表 +Polynomial = Dict[int, Expression] # {指数: 表达式} + +def print_matrix_fraction(matrix, is_column_vector=False): + """ + 支持一维向量和二维矩阵的分数字符串打印 + :param matrix: 一维列表(向量)或二维列表/数组(矩阵) + :param is_column_vector: 一维向量是否按列向量格式打印(默认False:行向量) + """ + # 步骤1:统一转换为二维矩阵格式 + if isinstance(matrix, (list, np.ndarray)): + # 若为一维,转为二维(行向量:1×N 或 列向量:N×1) + if np.ndim(matrix) == 1: + if is_column_vector: + # 列向量:N行1列 + two_d_matrix = [[x] for x in matrix] + else: + # 行向量:1行N列 + two_d_matrix = [matrix] + else: + # 若为二维,直接使用 + two_d_matrix = matrix + else: + raise TypeError("输入必须是列表或numpy数组") + + # 步骤2:转换为Fraction数组 + fraction_matrix = np.array([[Fraction(x).limit_denominator() for x in row] for row in two_d_matrix]) + rows = len(fraction_matrix) + cols = len(fraction_matrix[0]) + + # 步骤3:转换为字符串矩阵并计算每列最大宽度 + str_matrix = [] + col_widths = [0] * cols # 每列的最大宽度 + for row in fraction_matrix: + str_row = [] + for j, f in enumerate(row): + s = f"{f.numerator}/{f.denominator}" + str_row.append(s) + current_length = len(s) + if current_length > col_widths[j]: + col_widths[j] = current_length + str_matrix.append(str_row) + + # 步骤4:打印(保持原有对齐风格) + for i in range(rows): + row_elements = [] + for j in range(cols): + element = str_matrix[i][j] + formatted_element = f"{element:>{col_widths[j]}}" # 右对齐 + if j < cols - 1: + formatted_element += ", " + else: + formatted_element += " " + row_elements.append(formatted_element) + formatted_row = "".join(row_elements) + print(f"[ {formatted_row}]") + print() + +def extract_max_common_factor(numbers, max_denominator=1000000): + """提取最大公共因子,并优化符号""" + + def _to_python_number(x): + if isinstance(x, (np.integer, np.floating)): + return x.item() + return x + + def _smart_fraction(x): + val = _to_python_number(x) + return Fraction(val).limit_denominator(max_denominator) if isinstance(val, float) else Fraction(val) + + # 1. 转换并计算绝对值因子(始终为正) + fractions = [_smart_fraction(x) for x in numbers] + if not fractions: + return Fraction(1, 1), [] + if all(f == 0 for f in fractions): + return Fraction(1, 1), [0] * len(fractions) + + numerators = [f.numerator for f in fractions] + denominators = [f.denominator for f in fractions] + + numerator_gcd = reduce(gcd, numerators) + denominator_lcm = reduce(lambda a, b: abs(a * b) // gcd(a, b) if a and b else 0, denominators) + + abs_factor = Fraction(numerator_gcd, denominator_lcm) # 正值因子 + + # 2. 符号优化:测试正负两种提取方式 + simplified_pos = [f / abs_factor for f in fractions] + simplified_neg = [f / (-abs_factor) for f in fractions] + + # 统计正数个数 + pos_count_pos = sum(1 for f in simplified_pos if f > 0) + pos_count_neg = sum(1 for f in simplified_neg if f > 0) + + # 3. 决策:选择使正数更多的因子 + if pos_count_neg > pos_count_pos: + factor, simplified = -abs_factor, simplified_neg + elif pos_count_neg < pos_count_pos: + factor, simplified = abs_factor, simplified_pos + else: # 平局处理 + # 两项时优先第一项为正 + target_idx = 0 if len(numbers) == 2 else 0 + if simplified_pos[target_idx] > 0: + factor, simplified = abs_factor, simplified_pos + else: + factor, simplified = -abs_factor, simplified_neg + + # 4. 转换并确保互质 + simplified_integers = [sf.numerator for sf in simplified] + final_gcd = reduce(gcd, simplified_integers) + if final_gcd != 1: + factor *= final_gcd + simplified_integers = [x // final_gcd for x in simplified_integers] + + return factor, simplified_integers + +def term_multiply(t1: Term, t2: Term) -> Term: + """ + 两个 Term 相乘 + 示例: (2, [1]) * (3, [2]) = (6, [1, 2]) + """ + coeff1, symbols1 = t1 + coeff2, symbols2 = t2 + # 合并符号列表并排序 + new_symbols = sorted(symbols1 + symbols2) + return (coeff1 * coeff2, new_symbols) + +def expression_add(expr1: Expression, expr2: Expression) -> Expression: + """ + 两个 Expression 相加,合并同类项 + 示例: [(2,[1])] + [(3,[2]), (2,[1])] = [(4,[1]), (3,[2])] + """ + # 用字典合并:键是符号元组,值是系数和 + term_dict = defaultdict(int) + + for coeff, symbols in expr1 + expr2: + key = tuple(symbols) + term_dict[key] += coeff + + # 过滤系数为0的项,转换回列表 + result = [(coeff, list(symbols)) for symbols, coeff in term_dict.items() if coeff != 0] + return result + +def polynomial_square(polynomial: Polynomial) -> Polynomial: + """ + 多项式平方展开 + 算法: 遍历所有指数对 (i, j),对应项相乘后指数相加 + """ + result: Polynomial = defaultdict(list) + exps = sorted(polynomial.keys()) + + # 1. 平方项 (i == j) + for exp in exps: + expr = polynomial[exp] + new_exp = exp * 2 + + # 表达式与自身相乘 + for i, term1 in enumerate(expr): + # 平方项 + squared_term = term_multiply(term1, term1) + result[new_exp].append(squared_term) + + # 交叉项 (2ab) + for term2 in expr[i+1:]: + cross_term = term_multiply(term1, term2) + # 系数乘以2 + double_cross = (2 * cross_term[0], cross_term[1]) + result[new_exp].append(double_cross) + + # 2. 交叉项 (i != j) + for i in range(len(exps)): + for j in range(i + 1, len(exps)): + exp_i, exp_j = exps[i], exps[j] + new_exp = exp_i + exp_j + + for term1 in polynomial[exp_i]: + for term2 in polynomial[exp_j]: + product = term_multiply(term1, term2) + # 乘以2 + result[new_exp].append((2 * product[0], product[1])) + + # 合并同类项 + final_result = {} + for exp, expr in result.items(): + final_result[exp] = expression_add(expr, []) + + return final_result + +def integrate_polynomial(polynomial: Polynomial) -> Polynomial: + """ + 对多项式进行积分: ∫ x^k dx = x^(k+1)/(k+1) + 符号部分保持不变,数值系数除以 (k+1) + """ + integrated: Polynomial = {} + + for exp, expr in polynomial.items(): + new_exp = exp + 1 + integrated[new_exp] = [] + + for coeff, symbols in expr: + # ∫ c*x^exp dx = (c/(exp+1))*x^(exp+1) + # 系数除以 (exp+1),可能是分数 + new_coeff = coeff / (exp + 1) + integrated[new_exp].append((new_coeff, symbols)) + + return integrated + +def format_term(term: Term) -> str: + """格式化单个 Term""" + coeff, symbols = term + + if not symbols: # 常数项 + return str(coeff) + + # 统计符号出现次数,处理 a1*a1 → a1^2 + symbol_counts = defaultdict(int) + for idx in symbols: + symbol_counts[idx] += 1 + + parts = [] + for idx in sorted(symbol_counts.keys()): + count = symbol_counts[idx] + if count == 1: + parts.append(f"a{idx}") + else: + parts.append(f"a{idx}^{count}") + + symbol_str = "*".join(parts) + + # 处理系数为1的情况(省略系数) + if coeff == 1: + return symbol_str + # 处理分数系数 + elif isinstance(coeff, float) and not coeff.is_integer(): + return f"({coeff})*{symbol_str}" + else: + return f"{int(coeff)}*{symbol_str}" + +def sum_integrals_same_bounds(polynomials: List[Polynomial], + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 多个多项式在相同区间[a, b]上积分后求和 + + 参数: + polynomials: 多项式列表 [poly1, poly2, ...] + a, b: 积分下限和上限(所有多项式相同) + + 返回: + 合并后的符号表达式(不含x) + """ + # 初始化结果为空表达式 + total_expression = [] # 相当于0 + + #print(f"sum_integrals_same_bounds polynomials={polynomials}") + + # 遍历每个多项式 + for idx, poly in enumerate(polynomials): + # 对当前多项式在[a, b]上积分 + integral_result = evaluate_polynomial_integral(poly, a, b) + + # 打印每个多项式的积分结果(调试用) + #print(f" Integration Result for Term{idx+1}: {format_expression(integral_result)}") + print(f" Integration Result for Term{idx+1}: {format_expression_fraction(integral_result)}") + + # 累加到总表达式中 + total_expression = expression_add(total_expression, integral_result) + + return total_expression + +def format_expression(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + if len(symbols) == 1: + term_strs.append(f"{coeff}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{coeff}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coeff}*{symbol_str}") + + return " + ".join(term_strs) + +def format_expression_fraction(expr: Expression) -> str: + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + for coeff, symbols in expr: + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + if len(symbols) == 1: + term_strs.append(f"{frac_str}*a{symbols[0]}") + elif symbols[0] == symbols[1]: + term_strs.append(f"{frac_str}*a{symbols[0]}^2") + else: + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{frac_str}*{symbol_str}") + + return " + ".join(term_strs) + +def print_polynomial(polynomial: Polynomial, title: str = ""): + """打印多项式,带标题""" + if title: + print(f"\n{title}:") + + if not polynomial: + print("0") + return + + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + expr_str = format_expression(polynomial[exp]) + + # 处理常数项 (x^0) + if exp == 0: + print(f"({expr_str})", end='') + else: + print(f"({expr_str})*x^{exp}", end='') + + # 打印连接符 + if idx < len(sorted_exps) - 1: + print(" + ", end='') + else: + print() + +def derivative_form(n, m): + """ + 返回 x^n 的 m 阶导数形式 (系数, x的指数) + 示例: derivative_form(3, 2) → (6, 1) 表示 6x^1 + """ + if m > n: + return (0, 0) # 或返回 None + + # 计算系数 n!/(n-m)! + coeff = math.factorial(n) // math.factorial(n - m) + power = n - m + + return (coeff, power) + +def evaluate_polynomial_integral(polynomial: Polynomial, + a: float = -0.5, + b: float = 0.5) -> Expression: + """ + 在区间 [a, b] 上对多项式进行定积分 + 返回:纯符号表达式(不再含x) + + 示例: + ∫[-0.5,0.5] [a1^2 + 4*a1*a2*x + 4*a2^2*x^2] dx + = 1*a1^2 + 0*a1*a2 + (1/3)*a2^2 + """ + # 用字典存储符号表达式:键=符号元组,值=总系数 + # 例如: {(1,1): 1.0, (2,2): 0.3333} + result_dict = defaultdict(float) + + # 遍历多项式的每一项:指数 -> 表达式 + # polynomial = {0: [(1, [1,1])], 1: [(4, [1,2])], 2: [(4, [2,2])]} + for exp, expr in polynomial.items(): + # exp: x的指数(0, 1, 2...) + # expr: 对应指数的表达式列表 + + # 计算 ∫ x^exp dx = [x^(exp+1)/(exp+1)] 在 a 到 b 的值 + # integral_factor = (b^(exp+1) - a^(exp+1)) / (exp+1) + integral_factor = (b ** (exp + 1) - a ** (exp + 1)) / (exp + 1) + # 示例: exp=0 → (0.5^1 - (-0.5)^1)/1 = (0.5 + 0.5) = 1 + # exp=1 → (0.5^2 - (-0.5)^2)/2 = (0.25 - 0.25)/2 = 0 + # exp=2 → (0.5^3 - (-0.5)^3)/3 = (0.125 + 0.125)/3 = 0.25/3 ≈ 0.08333 + + # 遍历该指数下的所有项 + for coeff, symbols in expr: + # coeff: 数值系数(如 4) + # symbols: 符号下标列表(如 [1,2] 表示 a1*a2) + + # 生成符号键:排序后的符号元组(确保 a1*a2 和 a2*a1 相同) + # tuple(sorted([1,2])) = (1,2) + symbol_key = tuple(sorted(symbols)) + + # 累加积分后的系数 + # 总系数 = 原系数 * 积分因子 + # 例: exp=2, coeff=4, integral_factor≈0.08333 + # contribution = 4 * 0.08333 ≈ 0.3333 + contribution = coeff * integral_factor + + result_dict[symbol_key] += contribution + # 如果是相同符号项,系数会累加(如多处出现 a1^2) + + # 转换回 Expression 格式:[(系数, [符号列表]), ...] + # 过滤掉系数为0的项(如奇函数项) + result_expression = [ + (coeff, list(symbols)) + for symbols, coeff in result_dict.items() + if abs(coeff) > 1e-10 # 避免浮点误差 + ] + + return result_expression + +def evaluate_and_print(polynomial: Polynomial, title: str = ""): + """求值并打印结果""" + if title: + print(f"\n{title}") + + # 在 [-0.5, 0.5] 上积分 + result = evaluate_polynomial_integral(polynomial, -0.5, 0.5) + + # 格式化输出 + result_str = format_expression(result) + print(f"∫[-1/2,1/2] P(x) dx = {result_str}") + +def print_power_symbol(power_map): + sorted_keys = sorted(power_map) + # 遍历所有键,但只在不是最后一项时打印 "+" + #print(f"len(sorted_keys)={len(sorted_keys)}") + for idx, key in enumerate(sorted_keys): + mylist = power_map[key] + n = len( mylist ) + print(f"(", end = '') + for i in range(n): + coef, acoef = mylist[i] + if i == n-1: + print(f"{coef}*a{acoef}", end = '') + else: + print(f"{coef}*a{acoef} + ", end = '') + # 判断是否是最后一项(关键修改) + print(f")*x^{key}",end = '') + if idx < len(sorted_keys) - 1: + print(" + ",end = '') # 不是最后一项,打印 "+" + else: + print() # 是最后一项,只换行不打印 "+" + +def print_polynomial_old_style(polynomial, title=""): + """ + 改造重点1:创建兼容旧格式的打印函数 + 总是显示 *x^e,包括 *x^0 + """ + if title: + print(f"\n{title}") + + if not polynomial: + print("0") + return + + # 按指数排序 + sorted_exps = sorted(polynomial.keys()) + + for idx, exp in enumerate(sorted_exps): + # 格式化x^e项的系数部分 + expr = polynomial[exp] + term_strs = [] + for coef, symbols in expr: + # 如果符号列表只有一个元素 + if len(symbols) == 1: + term_strs.append(f"{coef}*a{symbols[0]}") + else: + # 多个符号相乘(虽然这里用不到,但为完整性保留) + symbol_str = "*".join([f"a{s}" for s in symbols]) + term_strs.append(f"{coef}*{symbol_str}") + + # 总是显示 *x^exp,包括 *x^0 + print(f"({' + '.join(term_strs)})*x^{exp}", end="") + + # 不是最后一项就打印" + " + if idx < len(sorted_exps) - 1: + print(" + ", end="") + else: + print() # 最后一项换行 + +def verify_format(poly: Polynomial): + """验证格式是否正确""" + for exp, expr in poly.items(): + print(f"指数 {exp}: {expr}") + for term in expr: + assert isinstance(term, tuple), "Term必须是元组" + assert len(term) == 2, "Term必须有2个元素" + coeff, symbols = term + assert isinstance(coeff, (int, float)), "系数必须是数字" + assert isinstance(symbols, list), "符号必须是列表" + print(f" - 系数: {coeff}, 符号: {symbols}") + +def compute_alpha_beta(row_index: int, r: int) -> Tuple[float, float]: + """计算第row_index行的积分区间[α, β]""" + middle = -r + row_index + return middle - 0.5, middle + 0.5 + +def compute_integral(alpha: float, beta: float, power: int) -> float: + """计算∫_{α}^{β} ξ^power dξ""" + if power == 0: + return beta - alpha + return (beta**(power + 1) - alpha**(power + 1)) / (power + 1) + +def compute_mass_matrix(k: int, r: int) -> np.ndarray: + """计算k×k的矩阵M(数值矩阵)""" + M = np.zeros((k, k), dtype=float) + for i in range(k): + alpha, beta = compute_alpha_beta(i, r) + for j in range(k): + M[i, j] = compute_integral(alpha, beta, j) + return M + +def build_moment_matrix(r: int, k: int) -> np.ndarray: + rows = [] + for m in range(k): + # Spatial index of the m-th cell in the substencil: j = i - r + m + j = -r + m + left = Fraction(j) - Fraction(1, 2) + right = Fraction(j) + Fraction(1, 2) + row = [] + for i in range(stencil_width): + val = integral_xi(right, i) - integral_xi(left, i) + row.append(val) + rows.append(row) + return np.array(rows, dtype=object) + +def compute_stencil_coefficients_for_point(k,r,x_point): + #M = build_moment_matrix(k,r) + M = compute_mass_matrix(k,r) + #print(f'M={M}') + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv={M_inv}') + monomials = np.array([x_point ** i for i in range(k)], dtype=object) + coefficients = monomials @ M_inv + return coefficients + +def generate_weno_substencils(k: int, x_point: Fraction) -> np.ndarray: + stencils = [] + for r in range(k): + # r = 0 → rightmost stencil + # r = k-1 → leftmost stencil + coef = compute_stencil_coefficients_for_point(k,r,x_point) + #print(f'coef={coef}') + stencils.append(coef) + return np.vstack(stencils) + +def generate_left_stencils(k: int, offset: Fraction = Fraction(1, 2)): + """生成左偏模板(用于 vi+1/2)""" + return generate_weno_substencils(k, offset) + +def generate_right_stencils(k: int, offset: Fraction = Fraction(1, 2)): + """生成右偏模板(用于 vi-1/2)""" + return generate_weno_substencils(k, -offset) + +def create_differential_matrix(k): + rows = k - 1 + cols = k - 1 + # matrix每个元素存Term + power + matrix = np.empty((rows, cols), dtype=object) + + # 构建matrix并打印导数系数表 + # x^1, x^2, ..., x^(k-1) + # d^1/dx^1 1 , 2x^1,...,(k-1)x^(k-2) + # d^2/dx^2 0 , 2 ,...,(k-2)(k-1)x^(k-3) + # .... + # d^k-1/dx^k-1 0 ,0 ,..., k! + # coef, power = n!/(n-m)!, n-m + for i in range(rows): + for j in range(cols): + #返回 x^n 的 m 阶导数形式 (系数, x的指数) + coef, power = derivative_form(j + 1, i + 1) + acoef = j + 1 + + if coef != 0: + # 新结构:Term = (系数, [符号下标列表]) + term = (coef, [acoef]) + else: + term = (0, []) + + # 存储 (Term, power) 元组 + matrix[i][j] = (term, power) + + #print(f'matrix=\n{matrix}') + return matrix + +def build_polynomial_list(matrix, num_rows, num_cols): + """ + 从差分矩阵构建多项式列表。 + 每个多项式是一个字典:{power: [terms]},其中term = (coef, symbols)。 + """ + polynomial_list = [] + for i in range(num_rows): + polynomial = defaultdict(list) + for j in range(num_cols): + term, power = matrix[i][j] + coef, symbols = term + if coef != 0: + polynomial[power].append(term) + polynomial_list.append(dict(polynomial)) + return polynomial_list + + +def compute_squared_polynomials(polynomial_list): + """ + 计算多项式列表中每个多项式的平方。 + 返回平方后的多项式列表。 + """ + squared_list = [] + for poly in polynomial_list: + squared = polynomial_square(poly) + squared_list.append(squared) + return squared_list + +def print_original_polynomials(squared_polynomials): + """ + 以旧风格打印平方后的多项式列表。 + """ + print("\nInitial Polynomial Expressions (before integration):") + for i, poly in enumerate(squared_polynomials, 1): + print(f" Polynomial Term {i}: P{i}(x) = ", end="") + print_polynomial_old_style(poly, "") + +def solve_for_coefficients(M): + rows, cols = M.shape + #print(f'rows,cols={rows},{cols}') + a_coeffs = np.empty((rows, cols), dtype=object) + for i in range(rows): + for j in range(cols): + coeff = M[i, j] + a_coeffs[i,j] = (coeff, j) + #print(f'a_coeffs={a_coeffs}') + return a_coeffs + +def to_fraction(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + return frac + +def float_to_fraction_str(num, max_denominator=1000000): + """将浮点数转换为最简分数字符串""" + frac = Fraction(num).limit_denominator(max_denominator) + if frac.denominator == 1: + return str(frac.numerator) + return f"{frac.numerator}/{frac.denominator}" + +def coef_to_str(coeff, id, isfirst): + csign = '-' + #print(f'isfirst={isfirst}') + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = ' ' + else: + csign = '-' + + v_str = f'{csign}{abs(coeff)}*v[{id}]' + return v_str + +def id_with_sign(id): + id_sign = '-' + if id >= 0: + id_sign = '+' + return id_sign, f"{abs(id)}" + +def coef_to_fraction_str(coeff, id, isfirst): + csign = '-' + if coeff >= 0: + if not isfirst: + csign = '+' + else: + csign = '' + else: + csign = '-' + + max_denominator = 1000000 + frac = Fraction(abs(coeff)).limit_denominator(max_denominator) + frac_str = f"{frac.numerator}/{frac.denominator}" + if frac.denominator == 1: + frac_str = f"{frac.numerator}" + + #frac_star = f"{frac_str}·" + frac_star = f"{frac_str}" + + if frac_str == "1": + frac_star ="" + + if frac_str == "0": + return "" + + id_sign, abs_id = id_with_sign(id) + + v_str = f"{csign} {frac_star}v[i{id_sign}{abs_id}]" + return v_str + +def print_coeffs_expression(a_coeffs,k,r): + rows, cols = a_coeffs.shape + print(f'\nr={r},k={k}') + for i in range(rows): + expr_parts = [f'a[{i}] = '] + for j in range(cols): + coeff, id = a_coeffs[i, j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f'{v_str}') + expr = ' '.join(expr_parts) + print(f'{expr}') + + return a_coeffs + +def print_separator(length=70, char='='): + """打印指定长度和字符的分隔线""" + print(char * length) + +def get_index_range(k, r): + # Generate index range string + if r == 0: + return f"[i,i+{k-1}]" + elif r == k-1: + return f"[i-{k-1}, i]" + else: + return f"[i-{r},i+{k-1-r}]" + +def print_polynomial_coefficients(k, coeffs_list, v_name='v'): + """ + Print reconstruction coefficients in a professional academic format (English) + """ + # Print header + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + + # Process each r value + r_values = list(range(k)) + for idx, (r, coeffs) in enumerate(zip(r_values, coeffs_list)): + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + print_separator(60, "-") + + M_inv = coeffs # Assuming input is already M^{-1} + + for a_idx in range(k): + terms = [] + + for col in range(k): + coeff, id = M_inv[a_idx, col] + + # Skip near-zero coefficients + if abs(coeff) < 1e-12: + continue + + # Convert to fraction + frac = Fraction(coeff).limit_denominator(1000000) + if frac.denominator == 1: + coeff_str = str(frac.numerator) + else: + coeff_str = f"{frac.numerator}/{frac.denominator}" + + # Handle sign + if float(coeff) >= 0: + sign = " + " if terms else " " + else: + sign = " - " + if coeff_str.startswith('-'): + coeff_str = coeff_str[1:] + + # Handle coefficient of ±1 + if coeff_str == '1': + term = f"{sign}{v_name}[i" + else: + term = f"{sign}{coeff_str}·{v_name}[i" + + # Determine index offset + offset = col - r + if offset > 0: + term += f"+{offset}" + elif offset < 0: + term += f"{offset}" + term += "]" + + terms.append(term) + + expression = "".join(terms) if terms else " 0" + print(f"a{a_idx} = {expression}") + + print() + print_separator() + +def solve_polynomial_coefficients(k, r): + M = compute_mass_matrix(k,r) + #print(f'mass_matrix=\n{M}') + + try: + M_inv = np.linalg.inv(M) + except np.linalg.LinAlgError: + raise ValueError(f"矩阵M (k={k}, r={r})不可逆!") + #print(f'M_inv=\n{M_inv}') + + a_coeffs = solve_for_coefficients(M_inv) + return a_coeffs + + +def solve_smoothness_indicator(k): + # 创建差分矩阵 + matrix = create_differential_matrix(k) + num_rows = k - 1 + num_cols = k - 1 + + #print(f'差分矩阵:\n{matrix}') + + # 从矩阵构建多项式列表 + polynomial_list = build_polynomial_list(matrix, num_rows, num_cols) + #print(f"k={k},polynomial_list={polynomial_list}") + + # 计算每个多项式的平方 + squared_polynomials = compute_squared_polynomials(polynomial_list) + + # 打印平方后的多项式(原始多项式列表) + print_original_polynomials(squared_polynomials) + + # 在指定区间上积分求和 + lower_bound = -0.5 + upper_bound = 0.5 + domain = f"[{to_fraction(lower_bound)}, {to_fraction(upper_bound)}]" + print(f"\nStep-by-step Integration and Summation (integration domain: x∈{domain}):") + + total_result = sum_integrals_same_bounds(squared_polynomials, lower_bound, upper_bound) + #print(f"k={k},total_result={total_result}") + + # 打印最终结果 + print(f"\nFinal Aggregated Result (sum of all integrated terms):") + #formatted_result = format_expression(total_result) + formatted_result = format_expression_fraction(total_result) + print(f"Σ ∫ P_i(x) dx = {formatted_result}") + return total_result + +def sort_indices_with_counts(index_list): + """ + 统计下标频次并排序 + + 返回: (排序后的下标列表, 对应的次数列表) + """ + freq_dict = Counter(index_list) + sorted_items = sorted(freq_dict.items()) + indices, counts = zip(*sorted_items) # 解压元组 + return list(indices), list(counts) + +def polynomial_coefficients_str(coeffs,k,r): + expr_parts = [] + for j in range(k): + coeff, id = coeffs[j] + v_str = coef_to_fraction_str(coeff, -r+id, j==0) + expr_parts.append(f"{v_str}") + expr = ' '.join(expr_parts) + #print(f'{expr}') + return expr + +def get_numeric_list(numbers): + """ + 从元组列表中提取第一个数值元素 + + 参数: + numbers: list[tuple] - 元组列表,每个元组第一个元素为np.float64 + 返回: + list[np.float64] - 数值列表 + """ + # 列表推导式 + 简单校验,避免索引越界 + return [item[0] for item in numbers if isinstance(item, tuple) and len(item) >= 1] + +def unpack_tuple_list(numbers): + #print("输入类型:", type(numbers)) # 调试:查看是list还是np.ndarray + #print("输入内容:", numbers) + float_list = [] + index_list = [] + # 遍历+类型校验,避免非法数据报错 + for item in numbers: + if isinstance(item, tuple) and len(item) >= 2: + float_val, index = item[0], item[1] + float_list.append(float_val) + index_list.append(index) + return float_list, index_list + +def zip_lists_to_tuples(value_list, index_list): + """ + 极简版合并列表,兼容任意类型(无校验,适合内部可信数据) + + 参数: + value_list: list - 任意类型数值列表 + index_list: list - 任意类型索引列表 + 返回: + list[tuple] - 合并后的元组列表 + """ + return list(zip(value_list, index_list)) + +def sort_by_first_list(primary_list, *other_lists, key=None, reverse=False): + """ + 根据第一个列表排序,同步调整任意数量其他列表 + + 参数: + primary_list: 主排序参考列表 + *other_lists: 其他需要同步排序的列表(可变参数) + key: 排序key函数(如abs, lambda x: x**2等) + reverse: 是否降序 + + 返回: + 元组: (sorted_primary, sorted_other1, sorted_other2, ...) + """ + # 核心:动态生成key函数 + if key is None: + key_func = lambda i: primary_list[i] # 默认:直接比较值 + else: + key_func = lambda i: key(primary_list[i]) # 自定义:对值应用key函数 + + # 获取排序索引 + indices = sorted(range(len(primary_list)), key=key_func, reverse=reverse) + + # 应用索引到所有列表(包括主列表) + all_lists = (primary_list,) + other_lists + result = tuple([lst[i] for i in indices] for lst in all_lists) + return result + +def format_expression_coefficients(expr, a_coeffs, k, r): + """格式化纯符号表达式""" + if not expr: + return "0" + + term_strs = [] + frac_list = [] + for coeff, symbols in expr: + indices, counts = sort_indices_with_counts(symbols) + #print(f"排序下标: {indices}") + #print(f"出现次数: {counts}") + + nSize = len(indices) + symbol_str = [] + totalfactor = 1 + for i in range(nSize): + id = indices[i] + co = counts[i] + #print(f'a_coeffs[id]={a_coeffs[id]}') + floatlist, idlist = unpack_tuple_list(a_coeffs[id]) + factor, simplified = extract_max_common_factor(floatlist) + a_coeff_new = zip_lists_to_tuples(simplified, idlist) + factors = pow(factor, co) + totalfactor *= factors + coefficients_str = polynomial_coefficients_str(a_coeff_new,k,r) + #print(f"coefficients_str: {coefficients_str}") + symbol_str.append(f"({coefficients_str} )^{co}") + symbol_str_final = "*".join(symbol_str) + + #print(f"symbol_str_final: {symbol_str_final}") + frac = Fraction(coeff*totalfactor).limit_denominator(1000000) + frac_list.append(frac) + #term_strs.append(f"{frac}·{symbol_str_final}") + term_strs.append(f"{frac}{symbol_str_final}") + + _, term_strs = sort_by_first_list(frac_list, term_strs, key=abs, reverse=True) + + return " + ".join(term_strs) + +def print_smoothness_indicator(expression,a_coeffs,k,r): + print(f"expression={expression}") + print(f"\nConfiguration Parameters: k = {k} (Polynomial Degree = {k-1})") + print(f"\n[r = {r}] (Stencil Center Offset, Covering Cells {get_index_range(k,r)})") + #print(f"β{r} = {format_expression(expression)}") + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def print_smoothness_indicators(expression,coeffs_list,k): + print_separator() + print(f"Polynomial Reconstruction: p(ξ) = a₀ + a₁ξ + a₂ξ² + ... + aₖ₋₁ξ^{{k-1}}") + print(f"Configuration Parameters: k = {k} (Polynomial Degree = {k-1})") + print_separator() + for r in range(k): + a_coeffs = coeffs_list[r] + expr_str = format_expression_coefficients(expression, a_coeffs, k, r) + print(f"β{r} = {expr_str}") + +def demo_smoothness_indicatorOld(k): + total_result = solve_smoothness_indicator(k) + #print(f'total_result={total_result}') + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + coeffs_list.append( a_coeffs ) + print_smoothness_indicator(total_result,a_coeffs,k,r) + print_polynomial_coefficients(k, coeffs_list) + +def demo_smoothness_indicator(k): + total_result = solve_smoothness_indicator(k) + + coeffs_list = [] + for r in range(k): + a_coeffs = solve_polynomial_coefficients(k, r) + #print(f"k={k},a_coeffs={a_coeffs}") + coeffs_list.append( a_coeffs ) + + print_smoothness_indicators(total_result,coeffs_list,k) + print_polynomial_coefficients(k, coeffs_list) + +if __name__ == "__main__": + #demo_smoothness_indicator(1) + #demo_smoothness_indicator(2) + #demo_smoothness_indicator(3) + kk = 3 + matrix_stencils = generate_left_stencils(kk) + print_matrix_fraction(matrix_stencils) + demo_smoothness_indicator(kk) \ No newline at end of file diff --git a/example/weno-coef/crj/python/01e/crj.py b/example/weno-coef/crj/python/01e/crj.py new file mode 100644 index 00000000..0a6fd918 --- /dev/null +++ b/example/weno-coef/crj/python/01e/crj.py @@ -0,0 +1,48 @@ +from fractions import Fraction + +def calculate_crj(r, j, k): + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + result += Fraction(numerator, denominator) + return result + +for k in range(1,8): + print(f"=== k = {k} ===") + mat = [] + r_values = range(-1, k) + for r in r_values: + row = [] + for j in range(k): + row.append(calculate_crj(r, j, k)) + mat.append(row) + + # 计算宽度(自动处理分数/负数长度) + max_width = max(len(str(item)) for row in mat for item in row) + r_width = max(len(str(r)) for r in r_values) + r_width = max(r_width, len("r")) # 表头“r”也占位 + + # 打印表头 + header = " ".join(f"{j:^{max_width}}" for j in range(k)) + print(f"{'r':>{r_width}} | {header}") + + # 打印数据行 + for r, row in zip(r_values, mat): + r_str = f"{r:>{r_width}}" + cells = " ".join(f"{str(item):^{max_width}}" for item in row) + print(f"{r_str} | {cells}") + print() # 每个 k 后空一行分隔 \ No newline at end of file diff --git a/example/weno-coef/crj/python/01f/crj.py b/example/weno-coef/crj/python/01f/crj.py new file mode 100644 index 00000000..342260e7 --- /dev/null +++ b/example/weno-coef/crj/python/01f/crj.py @@ -0,0 +1,52 @@ +from fractions import Fraction + +def calculate_crj(r, j, k): + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + result += Fraction(numerator, denominator) + return result + +for k in range(1, 8): + print(f"=== k = {k} ===") + mat = [] + r_values = range(-1, k) + for r in r_values: + row = [] + for j in range(k): + row.append(calculate_crj(r, j, k)) + mat.append(row) + + # 所有会出现字符串(包括表头 j=0, j=1...)用于计算统一宽度 + header_cells = [f"j={j}" for j in range(k)] + all_strings = header_cells + [str(item) for row in mat for item in row] + max_width = max(len(s) for s in all_strings) if all_strings else 8 + + # r 列宽度(包含表头 "r") + r_strings = ["r"] + [str(r) for r in r_values] + r_width = max(len(s) for s in r_strings) + + # 表头 + header = " ".join(f"{cell:^{max_width}}" for cell in header_cells) + print(f"{'r':>{r_width}} | {header}") + + # 数据行(数值右对齐) + for r, row in zip(r_values, mat): + r_str = f"{r:>{r_width}}" + cells = " ".join(f"{str(item):>{max_width}}" for item in row) + print(f"{r_str} | {cells}") + print() \ No newline at end of file diff --git a/example/weno-coef/crj/python/01g/crj.py b/example/weno-coef/crj/python/01g/crj.py new file mode 100644 index 00000000..16d37ec9 --- /dev/null +++ b/example/weno-coef/crj/python/01g/crj.py @@ -0,0 +1,53 @@ +from fractions import Fraction + +def calculate_crj(r, j, k): + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + result += Fraction(numerator, denominator) + return result + +for k in range(1, 8): + print(f"=== k = {k} ===") + mat = [] + # 关键修改:将 range(-1, k) 改为反向遍历(从大到小) + r_values = range(k - 1, -2, -1) # 原范围是 [-1, 0, 1, ..., k-1],反向后为 [k-1, ..., 1, 0, -1] + for r in r_values: + row = [] + for j in range(k): + row.append(calculate_crj(r, j, k)) + mat.append(row) + + # 所有会出现字符串(包括表头 j=0, j=1...)用于计算统一宽度 + header_cells = [f"j={j}" for j in range(k)] + all_strings = header_cells + [str(item) for row in mat for item in row] + max_width = max(len(s) for s in all_strings) if all_strings else 8 + + # r 列宽度(包含表头 "r") + r_strings = ["r"] + [str(r) for r in r_values] + r_width = max(len(s) for s in r_strings) + + # 表头 + header = " ".join(f"{cell:^{max_width}}" for cell in header_cells) + print(f"{'r':>{r_width}} | {header}") + + # 数据行(数值右对齐) + for r, row in zip(r_values, mat): + r_str = f"{r:>{r_width}}" + cells = " ".join(f"{str(item):>{max_width}}" for item in row) + print(f"{r_str} | {cells}") + print() \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01/testprj.py b/example/weno-coef/crj/python/expand_formula/01/testprj.py new file mode 100644 index 00000000..91654843 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01/testprj.py @@ -0,0 +1,44 @@ +def expand_formula(k: int, r: int) -> str: + """ + 展开公式 v_{i+1/2} = \sum_{j=0}^{k-1} c_{rj} \bar{v}_{i-r+j} + 返回展开后的 LaTeX 字符串 + """ + terms = [] + for j in range(k): + # 系数 + coeff = f"c_{{{r}{j}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(无空格,标准写法 i-2、i、i+3 等) + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset 为负时自动带 - + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 整项 + term = coeff + v_term + terms.append(term) + + # 左侧(保持你原始写法,也可以用 v_{i+1/2},如果你想要分数形式可改为 i + \frac{1}{2}) + left = r"v_{i+1/2}" + + # 用 " + " 连接所有项(自动处理只有一项或首项为负的情况) + equation = left + r" = " + " + ".join(terms) + + return equation + + +# 示例 +print("k=3, r=0 的展开结果:") +print(expand_formula(3, 0)) +print("\nk=3, r=1 的展开结果:") +print(expand_formula(3, 1)) +print("\nk=4, r=2 的展开结果:") +print(expand_formula(4, 2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01a/testprj.py b/example/weno-coef/crj/python/expand_formula/01a/testprj.py new file mode 100644 index 00000000..981bea1a --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01a/testprj.py @@ -0,0 +1,44 @@ +def expand_formula(k: int, r: int) -> str: + """ + 展开公式 v_{i+1/2} = \sum_{j=0}^{k-1} c_{rj} \bar{v}_{i-r+j} + 返回展开后的 LaTeX 字符串 + """ + terms = [] + for j in range(k): + # 系数:改为 c_{r,j} 形式(添加逗号) + coeff = f"c_{{{r},{j}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(无空格,标准写法 i-2、i、i+3 等) + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset 为负时自动带 - + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 整项:在系数和变量之间加薄空格 \, + term = coeff + r"\," + v_term + terms.append(term) + + # 左侧:改为 v_{i+1/2}^{(r)} 形式,其中 r 用真实值替换 + left = f"v_{{i+1/2}}^{{({r})}}" + + # 用 " + " 连接所有项(自动处理只有一项或首项为负的情况) + equation = left + r" = " + " + ".join(terms) + + return equation + + +# 示例 +print("k=3, r=0 的展开结果:") +print(expand_formula(3, 0)) +print("\nk=3, r=1 的展开结果:") +print(expand_formula(3, 1)) +print("\nk=4, r=2 的展开结果:") +print(expand_formula(4, 2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01b/testprj.py b/example/weno-coef/crj/python/expand_formula/01b/testprj.py new file mode 100644 index 00000000..9f44ba86 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01b/testprj.py @@ -0,0 +1,141 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的 r 参数 + j: 求和索引 j + k: 模板阶数 k + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m 从 j+1 到 k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + """ + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的 r 参数 + + 返回: + str: 展开后的 LaTeX 字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为 0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将 Fraction 转换为 LaTeX 格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为 ±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset 为负时自动带 - + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串 + if coeff_str == "": + # 系数为 ±1 + if is_negative: + term = "-" + v_term + else: + term = v_term + else: + # 系数不为 ±1 + term = coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是 0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + # 组合方程 + equation = left + r" = " + right_side + + return equation + +# 示例 +if __name__ == "__main__": + print("k=3, r=0 的展开结果:") + print(expand_formula_with_values(3, 0)) + print("\nk=3, r=1 的展开结果:") + print(expand_formula_with_values(3, 1)) + print("\nk=4, r=2 的展开结果:") + print(expand_formula_with_values(4, 2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01b0/testprj.py b/example/weno-coef/crj/python/expand_formula/01b0/testprj.py new file mode 100644 index 00000000..65f0278d --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01b0/testprj.py @@ -0,0 +1,166 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 测试单个展开 + print("单个展开测试:") + print("k=3, r=0 的展开结果:") + print(expand_formula_with_values(3, 0)) + print("\nk=3, r=1 的展开结果:") + print(expand_formula_with_values(3, 1)) + print("\nk=4, r=2 的展开结果:") + print(expand_formula_with_values(4, 2)) + print("\n" + "="*60 + "\n") + + # 测试array输出 + for k in [3, 4, 5]: + print(f"k={k} 时的展开公式组:") + print(generate_latex_array(k)) + print("\n" + "="*60 + "\n") \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01c/testprj.py b/example/weno-coef/crj/python/expand_formula/01c/testprj.py new file mode 100644 index 00000000..806999ca --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01c/testprj.py @@ -0,0 +1,180 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) + + print("\n" + "="*60 + "\n") + + # 额外:测试k=2 + print("="*60) + print("k=2 时的展开公式组(r从1到-1):") + print("="*60) + print(generate_latex_array(2)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01d/testprj.py b/example/weno-coef/crj/python/expand_formula/01d/testprj.py new file mode 100644 index 00000000..a053e6dc --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01d/testprj.py @@ -0,0 +1,173 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串 + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + # 修正:使用正确的双反斜杠换行符(LaTeX中的\\对应Python字符串中的\\\\) + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01e/testprj.py b/example/weno-coef/crj/python/expand_formula/01e/testprj.py new file mode 100644 index 00000000..ebdcfd5b --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01e/testprj.py @@ -0,0 +1,175 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(使用\phantom实现宽度对齐) + # i -> i\phantom{+0} (占位但不显示,宽度与i+1相同) + # i+1 -> i+1 + # i-1 -> i-1 + if offset == 0: + subscript = r"i\hphantom{+0}" # 关键修改:占位对齐 + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01f/testprj.py b/example/weno-coef/crj/python/expand_formula/01f/testprj.py new file mode 100644 index 00000000..cfe46149 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01f/testprj.py @@ -0,0 +1,171 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def expand_formula_with_values(k: int, r: int) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 将Fraction转换为LaTeX格式 + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + # 系数为±1,省略数字 + coeff_str = "" + else: + coeff_str = str(abs_fraction.numerator) + else: + # 分数情况 + coeff_str = f"\\frac{{{abs_fraction.numerator}}}{{{abs_fraction.denominator}}}" + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(使用\phantom实现宽度对齐) + # i -> i\phantom{+0} (占位但不显示,宽度与i+1相同) + # i+1 -> i+1 + # i-1 -> i-1 + if offset == 0: + subscript = r"i\hphantom{+0}" # 关键修改:占位对齐 + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串(修正符号处理) + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号,添加分隔符 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + equations = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01f0/testprj.py b/example/weno-coef/crj/python/expand_formula/01f0/testprj.py new file mode 100644 index 00000000..bde9b35e --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01f0/testprj.py @@ -0,0 +1,147 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + result += Fraction(numerator, denominator) + + return result + +def calculate_max_widths(k: int) -> dict: + """ + 计算所有公式中最大的分子、分母宽度 + + 参数: + k: 模板阶数 + + 返回: + dict: {'num': 最大分子长度, 'den': 最大分母长度} + """ + max_num_len = 0 + max_den_len = 0 + + for r in range(k-1, -2, -1): + for j in range(k): + coeff = calculate_crj(r, j, k) + if coeff == 0: + continue + + if coeff.denominator != 1: + max_num_len = max(max_num_len, len(str(abs(coeff.numerator)))) + max_den_len = max(max_den_len, len(str(abs(coeff.denominator)))) + + return {'num': max_num_len, 'den': max_den_len} + +def generate_latex_alignat(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的alignat环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX alignat字符串 + """ + max_widths = calculate_max_widths(k) + + lines = [] + + for r in range(k-1, -2, -1): + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + # 右侧开始 + right_parts = [] + + # 收集所有项数据 + terms_data = [] + for j in range(k): + coeff = calculate_crj(r, j, k) + if coeff == 0: + continue + + is_negative = (coeff < 0) + abs_coeff = abs(coeff) + coeff_str = abs_coeff + + # 下标部分 + offset = j - r + if offset == 0: + subscript = r"i\hphantom{+0}" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" + + v_term = r"\bar{v}_{" + subscript + "}" + terms_data.append((is_negative, coeff_str, v_term)) + + # 组装右侧表达式 + if not terms_data: + right_parts.append("0") + else: + for idx, (is_negative, coeff_str, v_term) in enumerate(terms_data): + # 符号处理 + if idx == 0: + # 第一项:符号与项在同一单元格 + sign = "-" if is_negative else r"\phantom{+}" + term = f"{sign}{coeff_str}{v_term}" if coeff_str else f"{sign}{v_term}" + right_parts.append(term) + else: + # 后续项:符号和项分开对齐 + sign = "-" if is_negative else "+" + term = f"{coeff_str}{v_term}" if coeff_str else v_term + right_parts.append(f"&&{sign} {term}") + + # 组合成行 + line = f" {left} &= " + " ".join(right_parts) + r" \\" + lines.append(line) + + # 计算alignat列数:每项需要2列(符号列和项列),但第一项的符号和项在同一列 + max_terms = max(len([j for j in range(k) if calculate_crj(r, j, k) != 0]) for r in range(k-1, -2, -1)) + alignat_cols = 2 * max_terms - 1 + + content = "\n".join(lines) + return f"\\begin{{alignat}}{{{alignat_cols}}}\n{content}\n\\end{{alignat}}" + +# 主函数示例 +if __name__ == "__main__": + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_alignat(3)) + + print("\n" + "="*60 + "\n") + + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_alignat(4)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/01g/testprj.py b/example/weno-coef/crj/python/expand_formula/01g/testprj.py new file mode 100644 index 00000000..37cc5706 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/01g/testprj.py @@ -0,0 +1,167 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + # 外层求和:m从j+1到k + for m in range(j + 1, k + 1): + # 计算分子部分 + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + # 计算连乘积 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + # 计算分母部分 + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + # 累加当前项 + result += Fraction(numerator, denominator) + + return result + +def generate_latex_alignat(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的alignat环境格式化输出 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX alignat字符串 + """ + rows = [] + + # r从k-1到-1(降序) + for r in range(k-1, -2, -1): + terms_info = [] + for j in range(k): + coeff_fraction = calculate_crj(r, j, k) + + if coeff_fraction == 0: + continue + + # 符号和绝对值 + sign = -1 if coeff_fraction < 0 else 1 + abs_coeff = abs(coeff_fraction) + + # 生成系数字符串 + if abs_coeff.denominator == 1: + if abs_coeff.numerator == 1: + coeff_str = "" # 系数为1,省略数字 + else: + coeff_str = str(abs_coeff.numerator) + else: + coeff_str = f"\\frac{{{abs_coeff.numerator}}}{{{abs_coeff.denominator}}}" + + # 生成变量下标 + offset = j - r + if offset == 0: + subscript = "i" + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" + + # 变量部分 + v_term = f"\\bar{{v}}_{{{subscript}}}" + + terms_info.append({ + 'sign': sign, + 'coeff_str': coeff_str, + 'v_term': v_term + }) + + # 构建行内容 + if not terms_info: + row = f" v_{{i+1/2}}^{{({r})}} &= 0" + else: + # 左侧 + left_part = f" v_{{i+1/2}}^{{({r})}} &=" + + # 处理每一项 + term_parts = [] + for idx, term_info in enumerate(terms_info): + # 符号处理 + if idx == 0: + # 第一项 + if term_info['sign'] == 1: + sign_str = "\\phantom{+}" + else: + sign_str = "-" + else: + # 后续项 + if term_info['sign'] == 1: + sign_str = "+" + else: + sign_str = "-" + + # 系数和变量 + coeff_str = term_info['coeff_str'] + v_term = term_info['v_term'] + + if coeff_str == "": + term_str = f"{sign_str} {v_term}" + else: + term_str = f"{sign_str} {coeff_str}{v_term}" + + term_parts.append(term_str) + + # 用 && 连接各项(第一项前不加&&) + right_side = term_parts[0] + for part in term_parts[1:]: + right_side += f" && {part}" + + row = f"{left_part} {right_side}" + + rows.append(row) + + # 构建alignat环境 + n_columns = k # 项数 + array_content = " \\\\\n".join(rows) + return f"\\begin{{alignat}}{{{n_columns}}}\n{array_content}\n\\end{{alignat}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_alignat(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_alignat(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_alignat(5)) \ No newline at end of file diff --git a/example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py b/example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py new file mode 100644 index 00000000..d9001b50 --- /dev/null +++ b/example/weno-coef/crj/python/expand_formula/KKKKKK01f/testprj.py @@ -0,0 +1,210 @@ +from fractions import Fraction + +def calculate_crj(r: int, j: int, k: int) -> Fraction: + """ + 计算系数 c_{r,j} 的值 + + 参数: + r: 公式中的r参数 + j: 求和索引j + k: 模板阶数 + + 返回: + Fraction: 系数的有理数值 + """ + result = Fraction(0, 1) + for m in range(j + 1, k + 1): + numerator = 0 + for l in range(0, k + 1): + if l == m: + continue + product = 1 + for q in range(0, k + 1): + if q == m or q == l: + continue + product *= (r - q + 1) + numerator += product + + denominator = 1 + for l in range(0, k + 1): + if l == m: + continue + denominator *= (m - l) + + result += Fraction(numerator, denominator) + + return result + +def generate_aligned_fraction(abs_fraction: Fraction, max_num_len: int, max_den_len: int) -> str: + """ + 生成分数字符串,带有对齐填充 + + 参数: + abs_fraction: 分数的绝对值 + max_num_len: 最大分子字符串长度 + max_den_len: 最大分母字符串长度 + + 返回: + str: 对齐的LaTeX分数字符串 + """ + if abs_fraction.denominator == 1: + # 整数情况 + if abs_fraction.numerator == 1: + return "" # ±1时省略 + else: + return str(abs_fraction.numerator) + + # 分数情况 + num_str = str(abs_fraction.numerator) + den_str = str(abs_fraction.denominator) + + # 计算需要的填充长度 + num_pad_len = max_num_len - len(num_str) + den_pad_len = max_den_len - len(den_str) + + # 构建填充字符串(使用\hphantom) + # \hphantom{0} 会创建一个与"0"等宽的空白 + num_padding = r"\hphantom{" + "0" * num_pad_len + "}" if num_pad_len > 0 else "" + den_padding = r"\hphantom{" + "0" * den_pad_len + "}" if den_pad_len > 0 else "" + + # 返回带填充的分数 + return f"\\frac{{{num_padding}{num_str}}}{{{den_padding}{den_str}}}" + +def expand_formula_with_values(k: int, r: int, max_widths: dict) -> str: + r""" + 展开公式 v_{i+1/2}^{(r)} = \sum_{j=0}^{k-1} c_{r,j} \bar{v}_{i-r+j} + 其中 c_{r,j} 使用 calculate_crj(r, j, k) 计算的实际值 + + 参数: + k: 模板阶数 + r: 公式中的r参数 + max_widths: 包含全局最大宽度的字典 {'num': int, 'den': int} + + 返回: + str: 展开后的LaTeX字符串 + """ + terms = [] + + for j in range(k): + # 计算实际系数值 + coeff_fraction = calculate_crj(r, j, k) + + # 如果系数为0,跳过该项 + if coeff_fraction == 0: + continue + + # 判断符号并取绝对值 + is_negative = (coeff_fraction < 0) + abs_fraction = abs(coeff_fraction) + + # 生成分数字符串(带对齐) + coeff_str = generate_aligned_fraction(abs_fraction, max_widths['num'], max_widths['den']) + + # 计算下标偏移量 + offset = j - r + + # 生成下标字符串(使用\hphantom实现宽度对齐) + if offset == 0: + subscript = r"i\hphantom{+0}" # 占位对齐 + elif offset > 0: + subscript = f"i+{offset}" + else: + subscript = f"i{offset}" # offset为负时自动带- + + # 变量部分 + v_term = r"\bar{v}_{" + subscript + "}" + + # 构建最终项字符串 + sign_prefix = "-" if is_negative else "" + + if coeff_str == "": + term = sign_prefix + v_term + else: + term = sign_prefix + coeff_str + r"\," + v_term + + terms.append(term) + + # 如果所有项都是0 + if not terms: + right_side = "0" + else: + # 处理符号连接,确保格式美观 + formatted_terms = [] + for i, term in enumerate(terms): + if i == 0: + # 第一项直接添加 + formatted_terms.append(term) + else: + # 后续项根据符号添加连接符 + if term.startswith('-'): + formatted_terms.append(' - ' + term[1:]) # 去掉负号 + else: + formatted_terms.append(' + ' + term) + + right_side = "".join(formatted_terms) + + # 左侧:v_{i+1/2}^{(r)} + left = f"v_{{i+1/2}}^{{({r})}}" + + return f"{left} = {right_side}" + +def generate_latex_array(k: int) -> str: + """ + 生成给定k值时,r从k-1到-1的所有展开公式 + 使用LaTeX的array环境格式化输出,带全局分数对齐 + + 参数: + k: 模板阶数 + + 返回: + str: 包含所有r值的LaTeX array字符串 + """ + # 第一步:计算所有公式的系数,找出全局最大分子分母宽度 + max_num_len = 0 + max_den_len = 0 + + for r in range(k-1, -2, -1): + for j in range(k): + coeff = calculate_crj(r, j, k) + if coeff == 0: + continue + + if coeff.denominator != 1: + max_num_len = max(max_num_len, len(str(abs(coeff.numerator)))) + max_den_len = max(max_den_len, len(str(abs(coeff.denominator)))) + + max_widths = {'num': max_num_len, 'den': max_den_len} + + # 第二步:生成所有对齐的公式 + equations = [] + for r in range(k-1, -2, -1): + eq = expand_formula_with_values(k, r, max_widths) + equations.append(eq) + + # 构建array环境,左对齐 + array_content = " \\\\\n".join([f" {eq}" for eq in equations]) + return f"\\begin{{array}}{{l}}\n{array_content}\n\\end{{array}}" + +# 主函数示例 +if __name__ == "__main__": + # 示例:k=3 时的多行公式输出 + print("="*60) + print("k=3 时的展开公式组(r从2到-1):") + print("="*60) + print(generate_latex_array(3)) + + print("\n" + "="*60 + "\n") + + # 示例:k=4 时的多行公式输出 + print("="*60) + print("k=4 时的展开公式组(r从3到-1):") + print("="*60) + print(generate_latex_array(4)) + + print("\n" + "="*60 + "\n") + + # 示例:k=5 时的多行公式输出 + print("="*60) + print("k=5 时的展开公式组(r从4到-1):") + print("="*60) + print(generate_latex_array(5)) \ No newline at end of file