|
| 1 | +import numpy as np |
| 2 | +import matplotlib.pyplot as plt |
| 3 | + |
| 4 | +# ====================== |
| 5 | +# 双线性插值采样函数 |
| 6 | +# ====================== |
| 7 | +def bilinear_sample(arr, yf, xf): |
| 8 | + """在浮点索引 (yf=row, xf=col) 上采样 2D 数组 arr 的双线性插值值。""" |
| 9 | + rows, cols = arr.shape |
| 10 | + if yf < 0 or xf < 0 or yf > rows - 1 or xf > cols - 1: |
| 11 | + return np.nan |
| 12 | + i0 = int(np.floor(yf)); j0 = int(np.floor(xf)) |
| 13 | + i1 = min(i0 + 1, rows - 1); j1 = min(j0 + 1, cols - 1) |
| 14 | + dy = yf - i0; dx = xf - j0 |
| 15 | + v00 = arr[i0, j0]; v10 = arr[i1, j0]; v01 = arr[i0, j1]; v11 = arr[i1, j1] |
| 16 | + return (v00 * (1 - dy) * (1 - dx) + |
| 17 | + v10 * dy * (1 - dx) + |
| 18 | + v01 * (1 - dy) * dx + |
| 19 | + v11 * dy * dx) |
| 20 | + |
| 21 | + |
| 22 | +# ====================== |
| 23 | +# 稳定版落石物理模拟 |
| 24 | +# ====================== |
| 25 | +def rockfall_physics_stable(dem, start_idx, cellsize=1.0, |
| 26 | + g=9.81, dt=0.1, friction=0.3, |
| 27 | + max_steps=20000, speed_tol=1e-2, slope_tol=1e-3): |
| 28 | + """ |
| 29 | + 稳定版落石运动模拟,包含半隐式积分与低坡度停止判断。 |
| 30 | + 返回路径 (row, col)、速度数组、动能数组。 |
| 31 | + """ |
| 32 | + rows, cols = dem.shape |
| 33 | + grad_y_full, grad_x_full = np.gradient(dem, cellsize, cellsize) |
| 34 | + |
| 35 | + start_row, start_col = start_idx |
| 36 | + pos_y = start_row * cellsize |
| 37 | + pos_x = start_col * cellsize |
| 38 | + vx, vy = 0.0, 0.0 |
| 39 | + |
| 40 | + path_indices = [(start_row, start_col)] |
| 41 | + path_speed = [0.0] |
| 42 | + path_energy = [0.0] # 动能 (0.5 * v^2),假设单位质量=1 |
| 43 | + |
| 44 | + for step in range(max_steps): |
| 45 | + frac_row = pos_y / cellsize |
| 46 | + frac_col = pos_x / cellsize |
| 47 | + |
| 48 | + if frac_row < 1 or frac_row > rows - 2 or frac_col < 1 or frac_col > cols - 2: |
| 49 | + break |
| 50 | + |
| 51 | + dg_dx = bilinear_sample(grad_x_full, frac_row, frac_col) |
| 52 | + dg_dy = bilinear_sample(grad_y_full, frac_row, frac_col) |
| 53 | + if np.isnan(dg_dx) or np.isnan(dg_dy): |
| 54 | + break |
| 55 | + |
| 56 | + grad_norm = np.hypot(dg_dx, dg_dy) |
| 57 | + denom = np.sqrt(1.0 + grad_norm**2) |
| 58 | + |
| 59 | + # 半隐式积分 |
| 60 | + a_x = -g * dg_dx / denom - friction * vx |
| 61 | + a_y = -g * dg_dy / denom - friction * vy |
| 62 | + |
| 63 | + vx += a_x * dt |
| 64 | + vy += a_y * dt |
| 65 | + pos_x += vx * dt |
| 66 | + pos_y += vy * dt |
| 67 | + |
| 68 | + speed = np.hypot(vx, vy) |
| 69 | + kinetic_E = 0.5 * speed**2 |
| 70 | + |
| 71 | + path_indices.append((pos_y / cellsize, pos_x / cellsize)) |
| 72 | + path_speed.append(speed) |
| 73 | + path_energy.append(kinetic_E) |
| 74 | + |
| 75 | + if speed < speed_tol or grad_norm < slope_tol: |
| 76 | + break |
| 77 | + |
| 78 | + return np.array(path_indices), np.array(path_speed), np.array(path_energy) |
| 79 | + |
| 80 | + |
| 81 | +# ====================== |
| 82 | +# 绘图函数(在独立弹窗中显示) |
| 83 | +# ====================== |
| 84 | +def plot_with_speed_and_energy(dem, path_idx, speed, energy, start_idx, cmap='terrain'): |
| 85 | + plt.figure(figsize=(14, 6)) |
| 86 | + gs = plt.GridSpec(1, 2, width_ratios=[2, 1]) |
| 87 | + |
| 88 | + # ---- 左图:DEM + 路径 ---- |
| 89 | + ax1 = plt.subplot(gs[0]) |
| 90 | + im = ax1.imshow(dem, cmap=cmap, origin='upper') |
| 91 | + plt.colorbar(im, ax=ax1, label='Elevation (m)') |
| 92 | + |
| 93 | + if len(path_idx) > 1: |
| 94 | + for k in range(len(path_idx)-1): |
| 95 | + y0, x0 = path_idx[k] |
| 96 | + y1, x1 = path_idx[k+1] |
| 97 | + s = speed[k] |
| 98 | + ax1.plot([x0, x1], [y0, y1], color=plt.cm.autumn_r(min(1.0, s/10.0)), linewidth=2) |
| 99 | + |
| 100 | + ax1.scatter([start_idx[1]], [start_idx[0]], c='k', s=60, label='Start', zorder=3) |
| 101 | + ax1.set_title('Rockfall Path (color ≈ speed)') |
| 102 | + ax1.legend() |
| 103 | + ax1.invert_yaxis() |
| 104 | + |
| 105 | + # ---- 右图:动能曲线 ---- |
| 106 | + ax2 = plt.subplot(gs[1]) |
| 107 | + ax2.plot(energy, color='orange', linewidth=2) |
| 108 | + ax2.set_title("Kinetic Energy vs Step") |
| 109 | + ax2.set_xlabel("Step") |
| 110 | + ax2.set_ylabel("Energy (J/kg)") |
| 111 | + ax2.grid(alpha=0.3) |
| 112 | + |
| 113 | + plt.tight_layout() |
| 114 | + plt.show() # 原生 Matplotlib 弹窗(含放大/缩放/回退功能) |
0 commit comments