feat: 增加驱动力系统、Marker渲染模式、动画防闪退、案例文档

- 新增 driving_force 驱动力系统(driver.txt 定义,支持周期控制)
- 新增 use_marker 渲染开关(GPU实例化点精灵,提升大量原子性能)
- 修复动画闪退:独立控制台、错误日志、启动存活检测
- 重绘 draw.py 架构:双渲染模式 + 预分配键线缓冲区
- 修复 raw trajectory 采样时间变量遮蔽 bug
- 重构 case05: 60原子一维链 + 驱动力 + 完整案例文档
- 修复所有案例 Readme.md 编码(GBK → UTF-8)
- 所有 input.txt 新增 driver_file / driving_force / use_marker 参数
This commit is contained in:
2026-06-10 15:34:53 +08:00
parent 0f04630fc0
commit 854f00ae44
28 changed files with 1404 additions and 68 deletions
+152
View File
@@ -69,8 +69,12 @@ GRAVITY_FIELD = 1 # 均匀重力场
GRAVITY_INTERACTION = 0 # 原子间万有引力 GRAVITY_INTERACTION = 0 # 原子间万有引力
ELASTIC_FORCE = 1 # 弹簧键力 ELASTIC_FORCE = 1 # 弹簧键力
DAMPING_FORCE = 0 # 阻尼 DAMPING_FORCE = 0 # 阻尼
DRIVING_FORCE = 0 # 驱动力
GRAVITY_STRENGTH = 1.0 GRAVITY_STRENGTH = 1.0
# 驱动力数据
DRIVER_DATA = None # 加载自 driver.txt
# 派生边界(根据 box_a 计算) # 派生边界(根据 box_a 计算)
X_MIN = None X_MIN = None
X_MAX = None X_MAX = None
@@ -328,6 +332,135 @@ def load_bond_connections(connection_path, atom_ids, atom_positions, bond_map):
) )
# ===========================================================================
# 驱动力加载 & 应用
# ===========================================================================
def load_driver_file(driver_path, atom_ids):
"""从 driver.txt 加载驱动力定义。
格式:
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
数值 0 0 0 0 0 10 0 0 90 all
其中:
position = A * cos(2π f t + φ), φ 为角度制(自动转弧度)
period = all | 数值(周期数,结束后原子静止)
"""
if not os.path.exists(driver_path):
print(f"[compute] 警告: 驱动力文件不存在: {driver_path}")
return None
atom_index = {int(aid): idx for idx, aid in enumerate(atom_ids)}
drivers = []
ncols = 11
with open(driver_path, "r", encoding="utf-8") as f:
header = f.readline().strip().split()
expected = ["n", "amp_x", "amp_y", "amp_z",
"freq_x", "freq_y", "freq_z",
"phi_x", "phi_y", "phi_z", "period"]
if header != expected:
raise ValueError(
f"driver.txt 表头应为: {' '.join(expected)},实际为: {' '.join(header)}")
for line_no, line in enumerate(f, start=2):
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
parts = stripped.split()
if len(parts) != ncols:
raise ValueError(
f"{driver_path}:{line_no} 应有 {ncols} 列,实际为 {len(parts)}")
n = int(parts[0])
if n not in atom_index:
raise ValueError(f"{driver_path}:{line_no} 原子序号 {n} 不在 coord.txt 中")
amp = np.array([float(parts[1]), float(parts[2]), float(parts[3])],
dtype=np.float64)
freq = np.array([float(parts[4]), float(parts[5]), float(parts[6])],
dtype=np.float64)
phi_deg = np.array([float(parts[7]), float(parts[8]), float(parts[9])],
dtype=np.float64)
phi_rad = phi_deg * np.pi / 180.0
period_str = parts[10]
drivers.append({
"atom_index": atom_index[n],
"atom_id": n,
"amp": amp,
"freq": freq,
"phi": phi_rad,
"period_str": period_str,
"period_cycles": None if period_str == "all" else float(period_str),
# 在模拟中动态记录:冻结步数索引、冻结位置
"freeze_step": None,
"freeze_pos": None,
})
if not drivers:
print(f"[compute] 警告: driver.txt 中没有有效数据")
return None
print(f"[compute] 已加载驱动力: {len(drivers)} 条定义")
for d in drivers:
print(f" 原子 {d['atom_id']}: "
f"A=({d['amp'][0]},{d['amp'][1]},{d['amp'][2]}), "
f"f=({d['freq'][0]},{d['freq'][1]},{d['freq'][2]}), "
f"φ=({phi_deg[d['amp'].tolist().index(max(d['amp']))]}° 等), "
f"period={d['period_str']}")
return drivers
def apply_driving_force(x, y, z, vx, vy, vz, t, step, drivers, dt):
"""对受驱原子按驱动力函数覆盖位置/速度。
驱动力公式:pos = A * cos(2π f t + φ)
vel = -A * 2π f * sin(2π f t + φ)
"""
if drivers is None:
return x, y, z, vx, vy, vz
for d in drivers:
idx = d["atom_index"]
# 确定驱动力持续到哪一步
if d["period_cycles"] is not None:
max_freq = np.max(np.abs(d["freq"]))
if max_freq > 1e-12:
period_duration = d["period_cycles"] / max_freq
period_steps = int(period_duration / dt)
else:
period_steps = 0
if step > period_steps:
# 驱动力已结束:原子静止(保持最后位置,速度归零)
if d["freeze_pos"] is not None:
x[idx], y[idx], z[idx] = d["freeze_pos"]
vx[idx] = vy[idx] = vz[idx] = 0.0
continue
else:
period_steps = None # 全程驱动
# 当前驱动力下的位置 / 速度
t_vec = np.array([t, t, t], dtype=np.float64)
pos_drive = d["amp"] * np.cos(2.0 * np.pi * d["freq"] * t_vec + d["phi"])
vel_drive = -d["amp"] * 2.0 * np.pi * d["freq"] * np.sin(2.0 * np.pi * d["freq"] * t_vec + d["phi"])
x[idx] = pos_drive[0]
y[idx] = pos_drive[1]
z[idx] = pos_drive[2]
vx[idx] = vel_drive[0]
vy[idx] = vel_drive[1]
vz[idx] = vel_drive[2]
# 若周期有限,记录冻结位置(驱动力最后一帧的位置)
if period_steps is not None and step == period_steps:
d["freeze_pos"] = (float(pos_drive[0]), float(pos_drive[1]), float(pos_drive[2]))
return x, y, z, vx, vy, vz
def parse_gravity_vector(value): def parse_gravity_vector(value):
"""Parse gravity into a 3D acceleration vector. """Parse gravity into a 3D acceleration vector.
@@ -379,6 +512,7 @@ def run_from_config(config, out_dir=None):
global box_color_r, box_color_g, box_color_b global box_color_r, box_color_g, box_color_b
global warmup_steps, sample_start, sample_end global warmup_steps, sample_start, sample_end
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
global DRIVING_FORCE, DRIVER_DATA
box_a = float(config.get("box_a", 10.0)) box_a = float(config.get("box_a", 10.0))
raw_alpha = config.get("alpha", 0.2) raw_alpha = config.get("alpha", 0.2)
@@ -439,11 +573,22 @@ def run_from_config(config, out_dir=None):
# 力开关 # 力开关
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
global DRIVING_FORCE, DRIVER_DATA
GRAVITY_FIELD = int(config.get("gravity_field", 1)) GRAVITY_FIELD = int(config.get("gravity_field", 1))
GRAVITY_INTERACTION = int(config.get("gravity_interaction", 0)) GRAVITY_INTERACTION = int(config.get("gravity_interaction", 0))
ELASTIC_FORCE = int(config.get("elastic_force", 1)) ELASTIC_FORCE = int(config.get("elastic_force", 1))
DAMPING_FORCE = int(config.get("damping_force", 0)) DAMPING_FORCE = int(config.get("damping_force", 0))
GRAVITY_STRENGTH = float(config.get("gravity_strength", 1.0)) GRAVITY_STRENGTH = float(config.get("gravity_strength", 1.0))
DRIVING_FORCE = int(config.get("driving_force", 0))
# 加载驱动力定义
DRIVER_DATA = None
if DRIVING_FORCE:
driver_rel = str(config.get("driver_file", os.path.join("input", "driver.txt")))
driver_path = driver_rel
if out_dir is not None and not os.path.isabs(driver_rel):
driver_path = os.path.join(out_dir, driver_rel)
DRIVER_DATA = load_driver_file(driver_path, ATOM_IDS)
print(f"[compute] 使用算法: {METHOD}") print(f"[compute] 使用算法: {METHOD}")
print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键") print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键")
@@ -1062,11 +1207,14 @@ def run_simulation():
vy = ATOM_VELOCITIES[:, 1].copy() vy = ATOM_VELOCITIES[:, 1].copy()
vz = ATOM_VELOCITIES[:, 2].copy() vz = ATOM_VELOCITIES[:, 2].copy()
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz) x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
# 初始时刻驱动力(t=0 时原子 1 的位置由驱动力决定而非 coord.txt)
x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, 0.0, 0, DRIVER_DATA, DT)
if warmup_steps is not None and warmup_steps > 0: if warmup_steps is not None and warmup_steps > 0:
print(f"[compute] 预热阶段: 前 {warmup_steps} 步不记录") print(f"[compute] 预热阶段: 前 {warmup_steps} 步不记录")
for step in trange(warmup_steps, desc="[compute] 预热"): for step in trange(warmup_steps, desc="[compute] 预热"):
t = (step + 1) * DT t = (step + 1) * DT
x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, t, step, DRIVER_DATA, DT)
x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B) x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
x, y, z = wrap_position(x, y, z) x, y, z = wrap_position(x, y, z)
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz) x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
@@ -1086,6 +1234,10 @@ def run_simulation():
traj_vz = np.zeros((record_steps, n_atoms), dtype=np.float64) traj_vz = np.zeros((record_steps, n_atoms), dtype=np.float64)
for step in trange(record_steps, desc="[compute] 计算中"): for step in trange(record_steps, desc="[compute] 计算中"):
t = (step + (warmup_steps or 0)) * DT
# 先施加驱动力(受驱原子的位置覆盖初始/固定约束,为弹簧力提供正确参考)
x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, t, step, DRIVER_DATA, DT)
traj_x[step] = x traj_x[step] = x
traj_y[step] = y traj_y[step] = y
traj_z[step] = z traj_z[step] = z
+68 -41
View File
@@ -72,6 +72,9 @@ PLOT_ATOM_ROW = int(disp_data.get("plot_atom_row", 0))
PLOT_ATOM_ID = int(disp_data.get("plot_atom_id", ATOM_IDS[0])) PLOT_ATOM_ID = int(disp_data.get("plot_atom_id", ATOM_IDS[0]))
BOND_PAIRS = disp_data.get("bond_pairs", []) BOND_PAIRS = disp_data.get("bond_pairs", [])
# 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵)
USE_MARKER = int(disp_data.get("use_marker", 0))
if N_FRAMES <= 0: if N_FRAMES <= 0:
raise ValueError( raise ValueError(
"output/display.txt 中没有可播放的帧,请检查 sample_start/sample_end/NSTEP 配置。") "output/display.txt 中没有可播放的帧,请检查 sample_start/sample_end/NSTEP 配置。")
@@ -171,7 +174,10 @@ axes_group.append(scene.visuals.Text(text="y", color=(0.2, 1.0, 0.2, 1.0), font_
axes_group.append(scene.visuals.Text(text="z", color=(0.3, 0.6, 1.0, 1.0), font_size=18, axes_group.append(scene.visuals.Text(text="z", color=(0.3, 0.6, 1.0, 1.0), font_size=18,
pos=(0, 0, axis_length + 0.2), anchor_x="left", anchor_y="bottom", parent=view.scene)) pos=(0, 0, axis_length + 0.2), anchor_x="left", anchor_y="bottom", parent=view.scene))
# 所有小球(每个原子一个球,不同颜色) # ── 原子渲染 ──────────────────────────────────
# 两种渲染模式:
# Sphere = 独立网格球体(精细,适合少量原子)
# Marker = GPU 实例化点精灵(高效,适合大量原子)
TAB10_RGB = np.array([ TAB10_RGB = np.array([
[0.1216, 0.4667, 0.7059], # 蓝 [0.1216, 0.4667, 0.7059], # 蓝
[0.8902, 0.4667, 0.1137], # 橙 [0.8902, 0.4667, 0.1137], # 橙
@@ -184,9 +190,26 @@ TAB10_RGB = np.array([
[0.7373, 0.7412, 0.1333], # 黄绿 [0.7373, 0.7412, 0.1333], # 黄绿
[0.0902, 0.7451, 0.8118], # 青 [0.0902, 0.7451, 0.8118], # 青
]) ])
balls = [] # 每个原子的颜色(循环使用 tab10 色板)
atom_colors = np.zeros((N_ATOMS, 4), dtype=np.float32)
for i in range(N_ATOMS): for i in range(N_ATOMS):
r, g, b = TAB10_RGB[i % len(TAB10_RGB)] r, g, b = TAB10_RGB[i % len(TAB10_RGB)]
atom_colors[i] = [r, g, b, 1.0]
if USE_MARKER:
# ── Marker 模式:GPU 实例化,一次 draw call ──
marker_pos = np.zeros((N_ATOMS, 3), dtype=np.float32)
marker_sizes = np.array([float(ATOM_RADII[i]) * 40 for i in range(N_ATOMS)], dtype=np.float32)
balls = scene.visuals.Markers(parent=view.scene)
balls.set_data(
pos=marker_pos, face_color=atom_colors,
size=marker_sizes, symbol='disc', edge_width=0,
)
else:
# ── Sphere 模式:每个原子一个独立网格球体 ──
balls = []
for i in range(N_ATOMS):
r, g, b, _ = atom_colors[i]
s = scene.visuals.Sphere( s = scene.visuals.Sphere(
radius=float(ATOM_RADII[i]), method="latitude", radius=float(ATOM_RADII[i]), method="latitude",
color=(r, g, b, 1.0), edge_color=None, parent=view.scene) color=(r, g, b, 1.0), edge_color=None, parent=view.scene)
@@ -196,11 +219,12 @@ for i in range(N_ATOMS):
# 成键线(原子之间的连接) # 成键线(原子之间的连接)
if len(BOND_PAIRS) > 0: if len(BOND_PAIRS) > 0:
n_bonds = len(BOND_PAIRS) n_bonds = len(BOND_PAIRS)
bond_pos = np.zeros((n_bonds * 2, 3), dtype=np.float32) bond_pos_buffer = np.zeros((n_bonds * 2, 3), dtype=np.float32) # 预分配,后续原地修改
bond_lines = scene.visuals.Line( bond_lines = scene.visuals.Line(
pos=bond_pos, color=(0.7, 0.7, 0.7, 0.8), width=3, pos=bond_pos_buffer, color=(0.7, 0.7, 0.7, 0.8), width=3,
connect="segments", parent=view.scene) connect="segments", parent=view.scene)
else: else:
bond_pos_buffer = None
bond_lines = None bond_lines = None
# 六个面形成立方体边界(每个面独立透明度,alpha<=0 时隐藏该面) # 六个面形成立方体边界(每个面独立透明度,alpha<=0 时隐藏该面)
@@ -361,19 +385,10 @@ def handle_key_press(event):
def reset_camera_view(): def reset_camera_view():
global frame_idx global frame_idx
frame_idx = 0 frame_idx = 0
# 立即复位所有小球到第 0 帧 # 立即复位所有原子到第 0 帧
for i in range(N_ATOMS): _update_atom_positions(frame_idx)
balls[i].transform = STTransform(translate=(
float(DISP_ALL_X[frame_idx, i]),
float(DISP_ALL_Y[frame_idx, i]),
float(DISP_ALL_Z[frame_idx, i]),
))
if bond_lines is not None and len(BOND_PAIRS) > 0: if bond_lines is not None and len(BOND_PAIRS) > 0:
pos = np.zeros((len(BOND_PAIRS) * 2, 3), dtype=np.float32) _update_bond_positions(frame_idx)
for b_idx, (i, j) in enumerate(BOND_PAIRS):
pos[b_idx * 2] = [DISP_ALL_X[frame_idx, i], DISP_ALL_Y[frame_idx, i], DISP_ALL_Z[frame_idx, i]]
pos[b_idx * 2 + 1] = [DISP_ALL_X[frame_idx, j], DISP_ALL_Y[frame_idx, j], DISP_ALL_Z[frame_idx, j]]
bond_lines.set_data(pos=pos)
# 复位相机 # 复位相机
view.camera.distance = initial_camera["distance"] view.camera.distance = initial_camera["distance"]
view.camera.elevation = initial_camera["elevation"] view.camera.elevation = initial_camera["elevation"]
@@ -413,31 +428,51 @@ def handle_mouse_press(event):
axis.visible = axes_visible axis.visible = axes_visible
# ===========================================================================
# 位置/键线更新辅助函数(两种渲染模式共用)
# ===========================================================================
def _update_atom_positions(f_idx):
"""更新所有原子到第 f_idx 帧的位置。"""
if USE_MARKER:
for i in range(N_ATOMS):
marker_pos[i] = [DISP_ALL_X[f_idx, i], DISP_ALL_Y[f_idx, i], DISP_ALL_Z[f_idx, i]]
balls.set_data(pos=marker_pos)
else:
for i in range(N_ATOMS):
balls[i].transform = STTransform(translate=(
float(DISP_ALL_X[f_idx, i]),
float(DISP_ALL_Y[f_idx, i]),
float(DISP_ALL_Z[f_idx, i]),
))
def _update_bond_positions(f_idx):
"""原地更新键线顶点位置,避免重新分配内存。"""
for b_idx, (i, j) in enumerate(BOND_PAIRS):
b2 = b_idx * 2
bond_pos_buffer[b2] = [DISP_ALL_X[f_idx, i], DISP_ALL_Y[f_idx, i], DISP_ALL_Z[f_idx, i]]
bond_pos_buffer[b2 + 1] = [DISP_ALL_X[f_idx, j], DISP_ALL_Y[f_idx, j], DISP_ALL_Z[f_idx, j]]
bond_lines.set_data(pos=bond_pos_buffer)
# =========================================================================== # ===========================================================================
# 动画初始化 # 动画初始化
# =========================================================================== # ===========================================================================
frame_idx = 0 frame_idx = 0
# 初始帧:摆放所有小球并刷新 UI # 初始帧:摆放所有原子并刷新 UI
for i in range(N_ATOMS): _update_atom_positions(frame_idx)
balls[i].transform = STTransform(translate=(
float(DISP_ALL_X[frame_idx, i]),
float(DISP_ALL_Y[frame_idx, i]),
float(DISP_ALL_Z[frame_idx, i]),
))
# 初始帧:更新成键线
if bond_lines is not None and len(BOND_PAIRS) > 0: if bond_lines is not None and len(BOND_PAIRS) > 0:
pos = np.zeros((len(BOND_PAIRS) * 2, 3), dtype=np.float32) _update_bond_positions(frame_idx)
for b_idx, (i, j) in enumerate(BOND_PAIRS):
pos[b_idx * 2] = [DISP_ALL_X[frame_idx, i], DISP_ALL_Y[frame_idx, i], DISP_ALL_Z[frame_idx, i]]
pos[b_idx * 2 + 1] = [DISP_ALL_X[frame_idx, j], DISP_ALL_Y[frame_idx, j], DISP_ALL_Z[frame_idx, j]]
bond_lines.set_data(pos=pos)
reposition_camera_info() reposition_camera_info()
update_ball_info(frame_idx, update_ball_info(frame_idx,
float(DISP_X[frame_idx]), float(DISP_Y[frame_idx]), float(DISP_Z[frame_idx]), float(DISP_X[frame_idx]), float(DISP_Y[frame_idx]), float(DISP_Z[frame_idx]),
float(DISP_VX[frame_idx]), float(DISP_VY[frame_idx]), float(DISP_VZ[frame_idx])) float(DISP_VX[frame_idx]), float(DISP_VY[frame_idx]), float(DISP_VZ[frame_idx]))
mode_str = "Marker (GPU 实例化)" if USE_MARKER else "Sphere (独立网格)"
print(f"[draw] 加载 output/display.txt: {N_FRAMES} 帧, {N_ATOMS} 个原子, NT={NT}, DT={DT}, NSTEP={NSTEP}") print(f"[draw] 加载 output/display.txt: {N_FRAMES} 帧, {N_ATOMS} 个原子, NT={NT}, DT={DT}, NSTEP={NSTEP}")
print(f"[draw] 渲染方式: {mode_str}")
print(f"[draw] 绘图参数: ball_radius={ball_radius}, box_color=({box_color_r:.2f},{box_color_g:.2f},{box_color_b:.2f}), alpha={alpha_list}") print(f"[draw] 绘图参数: ball_radius={ball_radius}, box_color=({box_color_r:.2f},{box_color_g:.2f},{box_color_b:.2f}), alpha={alpha_list}")
@@ -448,20 +483,12 @@ def update(event):
global frame_idx global frame_idx
frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放 frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放
# 更新所有小球位置 # 更新所有原子位置(两种模式共用逻辑)
for i in range(N_ATOMS): _update_atom_positions(frame_idx)
x = float(DISP_ALL_X[frame_idx, i])
y = float(DISP_ALL_Y[frame_idx, i])
z = float(DISP_ALL_Z[frame_idx, i])
balls[i].transform = STTransform(translate=(x, y, z))
# 更新成键线 # 更新成键线(原地修改,无内存分配)
if bond_lines is not None and len(BOND_PAIRS) > 0: if bond_lines is not None and len(BOND_PAIRS) > 0:
pos = np.zeros((len(BOND_PAIRS) * 2, 3), dtype=np.float32) _update_bond_positions(frame_idx)
for b_idx, (i, j) in enumerate(BOND_PAIRS):
pos[b_idx * 2] = [DISP_ALL_X[frame_idx, i], DISP_ALL_Y[frame_idx, i], DISP_ALL_Z[frame_idx, i]]
pos[b_idx * 2 + 1] = [DISP_ALL_X[frame_idx, j], DISP_ALL_Y[frame_idx, j], DISP_ALL_Z[frame_idx, j]]
bond_lines.set_data(pos=pos)
# 信息面板显示 plot_atom 的数据 # 信息面板显示 plot_atom 的数据
x = float(DISP_X[frame_idx]) x = float(DISP_X[frame_idx])
+37 -14
View File
@@ -12,6 +12,7 @@ dynamics.py
import os import os
import sys import sys
import subprocess import subprocess
import time
import argparse import argparse
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
@@ -316,6 +317,8 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
"elastic_force": int(data.get("elastic_force", 1)), "elastic_force": int(data.get("elastic_force", 1)),
"damping_force": int(data.get("damping_force", 0)), "damping_force": int(data.get("damping_force", 0)),
"gravity_strength": float(data.get("gravity_strength", 1.0)), "gravity_strength": float(data.get("gravity_strength", 1.0)),
"driving_force": int(config.get("driving_force", 0)),
"use_marker": int(config.get("use_marker", 0)),
} }
save_display_txt(disp_data, str(runtime_base)) save_display_txt(disp_data, str(runtime_base))
print(f"[run] 抽帧完成: {sample_end - sample_start} 步 -> {n_frames}") print(f"[run] 抽帧完成: {sample_end - sample_start} 步 -> {n_frames}")
@@ -331,7 +334,7 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'DejaVu Sans'] plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False plt.rcParams['axes.unicode_minus'] = False
time = np.arange(NT) * DT time_arr = np.arange(NT) * DT
n_atoms = all_x.shape[1] n_atoms = all_x.shape[1]
atom_ids_list = data.get("atom_ids", np.arange(n_atoms) + 1) atom_ids_list = data.get("atom_ids", np.arange(n_atoms) + 1)
@@ -353,7 +356,7 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
for ax, data_arr, title in plot_configs: for ax, data_arr, title in plot_configs:
for i in range(n_atoms): for i in range(n_atoms):
atom_id = int(atom_ids_list[i]) atom_id = int(atom_ids_list[i])
ax.plot(time, data_arr[:, i], color=colors[i], linewidth=1.5, label=f"原子 {atom_id}") ax.plot(time_arr, data_arr[:, i], color=colors[i], linewidth=1.5, label=f"原子 {atom_id}")
ax.set_title(title) ax.set_title(title)
ax.set_xlabel("时间 (s)") ax.set_xlabel("时间 (s)")
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
@@ -423,7 +426,7 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
ax_e = axes[2, 0] ax_e = axes[2, 0]
for i in range(n_atoms): for i in range(n_atoms):
aid = int(atom_ids_list[i]) aid = int(atom_ids_list[i])
ax_e.plot(time, e_total[:, i], color=colors[i], linewidth=1.5, label=f"原子 {aid}") ax_e.plot(time_arr, e_total[:, i], color=colors[i], linewidth=1.5, label=f"原子 {aid}")
ax_e.set_title("各原子总能量") ax_e.set_title("各原子总能量")
ax_e.set_xlabel("时间 (s)") ax_e.set_xlabel("时间 (s)")
ax_e.set_ylabel("能量") ax_e.set_ylabel("能量")
@@ -432,13 +435,13 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
# ── 第 3 行右:系统总能量 ── # ── 第 3 行右:系统总能量 ──
ax_sys = axes[2, 1] ax_sys = axes[2, 1]
ax_sys.plot(time, ek_sys, 'b-', linewidth=1.5, label="系统动能") ax_sys.plot(time_arr, ek_sys, 'b-', linewidth=1.5, label="系统动能")
ax_sys.plot(time, ug_sys, 'g-', linewidth=1.5, label="均匀重力势能") ax_sys.plot(time_arr, ug_sys, 'g-', linewidth=1.5, label="均匀重力势能")
if elastic_force_enabled and bond_pairs is not None and len(bond_pairs) > 0: if elastic_force_enabled and bond_pairs is not None and len(bond_pairs) > 0:
ax_sys.plot(time, us_sys, color='orange', linewidth=1.5, label="系统弹性势能") ax_sys.plot(time_arr, us_sys, color='orange', linewidth=1.5, label="系统弹性势能")
if gravity_interaction_enabled: if gravity_interaction_enabled:
ax_sys.plot(time, ug_grav_sys, color='purple', linewidth=1.5, label="万有引力势能") ax_sys.plot(time_arr, ug_grav_sys, color='purple', linewidth=1.5, label="万有引力势能")
ax_sys.plot(time, e_sys, 'r--', linewidth=1.5, label="系统总能量") ax_sys.plot(time_arr, e_sys, 'r--', linewidth=1.5, label="系统总能量")
ax_sys.set_title("系统总能量") ax_sys.set_title("系统总能量")
ax_sys.set_xlabel("时间 (s)") ax_sys.set_xlabel("时间 (s)")
ax_sys.set_ylabel("能量") ax_sys.set_ylabel("能量")
@@ -461,19 +464,39 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
# 5. 自动播放动画(可选) # 5. 自动播放动画(可选)
if config.get("step_animation", 0): if config.get("step_animation", 0):
draw_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "draw.py") draw_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "draw.py")
if os.path.exists(draw_script): if not os.path.exists(draw_script):
print(f"[run] 未找到动画脚本: {draw_script}")
else:
# 检查 display.txt 是否存在(step_sample=0 时可能没有)
disp_path = os.path.join(output_dir_abs, "display.txt")
if not os.path.exists(disp_path):
print(f"[run] 错误: 找不到 {disp_path}")
print(f"[run] 启动动画需要先运行抽帧(step_sample: 1),或手动保留 output/display.txt")
else:
try: try:
print("[run] 正在启动 VisPy 3D 动画窗口…") print("[run] 正在启动 VisPy 3D 动画窗口…")
subprocess.Popen( ansi_log = os.path.join(output_dir_abs, "animation.log")
[sys.executable, draw_script], if sys.platform == "win32":
# Windows 上给子进程独立控制台,避免父进程退出时连带关闭
creation_flags = subprocess.CREATE_NEW_CONSOLE
else:
creation_flags = 0
proc = subprocess.Popen(
[sys.executable, draw_script, output_dir_abs],
cwd=runtime_base, cwd=runtime_base,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=open(ansi_log, "w", encoding="utf-8"),
creationflags=creation_flags,
) )
# 等待半秒检查子进程是否成功启动(未立即崩溃)
time.sleep(0.5)
if proc.poll() is not None:
print(f"[run] ⚠ 动画进程已退出,返回码={proc.returncode}")
print(f"[run] 请查看错误日志: {ansi_log}")
else:
print(f"[run] VisPy 动画窗口已启动(PID={proc.pid}")
except Exception as e: except Exception as e:
print(f"[run] 启动动画失败: {e}") print(f"[run] 启动动画失败: {e}")
else:
print(f"[run] 未找到动画脚本: {draw_script}")
else: else:
print("[run] 运行 python draw.py 查看动画。") print("[run] 运行 python draw.py 查看动画。")
+1
View File
@@ -0,0 +1 @@
本案例描述两个粒子同一个键连接,受到重力作用下的动力学过程。
+8
View File
@@ -29,6 +29,7 @@ box_a: 20.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³
coord_file: input/coord.txt coord_file: input/coord.txt
connection_file: input/connection.txt connection_file: input/connection.txt
bond_file: input/bond.txt bond_file: input/bond.txt
driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效)
# 绘图/动画展示的原子序号(对应 coord_file 第一列 n # 绘图/动画展示的原子序号(对应 coord_file 第一列 n
plot_atom: 1 plot_atom: 1
@@ -44,6 +45,7 @@ gravity_field: 1 # 均匀重力场 (G)
gravity_interaction: 0 # 原子间万有引力 gravity_interaction: 0 # 原子间万有引力
elastic_force: 1 # 弹簧键力 elastic_force: 1 # 弹簧键力
damping_force: 0 # 阻尼 (B) damping_force: 0 # 阻尼 (B)
driving_force: 0 # 驱动力(需 driver_file 定义)
# #
gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效) gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效)
@@ -77,6 +79,12 @@ sample_end: null # null 表示到末尾
# ── 渲染方式 ──────────────────────────────────
# 3D 动画中原子渲染方式:
# 0 = Sphere (网格球体,效果精细,原子数少时推荐)
# 1 = Marker (GPU 实例化点,原子数多时性能更佳)
use_marker: 0
# ── 显示参数 ────────────────────────────────── # ── 显示参数 ──────────────────────────────────
# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 # 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5] alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5]
+1
View File
@@ -0,0 +1 @@
本案例描述地球围绕太阳做椭圆运动
+1 -1
View File
@@ -1,3 +1,3 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.28 0 0 0 0 0 0 1 1 1 1 1 0.28 0 0 0 0 0 0 1 1 1
2 1 0.28 0 0 4 0 4 0 0 0 0 2 1 0.28 4 0 0 0 0 4 0 0 0
+8
View File
@@ -29,6 +29,7 @@ box_a: 20.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³
coord_file: input/coord.txt coord_file: input/coord.txt
connection_file: input/connection.txt connection_file: input/connection.txt
bond_file: input/bond.txt bond_file: input/bond.txt
driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效)
# 绘图/动画展示的原子序号(对应 coord_file 第一列 n # 绘图/动画展示的原子序号(对应 coord_file 第一列 n
plot_atom: 1 plot_atom: 1
@@ -44,6 +45,7 @@ gravity_field: 0 # 均匀重力场 (G)
gravity_interaction: 1 # 原子间万有引力 gravity_interaction: 1 # 原子间万有引力
elastic_force: 0 # 弹簧键力 elastic_force: 0 # 弹簧键力
damping_force: 0 # 阻尼 (B) damping_force: 0 # 阻尼 (B)
driving_force: 0 # 驱动力(需 driver_file 定义)
# #
gravity_strength: 100.0 # 万有引力强度(仅 gravity_interaction=1 时有效) gravity_strength: 100.0 # 万有引力强度(仅 gravity_interaction=1 时有效)
@@ -77,6 +79,12 @@ sample_end: null # null 表示到末尾
# ── 渲染方式 ──────────────────────────────────
# 3D 动画中原子渲染方式:
# 0 = Sphere (网格球体,效果精细,原子数少时推荐)
# 1 = Marker (GPU 实例化点,原子数多时性能更佳)
use_marker: 0
# ── 显示参数 ────────────────────────────────── # ── 显示参数 ──────────────────────────────────
# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 # 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5] alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5]
+1
View File
@@ -0,0 +1 @@
地球围绕太阳转,月球围绕地球转,失败案例
+2
View File
@@ -0,0 +1,2 @@
bond_name k rest_length
k1 100.0 2.0
+2
View File
@@ -0,0 +1,2 @@
n1 n2 bond_name
+4
View File
@@ -0,0 +1,4 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.28 0 0 0 0 0 0 1 1 1
2 1 0.28 4 0 0 0 0 4 0 0 0
3 0.1 0.18 5 0 0 0 0 6 0 0 0
+101
View File
@@ -0,0 +1,101 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/trajectory.txt
step_sample: 1 # 抽帧 → output/display.txt
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy
force_calc: 0 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
# python = Python 参考实现(compute.py)
# c = C 引擎 (engines/c/build/dynamics_c)
# cpp = C++ 引擎 (engines/cpp/build/dynamics_cpp)
# fortran = Fortran 引擎 (engines/fortran/build/dynamics_f90)
engine: python # 默认使用 Python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 20.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
# ── 初始构型 ──────────────────────────────────
# 坐标文件格式:
# 第一行:n mass radius x y z vx vy vz
# 后续行:原子序号 质量 半径 x y z vx vy vz
coord_file: input/coord.txt
connection_file: input/connection.txt
bond_file: input/bond.txt
driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效)
# 绘图/动画展示的原子序号(对应 coord_file 第一列 n
plot_atom: 1
# ── 物理参数 ──────────────────────────────────
# 三个方向分量分别对应 x, y, z
G: [0.0, 0.0, -9.8] # 重力场分量 (m/s²)
# B: [0.5, 0.5, 0.5] # 阻尼分量
B: [0.0, 0.0, 0.0] # 阻尼分量
# ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G)
gravity_interaction: 1 # 原子间万有引力
elastic_force: 0 # 弹簧键力
damping_force: 0 # 阻尼 (B)
driving_force: 0 # 驱动力(需 driver_file 定义)
#
gravity_strength: 100.0 # 万有引力强度(仅 gravity_interaction=1 时有效)
# ── 数值算法 ──────────────────────────────────
# 可选:
# explicit_euler 显式欧拉法
# implicit_euler 隐式欧拉法
# midpoint 中点法
# leapfrog 蛙跳法
method: leapfrog
# ── 步骤控制 ──────────────────────────────────
# 以下参数控制哪些步骤被执行和保存
# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态)
warmup_steps: 0 # 默认 0(立即开始记录)
# 总模拟时间(秒),程序自动计算 NT = T_total / DT
# 如果同时指定了 NT,以 NT 为准
T_total: 10.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 2
# ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起)
sample_end: null # null 表示到末尾
# ── 渲染方式 ──────────────────────────────────
# 3D 动画中原子渲染方式:
# 0 = Sphere (网格球体,效果精细,原子数少时推荐)
# 1 = Marker (GPU 实例化点,原子数多时性能更佳)
use_marker: 0
# ── 显示参数 ──────────────────────────────────
# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5]
# 小球颜色
# 小球半径从 coord_file 的 radius 列读取
ball_color_r: 0.90 # R 分量 (0~1)
ball_color_g: 0.20 # G 分量
ball_color_b: 0.20 # B 分量
# 盒子面颜色
box_color_r: 0.80
box_color_g: 0.80
box_color_b: 0.85
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case01.
This script keeps program and data separated:
- program: ../../dynamics.py
- input: ./input
- output: ./output
"""
from __future__ import annotations
import argparse
import importlib.util
from pathlib import Path
CASE_DIR = Path(__file__).resolve().parent
DYNAMICS_PATH = Path("..") / ".." / "dynamics.py"
INPUT_DIR = Path("input")
OUTPUT_DIR = Path("output")
CONFIG_FILE = INPUT_DIR / "input.txt"
def load_dynamics_module(module_path: Path):
spec = importlib.util.spec_from_file_location("dynamics_module", module_path)
if spec is None or spec.loader is None:
raise ImportError(f"无法加载 dynamics.py: {module_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def main():
parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case01")
parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图")
args = parser.parse_args()
dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve()
input_dir = (CASE_DIR / INPUT_DIR).resolve()
output_dir = (CASE_DIR / OUTPUT_DIR).resolve()
config_path = (CASE_DIR / CONFIG_FILE).resolve()
module = load_dynamics_module(dynamics_path)
module.run_case(
config_path=config_path,
runtime_base=CASE_DIR,
input_dir=input_dir,
output_dir=output_dir,
no_plot=args.no_plot,
)
if __name__ == "__main__":
main()
+15
View File
@@ -0,0 +1,15 @@
地球围绕太阳转,月球围绕地球转,成功案例
| 项目 | 数值 | 比例 |
| --------- | --------------------: | ----------------: |
| 太阳—地球平均距离 | (1.496\times 10^8) km | 约为地月距离的 **389 倍** |
| 地球—月球平均距离 | (3.844\times 10^5) km | 1 |
R日地? : R地?月 ≈ 389
| 天体 | 质量/kg | 与地球质量之比 |
| -- | --------------------: | -------: |
| 太阳 | (1.989\times 10^{30}) | (333000) |
| 地球 | (5.972\times 10^{24}) | (1) |
| 月球 | (7.35\times 10^{22}) | (0.0123) |
M太阳?:M地球?:M月球?≈27100000:81.3:1
+2
View File
@@ -0,0 +1,2 @@
bond_name k rest_length
k1 100.0 2.0
+2
View File
@@ -0,0 +1,2 @@
n1 n2 bond_name
+4
View File
@@ -0,0 +1,4 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.28 0 0 0 0 0 0 1 1 1
2 1 0.28 4 0 0 0 0 4 0 0 0
3 0.1 0.18 5 0 0 0 0 6 0 0 0
+101
View File
@@ -0,0 +1,101 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/trajectory.txt
step_sample: 1 # 抽帧 → output/display.txt
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy
force_calc: 0 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
# python = Python 参考实现(compute.py)
# c = C 引擎 (engines/c/build/dynamics_c)
# cpp = C++ 引擎 (engines/cpp/build/dynamics_cpp)
# fortran = Fortran 引擎 (engines/fortran/build/dynamics_f90)
engine: python # 默认使用 Python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 20.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
# ── 初始构型 ──────────────────────────────────
# 坐标文件格式:
# 第一行:n mass radius x y z vx vy vz
# 后续行:原子序号 质量 半径 x y z vx vy vz
coord_file: input/coord.txt
connection_file: input/connection.txt
bond_file: input/bond.txt
driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效)
# 绘图/动画展示的原子序号(对应 coord_file 第一列 n
plot_atom: 1
# ── 物理参数 ──────────────────────────────────
# 三个方向分量分别对应 x, y, z
G: [0.0, 0.0, -9.8] # 重力场分量 (m/s²)
# B: [0.5, 0.5, 0.5] # 阻尼分量
B: [0.0, 0.0, 0.0] # 阻尼分量
# ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G)
gravity_interaction: 1 # 原子间万有引力
elastic_force: 0 # 弹簧键力
damping_force: 0 # 阻尼 (B)
driving_force: 0 # 驱动力(需 driver_file 定义)
#
gravity_strength: 100.0 # 万有引力强度(仅 gravity_interaction=1 时有效)
# ── 数值算法 ──────────────────────────────────
# 可选:
# explicit_euler 显式欧拉法
# implicit_euler 隐式欧拉法
# midpoint 中点法
# leapfrog 蛙跳法
method: leapfrog
# ── 步骤控制 ──────────────────────────────────
# 以下参数控制哪些步骤被执行和保存
# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态)
warmup_steps: 0 # 默认 0(立即开始记录)
# 总模拟时间(秒),程序自动计算 NT = T_total / DT
# 如果同时指定了 NT,以 NT 为准
T_total: 10.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 2
# ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起)
sample_end: null # null 表示到末尾
# ── 渲染方式 ──────────────────────────────────
# 3D 动画中原子渲染方式:
# 0 = Sphere (网格球体,效果精细,原子数少时推荐)
# 1 = Marker (GPU 实例化点,原子数多时性能更佳)
use_marker: 0
# ── 显示参数 ──────────────────────────────────
# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5]
# 小球颜色
# 小球半径从 coord_file 的 radius 列读取
ball_color_r: 0.90 # R 分量 (0~1)
ball_color_g: 0.20 # G 分量
ball_color_b: 0.20 # B 分量
# 盒子面颜色
box_color_r: 0.80
box_color_g: 0.80
box_color_b: 0.85
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case01.
This script keeps program and data separated:
- program: ../../dynamics.py
- input: ./input
- output: ./output
"""
from __future__ import annotations
import argparse
import importlib.util
from pathlib import Path
CASE_DIR = Path(__file__).resolve().parent
DYNAMICS_PATH = Path("..") / ".." / "dynamics.py"
INPUT_DIR = Path("input")
OUTPUT_DIR = Path("output")
CONFIG_FILE = INPUT_DIR / "input.txt"
def load_dynamics_module(module_path: Path):
spec = importlib.util.spec_from_file_location("dynamics_module", module_path)
if spec is None or spec.loader is None:
raise ImportError(f"无法加载 dynamics.py: {module_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def main():
parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case01")
parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图")
args = parser.parse_args()
dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve()
input_dir = (CASE_DIR / INPUT_DIR).resolve()
output_dir = (CASE_DIR / OUTPUT_DIR).resolve()
config_path = (CASE_DIR / CONFIG_FILE).resolve()
module = load_dynamics_module(dynamics_path)
module.run_case(
config_path=config_path,
runtime_base=CASE_DIR,
input_dir=input_dir,
output_dir=output_dir,
no_plot=args.no_plot,
)
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
# case05: 一维原子链(60原子)
60个原子沿 x 轴排列,相邻原子用弹簧连接。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 60 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**只沿 z 方向**振动(fix_x=1, fix_y=1, fix_z=0 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 初始条件
- 所有原子初始速度为零
- **第1个原子**在 z 方向有初始位移,位移量 = 1
- 其余原子初始 z=0
## 动力学行为
初始时刻第1个原子的 z 位移拉伸了它和第2个原子之间的弹簧,产生一个沿 z 方向的扰动。该扰动将以波的形式沿一维原子链传播,在链的两端反射,形成驻波叠加。
由于无阻尼,系统总能量守恒。
+471
View File
@@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case05 — 一维原子链驱动力学模拟 | 物理原理 &amp; 使用文档</title>
<style>
:root {
--bg: #f8f9fa;
--card: #fff;
--text: #1a1a2e;
--accent: #2563eb;
--accent-light: #dbeafe;
--code-bg: #1e293b;
--code-text: #e2e8f0;
--border: #e2e8f0;
--muted: #64748b;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
}
/* ── Header ── */
.hero {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
color: #fff;
padding: 56px 24px 48px;
text-align: center;
}
.hero h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; }
.hero .subtitle {
margin-top: 10px;
font-size: 1.05rem;
opacity: 0.8;
}
.hero .badge {
display: inline-block;
margin-top: 14px;
padding: 4px 14px;
border-radius: 999px;
background: rgba(255,255,255,0.12);
font-size: 0.82rem;
}
/* ── Layout ── */
.container { max-width: 820px; margin: 0 auto; padding: 32px 20px; }
section { margin-bottom: 44px; }
h2 {
font-size: 1.35rem;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--accent);
display: inline-block;
}
h3 {
font-size: 1.05rem;
font-weight: 600;
margin: 20px 0 10px;
}
p, li { margin-bottom: 10px; }
ul, ol { padding-left: 22px; }
strong { color: var(--accent); }
/* ── Cards ── */
.card {
background: var(--card);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 16px;
border: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
/* ── Formula / Code blocks ── */
.formula {
background: var(--card);
border-left: 4px solid var(--accent);
padding: 14px 20px;
margin: 14px 0;
font-family: "Times New Roman", "STIX", serif;
font-size: 1.05rem;
overflow-x: auto;
border-radius: 0 8px 8px 0;
}
code {
background: var(--accent-light);
padding: 2px 7px;
border-radius: 4px;
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: 0.88em;
}
pre {
background: var(--code-bg);
color: var(--code-text);
padding: 16px 20px;
border-radius: 10px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.5;
margin: 14px 0;
}
pre .cm { color: #94a3b8; font-style: italic; } /* comment */
/* ── Table ── */
table {
width: 100%;
border-collapse: collapse;
margin: 14px 0;
font-size: 0.92rem;
}
th, td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th { background: var(--accent-light); font-weight: 600; }
/* ── TOC ── */
.toc { counter-reset: toc; }
.toc li { counter-increment: toc; list-style: none; margin-bottom: 6px; }
.toc li::before { content: counter(toc) ". "; font-weight: 600; color: var(--accent); }
.toc a { color: var(--accent); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
/* ── Flow diagram ── */
.flow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: center; margin: 16px 0; }
.flow-step {
background: var(--accent-light);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 8px 16px;
font-size: 0.88rem;
font-weight: 500;
}
.flow-arrow { color: var(--muted); font-size: 1.2rem; }
@media (max-width: 600px) {
.hero h1 { font-size: 1.5rem; }
.flow { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
}
</style>
</head>
<body>
<!-- ============================================================ -->
<!-- Header -->
<!-- ============================================================ -->
<header class="hero">
<h1>一维原子链驱动力学模拟</h1>
<p class="subtitle">60 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case05 · examples/case05</span>
</header>
<div class="container">
<!-- ============================================================ -->
<!-- TOC -->
<!-- ============================================================ -->
<section>
<h2>目录</h2>
<ol class="toc">
<li><a href="#physics">物理原理</a></li>
<li><a href="#algorithm">数值算法</a></li>
<li><a href="#driver">驱动力模型</a></li>
<li><a href="#usage">使用方法</a></li>
<li><a href="#params">参数参考</a></li>
<li><a href="#files">文件结构</a></li>
<li><a href="#troubleshoot">常见问题</a></li>
</ol>
</section>
<!-- ============================================================ -->
<!-- 1. Physics -->
<!-- ============================================================ -->
<section id="physics">
<h2>一、物理原理</h2>
<div class="card">
<h3>1.1 一维原子链</h3>
<p>60 个原子沿 <strong>x 轴</strong> 等间距排列,原子间距为 1。相邻原子之间用 <strong>理想弹簧</strong> 连接,弹簧的劲度系数 <em>k</em> = 1.0,原长 <em>L</em>₀ = 1.0(与原子间距一致,初始状态弹簧无拉伸)。</p>
<p>每个原子被限制在 <strong>z 方向</strong> 自由振动,x 和 y 方向锁定(<code>fix_x=1, fix_y=1, fix_z=0</code>)。</p>
</div>
<div class="card">
<h3>1.2 弹簧力(胡克定律)</h3>
<p>当原子 <em>i</em><em>j</em> 之间有弹簧连接时,原子 <em>i</em> 受到的弹簧力为:</p>
<div class="formula">
<strong>F</strong> = <em>k</em> · (<em>d</em> <em>L</em>₀) · <strong>u</strong><sub><em>ij</em></sub>
</div>
<p>其中 <em>d</em> = |<strong>r</strong><sub><em>j</em></sub> <strong>r</strong><sub><em>i</em></sub>| 为两原子间距离,<strong>u</strong><sub><em>ij</em></sub> 为从 <em>i</em> 指向 <em>j</em> 的单位向量。由于原子只在 z 方向振动,弹簧在 z 方向的分量是 <strong>几何非线性</strong> 的——对于小振幅近似,z 方向等效于一个三次方恢复力(FPU 型非线性)。</p>
</div>
<div class="card">
<h3>1.3 运动方程</h3>
<p>对于第 <em>i</em> 个自由原子(非受驱),牛顿第二定律给出:</p>
<div class="formula">
<em>m</em> · <strong>a</strong><sub><em>i</em></sub> = <strong>F</strong><sub><em>i</em></sub><sup>spring</sup> + <strong>F</strong><sub><em>i</em></sub><sup>driving</sup>
</div>
<p>本案例中 <strong>唯一的外力</strong> 来自驱动力(仅施加于原子 1)。无重力、无万有引力、无阻尼,系统总能量守恒。</p>
</div>
<div class="card">
<h3>1.4 波传播</h3>
<p>原子 1 的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的 <strong>横波</strong>。由于横向振动的几何非线性(弹簧大部分张力在 x 方向,z 方向的有效刚度远小于 1),波的传播速度较慢,且高阶频率成分会在链中产生复杂的非线性动力学行为(类似 FPU 回波现象)。</p>
</div>
</section>
<!-- ============================================================ -->
<!-- 2. Algorithm -->
<!-- ============================================================ -->
<section id="algorithm">
<h2>二、数值算法</h2>
<div class="card">
<h3>2.1 蛙跳法(Leapfrog / Velocity-Verlet</h3>
<p>采用能量守恒特性优异的 <strong>蛙跳法</strong>(二阶辛积分器),更新公式为:</p>
<div class="formula">
<strong>v</strong>(<em>t</em> + ½Δ<em>t</em>) = <strong>v</strong>(<em>t</em>) + ½ <strong>a</strong>(<em>t</em>) · Δ<em>t</em><br>
<strong>r</strong>(<em>t</em> + Δ<em>t</em>) = <strong>r</strong>(<em>t</em>) + <strong>v</strong>(<em>t</em> + ½Δ<em>t</em>) · Δ<em>t</em><br>
<strong>a</strong>(<em>t</em> + Δ<em>t</em>) = <strong>F</strong>(<strong>r</strong>(<em>t</em> + Δ<em>t</em>), <strong>v</strong>(<em>t</em> + ½Δ<em>t</em>)) / <em>m</em><br>
<strong>v</strong>(<em>t</em> + Δ<em>t</em>) = <strong>v</strong>(<em>t</em> + ½Δ<em>t</em>) + ½ <strong>a</strong>(<em>t</em> + Δ<em>t</em>) · Δ<em>t</em>
</div>
<p>蛙跳法在长时间模拟中能量漂移极小(本案例验证 <strong>&lt; 0.004%</strong>),适合无阻尼的保守系统。</p>
</div>
<div class="card">
<h3>2.2 时间步长与采样</h3>
<table>
<tr><th>参数</th><th></th><th>说明</th></tr>
<tr><td>DT</td><td>0.01 s</td><td>积分步长(远小于 1/ω ≈ 0.16 s,满足稳定性条件)</td></tr>
<tr><td>T_total</td><td>100 s</td><td>总模拟时间 → NT = 10000 步</td></tr>
<tr><td>NSTEP</td><td>50</td><td>每 NSTEP 步取一帧用于动画 → 200 帧</td></tr>
<tr><td>method</td><td>leapfrog</td><td>蛙跳法(Velocity-Verlet</td></tr>
</table>
</div>
<div class="card">
<h3>2.3 计算流程</h3>
<div class="flow">
<span class="flow-step">读入 coord.txt<br>connection.txt<br>bond.txt</span>
<span class="flow-arrow"></span>
<span class="flow-step">施加驱动力<br>(驱动原子 1</span>
<span class="flow-arrow"></span>
<span class="flow-step">记录轨迹</span>
<span class="flow-arrow"></span>
<span class="flow-step">蛙跳法<br>更新位置/速度</span>
<span class="flow-arrow"></span>
<span class="flow-step">固定约束<br>x, y 锁定)</span>
<span class="flow-arrow"></span>
<span class="flow-step" style="background:#fef3c7;border-color:#f59e0b;">循环<br>NT 次</span>
</div>
<p style="margin-top:12px;">注意:驱动力在 <strong>每次积分前</strong> 施加,确保受驱原子的位置正确传递给弹簧力计算。</p>
</div>
</section>
<!-- ============================================================ -->
<!-- 3. Driving Force -->
<!-- ============================================================ -->
<section id="driver">
<h2>三、驱动力模型</h2>
<div class="card">
<h3>3.1 定义文件</h3>
<p>驱动力由 <code>input/driver.txt</code> 定义,格式如下:</p>
<pre>n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
1 0 0 5 0 0 1 0 0 90 all</pre>
</div>
<div class="card">
<h3>3.2 数学公式</h3>
<p>受驱原子的位置由下式决定(<strong>完全替换</strong> coord.txt 中的初始坐标和固定约束):</p>
<div class="formula">
<strong>r</strong>(<em>t</em>) = <strong>A</strong> · cos(2π<em>f</em> · <em>t</em> + <strong>φ</strong>)
</div>
<p>速度由解析导数给出:</p>
<div class="formula">
<strong>v</strong>(<em>t</em>) = <strong>A</strong> · 2π<em>f</em> · sin(2π<em>f</em> · <em>t</em> + <strong>φ</strong>)
</div>
<p>其中 <strong>A</strong> = (amp_x, amp_y, amp_z)<strong>f</strong> = (freq_x, freq_y, freq_z) 为不同方向的驱动频率,<strong>φ</strong> = (phi_x, phi_y, phi_z) 为相位(<strong>角度制</strong>,代码自动转换为弧度)。</p>
</div>
<div class="card">
<h3>3.3 本案例驱动参数</h3>
<table>
<tr><th>参数</th><th></th><th>含义</th></tr>
<tr><td>amp_z</td><td>5.0</td><td>z 方向驱动振幅</td></tr>
<tr><td>freq_z</td><td>1.0 Hz</td><td>驱动频率(周期 1 s</td></tr>
<tr><td>phi_z</td><td>90°</td><td>驱动相位 → z(0) = 5·cos(90°) = 0</td></tr>
<tr><td>period</td><td>all</td><td>全程驱动,永不停止</td></tr>
</table>
<div class="formula">
<em>z</em>(<em>t</em>) = 5.0 · cos(2π · 1.0 · <em>t</em> + 90°)
</div>
</div>
<div class="card">
<h3>3.4 有限周期驱动</h3>
<p><code>period</code> 参数支持三种模式:</p>
<ul>
<li><strong>all</strong> — 全程驱动</li>
<li><strong>数值</strong> — 驱动指定周期数后 <strong>静止</strong>(冻结在最终位置,速度归零)。例如 <code>period: 1</code> 表示驱动 1 个完整周期后停止。</li>
</ul>
</div>
<div class="card">
<h3>3.5 驱动与固定约束的关系</h3>
<p>对于受驱原子(<code>driver.txt</code><code>n</code> 指定的原子),其在 <code>coord.txt</code> 中的初始坐标和 <code>fix_x/fix_y/fix_z</code> 约束被 <strong>完全忽略</strong>。原子的位置和速度完全由驱动力公式决定。</p>
</div>
</section>
<!-- ============================================================ -->
<!-- 4. Usage -->
<!-- ============================================================ -->
<section id="usage">
<h2>四、使用方法</h2>
<div class="card">
<h3>4.1 完整运行(模拟 + 动画)</h3>
<pre>cd examples/case05
python run_dynamics.py</pre>
<p>这步会依次执行:物理模拟 → 抽帧 → 打开 VisPy 3D 动画窗口。</p>
</div>
<div class="card">
<h3>4.2 仅查看已有结果</h3>
<p>如果已经跑完模拟且生成了 <code>output/display.txt</code>,可以通过修改 <code>input.txt</code> 跳过计算,只开动画:</p>
<pre>step_simulate: 0 # 跳过模拟
step_sample: 0 # 跳过抽帧
step_animation: 1 # 播放动画</pre>
<p>然后运行:<code>python run_dynamics.py</code></p>
</div>
<div class="card">
<h3>4.3 手动 3D 动画</h3>
<p>也可以单独启动 VisPy 窗口:</p>
<pre>python ../../draw.py output/</pre>
</div>
<div class="card">
<h3>4.4 强制重新计算</h3>
<p>修改参数后需要重新运行模拟时,设置:</p>
<pre>force_calc: 1 # 忽略缓存,强制重新计算</pre>
</div>
<div class="card">
<h3>4.5 动画交互</h3>
<table>
<tr><th>操作</th><th>效果</th></tr>
<tr><td>鼠标拖动</td><td>旋转视角</td></tr>
<tr><td>滚轮</td><td>缩放</td></tr>
<tr><td>左上角 <strong>reset</strong> 按钮</td><td>复位视角到初始位置</td></tr>
<tr><td>左上角 <strong>info</strong> 按钮</td><td>切换信息面板显示/隐藏</td></tr>
<tr><td>左上角 <strong>axes</strong> 按钮</td><td>切换坐标轴显示/隐藏</td></tr>
<tr><td>Q / E 键</td><td>画面绕视向旋转 -90° / +90°</td></tr>
</table>
</div>
</section>
<!-- ============================================================ -->
<!-- 5. Parameters -->
<!-- ============================================================ -->
<section id="params">
<h2>五、参数参考</h2>
<div class="card">
<h3>5.1 input.txt 关键参数</h3>
<table>
<tr><th>参数</th><th>默认值</th><th>说明</th></tr>
<tr><td>gravity_field</td><td>0</td><td>均匀重力场(已关闭)</td></tr>
<tr><td>gravity_interaction</td><td>0</td><td>原子间万有引力(已关闭)</td></tr>
<tr><td>elastic_force</td><td>1</td><td>弹簧键力(已开启)</td></tr>
<tr><td>damping_force</td><td>0</td><td>阻尼(已关闭)</td></tr>
<tr><td><strong>driving_force</strong></td><td><strong>1</strong></td><td>驱动力开关(1=开启,需 driver.txt</td></tr>
<tr><td>method</td><td>leapfrog</td><td>数值积分方法</td></tr>
<tr><td>DT</td><td>0.01</td><td>积分步长 (s)</td></tr>
<tr><td>T_total</td><td>100.0</td><td>总模拟时间 (s)</td></tr>
<tr><td>NSTEP</td><td>50</td><td>抽帧步数间隔</td></tr>
<tr><td>engine</td><td>python</td><td>计算引擎(python / c / cpp / fortran</td></tr>
<tr><td>use_marker</td><td>1</td><td>渲染模式(0=Sphere 网格, 1=Marker GPU 实例化)</td></tr>
</table>
</div>
<div class="card">
<h3>5.2 流程控制参数</h3>
<table>
<tr><th>参数</th><th>0</th><th>1</th></tr>
<tr><td>step_simulate</td><td>跳过模拟(加载已有轨迹)</td><td>运行物理模拟</td></tr>
<tr><td>step_sample</td><td>跳过抽帧</td><td>从轨迹抽取显示帧</td></tr>
<tr><td>step_plot</td><td>不生成图表</td><td>生成轨迹/能量图</td></tr>
<tr><td>step_animation</td><td>不启动动画</td><td>自动打开 VisPy 3D 窗口</td></tr>
<tr><td>force_calc</td><td>自动检测缓存</td><td>强制重新计算</td></tr>
</table>
</div>
</section>
<!-- ============================================================ -->
<!-- 6. File Structure -->
<!-- ============================================================ -->
<section id="files">
<h2>六、文件结构</h2>
<pre>case05/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(60 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(10000 步 × 60 原子)
│ ├── display.txt # 抽帧后的动画数据(200 帧 × 60 原子)
│ ├── dynamics.log # 计算日志
│ └── animation.log # 动画启动日志(闪退时排查用)
├── doc/
│ └── index.html # <span class="cm">本文档</span>
├── Readme.md # 案例简介
└── run_dynamics.py # 案例运行入口</pre>
</section>
<!-- ============================================================ -->
<!-- 7. Troubleshooting -->
<!-- ============================================================ -->
<section id="troubleshoot">
<h2>七、常见问题</h2>
<div class="card">
<h3>7.1 动画窗口闪退</h3>
<p>如果 VisPy 窗口一闪就消失,请检查:</p>
<ul>
<li><code>output/animation.log</code> 中是否有错误信息</li>
<li><code>output/display.txt</code> 是否存在(需先跑 <code>step_sample: 1</code></li>
</ul>
</div>
<div class="card">
<h3>7.2 原子不振动</h3>
<p>可能原因:</p>
<ul>
<li><strong>NSTEP 过大</strong>:抽帧间隔大于驱动周期的一半时,动画会丢失振动细节。建议 NSTEP ≤ 1/(freq · DT · 10)</li>
<li><strong>相位 φ 使采样点落在零值</strong>:试试 <code>phi_z: 0</code> 让原子在 t=0 处于振幅峰值</li>
<li>确认 <code>driving_force: 1</code><code>driver.txt</code> 中 amp_z 不为 0</li>
</ul>
</div>
<div class="card">
<h3>7.3 渲染性能慢</h3>
<p>原子数多时动画卡顿:</p>
<ul>
<li>设置 <code>use_marker: 1</code>(使用 GPU 实例化渲染替代独立网格球体)</li>
<li>增大 <code>NSTEP</code> 减少动画帧数</li>
</ul>
</div>
</section>
<hr style="border:none;border-top:1px solid var(--border);margin:40px 0;">
<footer style="text-align:center;color:var(--muted);font-size:0.85rem;margin-bottom:40px;">
Dynamics Simulation Framework &nbsp;·&nbsp; 生成于 2026-06-10
</footer>
</div>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
bond_name k rest_length
k1 50.0 1.0
+60
View File
@@ -0,0 +1,60 @@
n1 n2 bond_name
1 2 k1
2 3 k1
3 4 k1
4 5 k1
5 6 k1
6 7 k1
7 8 k1
8 9 k1
9 10 k1
10 11 k1
11 12 k1
12 13 k1
13 14 k1
14 15 k1
15 16 k1
16 17 k1
17 18 k1
18 19 k1
19 20 k1
20 21 k1
21 22 k1
22 23 k1
23 24 k1
24 25 k1
25 26 k1
26 27 k1
27 28 k1
28 29 k1
29 30 k1
30 31 k1
31 32 k1
32 33 k1
33 34 k1
34 35 k1
35 36 k1
36 37 k1
37 38 k1
38 39 k1
39 40 k1
40 41 k1
41 42 k1
42 43 k1
43 44 k1
44 45 k1
45 46 k1
46 47 k1
47 48 k1
48 49 k1
49 50 k1
50 51 k1
51 52 k1
52 53 k1
53 54 k1
54 55 k1
55 56 k1
56 57 k1
57 58 k1
58 59 k1
59 60 k1
+61
View File
@@ -0,0 +1,61 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 1 0 0 0 1 1 0
2 1 0.1 1 0 0 0 0 0 1 1 0
3 1 0.1 2 0 0 0 0 0 1 1 0
4 1 0.1 3 0 0 0 0 0 1 1 0
5 1 0.1 4 0 0 0 0 0 1 1 0
6 1 0.1 5 0 0 0 0 0 1 1 0
7 1 0.1 6 0 0 0 0 0 1 1 0
8 1 0.1 7 0 0 0 0 0 1 1 0
9 1 0.1 8 0 0 0 0 0 1 1 0
10 1 0.1 9 0 0 0 0 0 1 1 0
11 1 0.1 10 0 0 0 0 0 1 1 0
12 1 0.1 11 0 0 0 0 0 1 1 0
13 1 0.1 12 0 0 0 0 0 1 1 0
14 1 0.1 13 0 0 0 0 0 1 1 0
15 1 0.1 14 0 0 0 0 0 1 1 0
16 1 0.1 15 0 0 0 0 0 1 1 0
17 1 0.1 16 0 0 0 0 0 1 1 0
18 1 0.1 17 0 0 0 0 0 1 1 0
19 1 0.1 18 0 0 0 0 0 1 1 0
20 1 0.1 19 0 0 0 0 0 1 1 0
21 1 0.1 20 0 0 0 0 0 1 1 0
22 1 0.1 21 0 0 0 0 0 1 1 0
23 1 0.1 22 0 0 0 0 0 1 1 0
24 1 0.1 23 0 0 0 0 0 1 1 0
25 1 0.1 24 0 0 0 0 0 1 1 0
26 1 0.1 25 0 0 0 0 0 1 1 0
27 1 0.1 26 0 0 0 0 0 1 1 0
28 1 0.1 27 0 0 0 0 0 1 1 0
29 1 0.1 28 0 0 0 0 0 1 1 0
30 1 0.1 29 0 0 0 0 0 1 1 0
31 1 0.1 30 0 0 0 0 0 1 1 0
32 1 0.1 31 0 0 0 0 0 1 1 0
33 1 0.1 32 0 0 0 0 0 1 1 0
34 1 0.1 33 0 0 0 0 0 1 1 0
35 1 0.1 34 0 0 0 0 0 1 1 0
36 1 0.1 35 0 0 0 0 0 1 1 0
37 1 0.1 36 0 0 0 0 0 1 1 0
38 1 0.1 37 0 0 0 0 0 1 1 0
39 1 0.1 38 0 0 0 0 0 1 1 0
40 1 0.1 39 0 0 0 0 0 1 1 0
41 1 0.1 40 0 0 0 0 0 1 1 0
42 1 0.1 41 0 0 0 0 0 1 1 0
43 1 0.1 42 0 0 0 0 0 1 1 0
44 1 0.1 43 0 0 0 0 0 1 1 0
45 1 0.1 44 0 0 0 0 0 1 1 0
46 1 0.1 45 0 0 0 0 0 1 1 0
47 1 0.1 46 0 0 0 0 0 1 1 0
48 1 0.1 47 0 0 0 0 0 1 1 0
49 1 0.1 48 0 0 0 0 0 1 1 0
50 1 0.1 49 0 0 0 0 0 1 1 0
51 1 0.1 50 0 0 0 0 0 1 1 0
52 1 0.1 51 0 0 0 0 0 1 1 0
53 1 0.1 52 0 0 0 0 0 1 1 0
54 1 0.1 53 0 0 0 0 0 1 1 0
55 1 0.1 54 0 0 0 0 0 1 1 0
56 1 0.1 55 0 0 0 0 0 1 1 0
57 1 0.1 56 0 0 0 0 0 1 1 0
58 1 0.1 57 0 0 0 0 0 1 1 0
59 1 0.1 58 0 0 0 0 0 1 1 0
60 1 0.1 59 0 0 0 0 0 1 1 0
+2
View File
@@ -0,0 +1,2 @@
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
1 0 0 1 0 0 0.1 0 0 90 all
+96
View File
@@ -0,0 +1,96 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/trajectory.txt
step_sample: 1 # 抽帧 → output/display.txt
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy
force_calc: 0 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
engine: python # 默认使用 Python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 80.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
# ── 初始构型 ──────────────────────────────────
# 坐标文件格式:
# 第一行:n mass radius x y z vx vy vz fix_x fix_y fix_z
# 后续行:原子序号 质量 半径 x y z vx vy vz fix_x fix_y fix_z
coord_file: input/coord.txt
connection_file: input/connection.txt
bond_file: input/bond.txt
driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效)
# 绘图/动画展示的原子序号(对应 coord_file 第一列 n
plot_atom: 1
# ── 物理参数 ──────────────────────────────────
# 三个方向分量分别对应 x, y, z
G: [0.0, 0.0, 0.0] # 重力场分量 (m/s²)
B: [0.0, 0.0, 0.0] # 阻尼分量
# ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G)
gravity_interaction: 0 # 原子间万有引力
elastic_force: 1 # 弹簧键力
damping_force: 0 # 阻尼 (B)
driving_force: 1 # 驱动力(需 driver_file 定义)
#
gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效)
# ── 数值算法 ──────────────────────────────────
# 可选:
# explicit_euler 显式欧拉法
# implicit_euler 隐式欧拉法
# midpoint 中点法
# leapfrog 蛙跳法
method: leapfrog
# ── 步骤控制 ──────────────────────────────────
# 以下参数控制哪些步骤被执行和保存
# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态)
warmup_steps: 0 # 默认 0(立即开始记录)
# 总模拟时间(秒),程序自动计算 NT = T_total / DT
# 如果同时指定了 NT,以 NT 为准
T_total: 100.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 50
# ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起)
sample_end: null # null 表示到末尾
# ── 渲染方式 ──────────────────────────────────
# 3D 动画中原子渲染方式:
# 0 = Sphere (网格球体,效果精细,原子数少时推荐)
# 1 = Marker (GPU 实例化点,原子数多时性能更佳)
use_marker: 1
# ── 显示参数 ──────────────────────────────────
# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5]
# 小球颜色
# 小球半径从 coord_file 的 radius 列读取
ball_color_r: 0.20 # R 分量 (0~1)
ball_color_g: 0.60 # G 分量
ball_color_b: 0.90 # B 分量
# 盒子面颜色
box_color_r: 0.80
box_color_g: 0.80
box_color_b: 0.85
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case05 — 1D atomic chain.
This script keeps program and data separated:
- program: ../../dynamics.py
- input: ./input
- output: ./output
"""
from __future__ import annotations
import argparse
import importlib.util
from pathlib import Path
CASE_DIR = Path(__file__).resolve().parent
DYNAMICS_PATH = Path("..") / ".." / "dynamics.py"
INPUT_DIR = Path("input")
OUTPUT_DIR = Path("output")
CONFIG_FILE = INPUT_DIR / "input.txt"
def load_dynamics_module(module_path: Path):
spec = importlib.util.spec_from_file_location("dynamics_module", module_path)
if spec is None or spec.loader is None:
raise ImportError(f"无法加载 dynamics.py: {module_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def main():
parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case01")
parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图")
args = parser.parse_args()
dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve()
input_dir = (CASE_DIR / INPUT_DIR).resolve()
output_dir = (CASE_DIR / OUTPUT_DIR).resolve()
config_path = (CASE_DIR / CONFIG_FILE).resolve()
module = load_dynamics_module(dynamics_path)
module.run_case(
config_path=config_path,
runtime_base=CASE_DIR,
input_dir=input_dir,
output_dir=output_dir,
no_plot=args.no_plot,
)
if __name__ == "__main__":
main()