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