Compare commits

..

24 Commits

Author SHA1 Message Date
admin e415bf5ba5 fix: color_xrange 范围限定 d_max + color_driver + color 范围改为 [0,1] + bug 修复 2026-06-24 16:09:39 +08:00
admin b50c55d0a3 docs: examples/Readme.html v2.1 更新引擎 DLL 架构说明 2026-06-21 17:05:19 +08:00
admin 75d8aae1f2 fix: 修复 DLL 文件被 git CRLF 转换损坏
将 DLL 标记为 binary,禁止行尾转换
2026-06-21 08:17:08 +08:00
admin dd6fbdfccf chore: .gitattributes 标记 DLL 为二进制文件 2026-06-21 08:16:43 +08:00
admin eea62de479 fix: case05 atom 1 z=0(起点拉回链平面) 2026-06-21 08:13:23 +08:00
admin 1261398be2 fix: case05 启用 step_simulate=1 2026-06-21 08:11:34 +08:00
admin 369b840cf2 fix: Makefile Windows 检测兼容 Msys2/MINGW 环境
uname -s 在 Msys2 上返回 MINGW64_NT-* 而非 Windows,
添加 findstring 匹配确保静态链接选项生效
2026-06-21 05:58:37 +08:00
admin 7fb2b730e9 refactor: 源码迁至 engines/src/,删除旧的 engines/{c,cpp,fortran}/
- 删除 engines/{c,cpp,fortran}/ 目录(源码和 Makefile 已移至 src/)
- engines/src/{c,cpp,fortran}/: 清理 main.*/bak 等无用文件
- Makefile 改为直接编译 DLL 到 engines/release/
- .gitignore: 更新路径指向 engines/src/*/build/
- engine_dll.py: 更新注释中的编译命令路径
2026-06-21 05:42:29 +08:00
admin e371fa8db1 refactor: 移除旧子进程模式的 main.* 源文件,Makefile 只编译 DLL
- 删除 engines/c/main.c, engines/cpp/main.cpp, engines/fortran/main.f90
- Makefile 移除 all/exe 构建目标,默认只编译 DLL
2026-06-21 05:33:52 +08:00
admin 7468a2d458 chore: 清理引擎目录旧校准缓存 _calib_cache.json
旧子进程模式的预校准缓存,切换到 DLL 路径后已无代码引用
2026-06-21 05:28:24 +08:00
admin 76a26d888e chore: 清理旧的引擎参数/校准 JSON 文件
- c.json / fortran.json: 旧子进程模式参数,DLL 路径不再需要
- _calib_c.json / _calib_fortran.json: 旧预校准缓存,DLL 路径实时跟踪无需预跑
2026-06-21 05:25:31 +08:00
admin 8183cbc22e chore: .gitignore 去掉全局 *.dll 限制,release DLL 可自由提交 2026-06-21 05:21:22 +08:00
admin 41c454eeab refactor: 引擎只走 DLL 路径,release 目录清理 exe
- dynamics.py: 去掉 exe 子进程回退,只走 DLL 路径
- .gitignore: 白名单 engines/release/dynamics_*.dll
- engines/release/: 移除 dynamics_*.exe,新增 3 个引擎 DLL
2026-06-21 05:19:33 +08:00
admin 6c06c00f4b fix: Fortran engine use_marker/alpha header passthrough + case04 Moon orbit fix
- engines/fortran/main.f90:
  - 新增 use_marker 和 alpha_str 的读取、传递、写入 display.txt header
  - 新增 json_get_alpha 函数兼容 JSON 单值或 6 元数组
- engines/release/: 引擎校准缓存 & fortran.json 参数
- examples/case04/: 修正月球位置 (11.5→10.5) 和速度 (526→647),
  地月距缩至希尔半径以内 (0.5 < 1.0), 新增希尔半径验证说明
- examples/Readme.html: case04/case09 描述更新
2026-06-19 08:05:05 +08:00
admin c8d12e7f45 docs: case04 Readme.html 新增月球绕地轨道交互图
- 月球轨道图使用绿色配色,与地球轨道图(蓝色)区分
- 默认偏心率 e=0.0549(月球真实值)
- 标注半长轴 a / 半短轴 b / 偏心距 c
- 标注近地点/远地点距离
- 地球在左焦点,月球在椭圆轨道运行
- 独立滑块控制月球偏心率
2026-06-18 23:06:55 +08:00
admin 7078cbc744 fix: 轨道示意图太阳移至左焦点,默认 e=0.017(地球真实 0.0167)
- 椭圆中心改为 cx+f,使左焦点=cx 为太阳
- 偏心距标注从椭圆中心指向左焦点
- a/b 标注、近日点/远日点位置同步修正
- 滑块映射改为 e=value/1000(精度 0.001)
- 滑块默认值 17 → e=0.017 ≈ 地球真实 0.0167
2026-06-18 23:05:23 +08:00
admin d4ce23a07e feat: case04 以月球质量=1 为基准更新质量比和质量缩放
- coord.txt: 太阳 27000 / 地球 81 / 月球 1(月球=1 质量单位)
- 地球绕日轨道速度 v=520(v = sqrt(g_strength * M_sun / r))
- 月球绕地速度 v=526(地球速度 + 6 绕地附加速度)
- box_a: 20 → 30(容纳更大轨道半径)
- Readme.html 同步更新质量表和初始构型表
- 已知局限说明太阳质量缩至 1/1000 的原因
2026-06-18 23:03:36 +08:00
admin 84aed813df docs: case04 Readme.html 新增交互式轨道示意图
使用 HTML5 Canvas + JavaScript 绘制椭圆轨道图,
支持滑块调整偏心率 e(0~0.85),实时更新:
- 椭圆形状变化
- a/b/c 参数标注
- 近日点/远日点数值
- 速度矢量方向
- 底部信息面板
2026-06-18 23:01:04 +08:00
admin f6c468702f docs: case04 新增 Readme.html 含 MathJax 公式
涵盖天体质量/轨道参数/偏心率/开普勒定律/近日点速度
公式等全部天体力学内容,使用 MathJax 3 (SVG) 渲染。
2026-06-18 22:59:07 +08:00
admin 8b82d3c4e4 fix: leapfrog 模式缺少边界反弹导致粒子穿模 2026-06-18 14:28:56 +08:00
admin 80b7467421 fix: leapfrog 模式缺少边界反弹,粒子穿透地板
问题:leapfrog 模式的主循环直接调用 leapfrog_staggered_step,
未经过 apply_motion_update,因此 Limit_in_box(边界反弹)
从未被执行。粒子超出 z<-20 后被 wrap_position 回绕到顶端,
导致物理行为完全错误。

修复:
1. 在预热和记录两个循环中,leapfrog 路径后显式调用
   Limit_in_box(与 apply_motion_update 内的非 leapfrog 路径一致)
2. 移除 apply_motion_update 末尾重复的 Limit_in_box 调用
   (统一到主循环中执行)
2026-06-18 09:28:38 +08:00
admin 974216332c docs: 更新 examples 文档,移动引擎源码结构
- 更新 examples/Readme.md 覆盖全部 10 个案例
- 新增 examples/Readme.html 案例总览页面
- 引擎源码移至 engines/src/ 目录
- 编译产物统一至 engines/release/(静态编译)
- compute.py/engine_dll.py 路径同步更新
2026-06-17 16:18:19 +08:00
admin a16d2239a1 refactor: 引擎源码移至 src/,编译产物统一至 release/
- 源码: engines/c/ → engines/src/c/
- 源码: engines/cpp/ → engines/src/cpp/
- 源码: engines/fortran/ → engines/src/fortran/
- 编译产物: engines/release/ (静态编译)
  - dynamics_c.exe   (516KB, C 引擎)
  - dynamics_cpp.exe (3.3MB, C++ 引擎)
  - dynamics_f90.exe (988KB, Fortran 引擎)
- compute.py: engine_map 指向 release/
- compute.py: param.json 和校准缓存移至 release/
- engine_dll.py: DLL 搜索路径指向 release/
- release/ 目录纳入 git 版本管理,用户克隆后直接可用
2026-06-17 15:48:30 +08:00
admin 0e636e275d docs: 更新 examples/Readme.md 并新增 Readme.html
- 覆盖全部 10 个案例(原 Readme 只到 case06)
- 新增案例选择指南表格
- Readme.html 为深色主题独立 HTML 页面
  (含卡片布局、标签分类、代码高亮、响应式设计)
- 各案例详情对齐最新配置参数
2026-06-17 15:33:49 +08:00
152 changed files with 368550 additions and 3707 deletions
+8 -6
View File
@@ -1,7 +1,9 @@
# 全部使用 LF 换行,仓库内外一致,不随系统自动转换
* text eol=lf
# 引擎 DLL 是二进制文件,禁止行尾转换
engines/release/*.dll binary
# 二进制文件不转换
*.png binary
*.jpg binary
*.ico binary
# Python 源码使用 LF
*.py text eol=lf
# Makefile 使用 LF
Makefile text eol=lf
*.mk text eol=lf
+4 -6
View File
@@ -18,10 +18,9 @@ pip-wheel-metadata/
venv/
ENV/
# ── C / C++ 编译产物 ─────────────────────────────────────────
# Makefile 构建输出(engines/c/build/
engines/c/build/
engines/cpp/build/
# ── C / C++ / Fortran 编译产物 ────────────────────────
# 源码目录 engines/src/*/ 中可能产生的构建输出
engines/src/*/build/
# CMake 构建目录(根目录或自定义 build 目录)
CMakeCache.txt
@@ -42,12 +41,11 @@ build_*/
*.so
*.so.*
*.dylib
*.dll
# 可执行文件(保留源码,排除编译出的二进制)
# 注意:Windows 下 .exe 后缀的可执行文件
*.exe
# engines/c/Makefile 里指定了 build/ 目录,已由上面覆盖
# 可在 engines/src/*/ 中用 make dll 编译引擎 DLL
# 运行时生成的引擎参数文件(每次运行都会覆盖)
engines/*/param.json
+238 -37
View File
@@ -668,12 +668,20 @@ def load_driver_file(driver_path, atom_ids):
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']}")
if len(drivers) <= 20:
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']}")
else:
_first = drivers[0]
_last = drivers[-1]
print(f" 原子 {_first['atom_id']}~{_last['atom_id']}: "
f"A=({_first['amp'][0]},{_first['amp'][1]},{_first['amp'][2]}), "
f"f=({_first['freq'][0]},{_first['freq'][1]},{_first['freq'][2]}), "
f"period={_first['period_str']}{len(drivers)}")
return drivers
@@ -947,6 +955,183 @@ def run_from_config(config, out_dir=None):
return traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz
def run_engine_dll(engine, output_dir, config):
"""通过 DLL(ctypes)调用计算引擎,不经过文件 I/O,直接返回轨迹数组。
Args:
engine: 引擎名称 "c", "cpp", 或 "fortran"
output_dir: 输出目录(用于保存 display.npz
config: YAML 配置字典
Returns:
None(结果直接写入 output_dir/display.npz
Raises:
FileNotFoundError: DLL 尚未编译
RuntimeError: DLL 运算出错
"""
import sys as _sys
import datetime as _datetime
_eng_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "engines")
if _eng_dir not in _sys.path:
_sys.path.insert(0, _eng_dir)
from engine_dll import load_dll, run_dynamics_dll, is_dll_available
if not is_dll_available(engine):
raise FileNotFoundError(
f"DLL 未找到(引擎 {engine})。"
f"请先编译:cd engines/{engine} && make dll")
lib = load_dll(engine)
# ── 构造原子数据 ──────────────────────────────────────────
if ATOM_POSITIONS is None:
raise RuntimeError("run_engine_dll: 请先调用 load_parameters() 加载配置")
pos = np.asarray(ATOM_POSITIONS, dtype=np.float64) # (n, 3)
vel = np.asarray(ATOM_VELOCITIES, dtype=np.float64)
mass = np.asarray(ATOM_MASSES, dtype=np.float64)
fixed= np.asarray(ATOM_FIXED, dtype=np.int32) # (n, 3)
# ── 键数据 ────────────────────────────────────────────────
n_bonds = len(BOND_PAIRS) if BOND_PAIRS is not None else 0
bp = np.asarray(BOND_PAIRS, dtype=np.int32) if n_bonds else np.zeros((0,2), dtype=np.int32)
bk = np.asarray(BOND_STIFFNESS, dtype=np.float64) if n_bonds else np.zeros(0)
br0 = np.asarray(BOND_REST_LENGTHS,dtype=np.float64) if n_bonds else np.zeros(0)
# ── 驱动数据 ──────────────────────────────────────────────
drv_list = []
if int(config.get("driving_force", 0)) and DRIVER_DATA:
atom_id_to_local = {int(aid): i for i, aid in enumerate(ATOM_IDS)}
for d in DRIVER_DATA:
aid = int(d.get("atom_id", -1))
if aid not in atom_id_to_local:
continue
local_idx = atom_id_to_local[aid]
# d["amp"], d["freq"], d["phi"] are numpy arrays; d["phi"] is already in radians
amp = [float(v) for v in d["amp"]]
freq = [float(v) for v in d["freq"]]
phi = [float(v) for v in d["phi"]] # radians
# eq_pos is set by run_from_config; fall back to initial position
eq_pos = d.get("eq_pos")
eq = ([float(v) for v in eq_pos] if eq_pos is not None
else [float(pos[local_idx, 0]), float(pos[local_idx, 1]), float(pos[local_idx, 2])])
pc = d.get("period_cycles") # None → unlimited, float → finite
nc = float(pc) if pc is not None else 0.0
hp = 1 if nc > 0 else 0
drv_list.append({"local_idx": local_idx, "amp": amp, "freq": freq,
"phi": phi, "eq_pos": eq, "n_cycles": nc, "has_period": hp})
# ── 进度回调 ──────────────────────────────────────────────
total_steps = int(config["NT"]) - int(config.get("warmup_steps", 0))
try:
from tqdm import tqdm as _tqdm
_pbar = _tqdm(total=total_steps, desc=f"[compute] DLL {engine}",
unit="", bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]')
def _cb(step, total):
_pbar.n = step
_pbar.refresh()
except ImportError:
_pbar = None
_cb = None
_t0 = time.time()
try:
result = run_dynamics_dll(
lib, config,
pos, vel, mass, fixed,
bp, bk, br0,
drv_list, np.asarray(ATOM_IDS),
progress_cb=_cb,
)
finally:
if _pbar is not None:
_pbar.n = total_steps
_pbar.close()
elapsed = time.time() - _t0
n_frames, n_atoms = result["x"].shape
print(f"[compute] DLL 完成: {n_frames}{n_atoms} 原子 {elapsed:.3f} s")
# ── 构建 header 并保存 display.npz ────────────────────────
# 与 run_simulation 写入的 header 保持字段完全一致,
# 确保 draw.py / plot_wave.py 读到所有必要参数。
G_vec = parse_gravity_vector(config.get("G", [0, 0, 0]))
B_vec = parse_damping_vector(config.get("B", [0, 0, 0]))
record_steps_hdr = int(config["NT"]) - int(config.get("warmup_steps", 0))
header = {
"DT": str(config["DT"]),
"NSTEP": str(config.get("NSTEP", 1)),
"method": str(config.get("method", "leapfrog")),
"NT": str(config["NT"]),
"warmup_steps": str(config.get("warmup_steps", 0)),
"dynamic_steps": str(record_steps_hdr),
"T_total": str(int(config["NT"]) * float(config["DT"])),
"box_a": str(config.get("box_a", 300.0)),
"gravity_field": str(config.get("gravity_field", 0)),
"gravity_interaction": str(config.get("gravity_interaction", 0)),
"elastic_force": str(config.get("elastic_force", 1)),
"damping_force": str(config.get("damping_force", 0)),
"driving_force": str(config.get("driving_force", 0)),
"gravity_strength": str(config.get("gravity_strength", 1.0)),
"G": json.dumps(G_vec.tolist()),
"B": json.dumps(B_vec.tolist()),
"number_of_frames": str(n_frames),
"number_of_particles": str(n_atoms),
# draw.py 需要的渲染参数
"use_marker": str(use_marker),
"display_color": json.dumps(config.get("display_color",
{"x":[0,[1.0,0.0,0.0]],"y":[0,[0.0,1.0,0.0]],"z":[0,[0.0,0.0,1.0]],
"xy":[0,[1.0,1.0,0.0]],"yz":[0,[0.0,1.0,1.0]],"zx":[0,[1.0,0.0,1.0]],
"xyz":[1,[1.0,1.0,1.0]]})),
"ball_radius": str(config.get("ball_radius", float(ATOM_RADII[0]) if ATOM_RADII is not None else 0.5)),
"ball_color_r": str(config.get("ball_color_r", 0.9)),
"ball_color_g": str(config.get("ball_color_g", 0.2)),
"ball_color_b": str(config.get("ball_color_b", 0.2)),
"box_color_r": str(config.get("box_color_r", 0.8)),
"box_color_g": str(config.get("box_color_g", 0.8)),
"box_color_b": str(config.get("box_color_b", 0.85)),
"alpha": ",".join(str(a) for a in (alpha if isinstance(alpha, list) else [alpha])),
# draw.py / plot_wave.py 需要的原子、键数据
"atom_radii": ",".join(str(r) for r in ATOM_RADII),
"atom_masses": json.dumps([float(v) for v in ATOM_MASSES]),
"atom_positions": json.dumps(ATOM_POSITIONS.tolist()),
"bond_pairs": json.dumps(BOND_PAIRS.tolist() if BOND_PAIRS is not None else []),
"bond_stiffness": json.dumps(BOND_STIFFNESS.tolist() if BOND_STIFFNESS is not None else []),
"bond_rest_lengths": json.dumps(BOND_REST_LENGTHS.tolist() if BOND_REST_LENGTHS is not None else []),
# 边界(draw.py 用于场景缩放)
"X_MIN": str(-float(config.get("box_a", 300.0))),
"X_MAX": str( float(config.get("box_a", 300.0))),
"Y_MIN": str(-float(config.get("box_a", 300.0))),
"Y_MAX": str( float(config.get("box_a", 300.0))),
"Z_MIN": str(-float(config.get("box_a", 300.0))),
"Z_MAX": str( float(config.get("box_a", 300.0))),
# 相机参数
"camera_distance": str(camera_distance),
"camera_elevation": str(camera_elevation),
"camera_azimuth": str(camera_azimuth),
"camera_center_x": str(camera_center_x),
"camera_center_y": str(camera_center_y),
"camera_center_z": str(camera_center_z),
"camera_keyframes": str(camera_keyframes_raw),
}
if display_amp_str:
header["display_amp"] = display_amp_str
if camera_pos_x is not None:
header["camera_pos_x"] = str(camera_pos_x)
header["camera_pos_y"] = str(camera_pos_y)
header["camera_pos_z"] = str(camera_pos_z)
os.makedirs(output_dir, exist_ok=True)
npz_path = os.path.join(output_dir, "display.npz")
save_display_npz(
npz_path,
result["x"], result["y"], result["z"],
result["vx"], result["vy"], result["vz"],
np.asarray(ATOM_IDS),
header_fields=header,
)
print(f"[compute] display.npz 已生成: {npz_path}")
def run_engine(engine, input_dir, output_dir, config):
"""调用外部计算引擎(C/C++/Fortran),生成 trajectory.txt。
@@ -961,32 +1146,43 @@ def run_engine(engine, input_dir, output_dir, config):
script_dir = os.path.dirname(os.path.abspath(__file__))
system = platform.system().lower()
engine_map = {
"c": "engines/c/build/dynamics_c",
"cpp": "engines/cpp/build/dynamics_cpp",
"fortran": "engines/fortran/build/dynamics_f90",
"c": "engines/release/dynamics_c",
"cpp": "engines/release/dynamics_cpp",
"c++": "engines/release/dynamics_cpp",
"fortran": "engines/release/dynamics_f90",
"f90": "engines/release/dynamics_f90",
"python": None, # 特殊处理:用 sys.executable 调用 main.py
}
if engine not in engine_map:
raise ValueError(f"不支持的引擎: {engine},可选: {list(engine_map.keys())}")
raise ValueError(f"不支持的引擎: {engine},可选: c, cpp, fortran, python")
engine_rel = engine_map[engine]
engine_path = os.path.join(script_dir, engine_rel)
if engine == "python":
# Python 引擎:用当前解释器运行 engines/python/main.py
py_main = os.path.join(script_dir, "engines", "python", "main.py")
if not os.path.exists(py_main):
raise FileNotFoundError(f"Python 引擎脚本不存在: {py_main}")
found = py_main
engine_path = sys.executable
else:
engine_rel = engine_map[engine]
engine_path = os.path.join(script_dir, engine_rel)
# 自动检测可执行文件后缀和平台专用版本
candidates = [
engine_path, # 无后缀
engine_path + ".exe", # Windows .exe
engine_path + f"_{system}.exe", # 平台专用 (c_linux.exe, c_darwin.exe)
]
found = None
for p in candidates:
if os.path.exists(p):
found = p
break
if found is None:
raise FileNotFoundError(
f"引擎可执行文件不存在: 尝试了 {candidates}\n"
f"请先编译: cd engines/{engine} && make\n"
f"或安装交叉编译器后: cd engines/{engine} && make {system}")
# 自动检测可执行文件后缀和平台专用版本
candidates = [
engine_path,
engine_path + ".exe",
engine_path + f"_{system}.exe",
]
found = None
for p in candidates:
if os.path.exists(p):
found = p
break
if found is None:
raise FileNotFoundError(
f"引擎可执行文件不存在: 尝试了 {candidates}\n"
f"请先编译: cd engines/{engine} && make\n"
f"或安装交叉编译器后: cd engines/{engine} && make {system}")
# 构造 param.json(数值参数)
G = parse_gravity_vector(config.get("G", [0, 0, -9.8]))
@@ -1024,7 +1220,7 @@ def run_engine(engine, input_dir, output_dir, config):
"camera_center_y": float(config.get("camera_center_y", 0.0)),
"camera_center_z": float(config.get("camera_center_z", 0.0)),
}
param_path = os.path.join(script_dir, "engines", engine, "param.json")
param_path = os.path.join(script_dir, "engines", "release", f"{engine}.json")
os.makedirs(os.path.dirname(param_path), exist_ok=True)
with open(param_path, "w", encoding="utf-8") as f:
json.dump(param_json, f, indent=2)
@@ -1042,7 +1238,7 @@ def run_engine(engine, input_dir, output_dir, config):
n_atoms_calib = len(ATOM_IDS) if ATOM_IDS is not None else 0
# 尝试读取缓存;当 n_atoms 相同且 NT 在 50% 范围内时视为有效
_cache_path = os.path.join(script_dir, "engines", engine, "_calib_cache.json")
_cache_path = os.path.join(script_dir, "engines", "release", f"_calib_{engine}.json")
_step_time = None
try:
with open(_cache_path, encoding="utf-8") as _cf:
@@ -1057,10 +1253,10 @@ def run_engine(engine, input_dir, output_dir, config):
if _step_time is None:
_calib_param = dict(param_json)
_calib_param["NT"] = _calib_nt
_calib_path = os.path.join(script_dir, "engines", engine, "_calib.json")
_calib_path = os.path.join(script_dir, "engines", "release", f"_calib_{engine}.json")
with open(_calib_path, "w", encoding="utf-8") as _cf:
json.dump(_calib_param, _cf, indent=2)
_calib_outdir = os.path.join(script_dir, "engines", engine, "_calib_out")
_calib_outdir = os.path.join(script_dir, "engines", "release", f"_calib_{engine}_out")
os.makedirs(_calib_outdir, exist_ok=True)
_ct0 = time.time()
subprocess.run(
@@ -1089,8 +1285,11 @@ def run_engine(engine, input_dir, output_dir, config):
t_start = time.time()
t_start_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Python 引擎:[python, main.py, args];其他引擎:[exe, args]
_cmd = ([engine_path, found] if engine == "python" else [engine_path]) + \
[os.path.abspath(input_dir), os.path.abspath(output_dir), param_path]
_p = subprocess.Popen(
[engine_path, os.path.abspath(input_dir), os.path.abspath(output_dir), param_path],
_cmd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, encoding='utf-8', errors='replace')
_engine_lines = []
@@ -1611,10 +1810,6 @@ def apply_motion_update(x, y, z, vx, vy, vz, dt, m, g, b):
x, y, z, vx, vy, vz = Leapfrog_Method(x, y, z, vx, vy, vz, dt, m, g, b)
else:
raise ValueError(f"未知算法: {METHOD}")
x, vx = Limit_in_box(x, X_MIN, X_MAX, vx)
y, vy = Limit_in_box(y, Y_MIN, Y_MAX, vy)
z, vz = Limit_in_box(z, Z_MIN, Z_MAX, vz)
return x, y, z, vx, vy, vz
@@ -1683,6 +1878,9 @@ def run_simulation(save_trajectory=0):
x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
else:
x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
x, vx = Limit_in_box(x, X_MIN, X_MAX, vx)
y, vy = Limit_in_box(y, Y_MIN, Y_MAX, vy)
z, vz = Limit_in_box(z, Z_MIN, Z_MAX, vz)
x, y, z = wrap_position(x, y, z)
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
print(
@@ -1737,6 +1935,9 @@ def run_simulation(save_trajectory=0):
x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
else:
x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
x, vx = Limit_in_box(x, X_MIN, X_MAX, vx)
y, vy = Limit_in_box(y, Y_MIN, Y_MAX, vy)
z, vz = Limit_in_box(z, Z_MIN, Z_MAX, vz)
x, y, z = wrap_position(x, y, z)
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
+280 -60
View File
@@ -45,7 +45,65 @@ if os.path.exists(npz_path):
disp_data = compute.load_display_npz(npz_path)
else:
disp_data = compute.load_display_txt(disp_path)
h = disp_data["header_fields"]
# ── 从 input.txt 读取参数(替代 display.npz 中的 meta)──
try:
import yaml
_have_yaml = True
except ImportError:
_have_yaml = False
input_dir = os.path.join(os.path.dirname(output_dir), "input")
input_path = os.path.join(input_dir, "input.txt")
if _have_yaml and os.path.exists(input_path):
try:
with open(input_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
except Exception:
config = {}
else:
config = {}
# 兼容旧版:若 input.txt 不存在或解析失败,降级到 display.npz 的 meta
if not config:
config = disp_data.get("header_fields", {})
# ── 从 coord.txt 读取平衡位置 ─────────────────
# 注意: config 中的 *_file 可能带 "input/" 前缀,但 input_dir 已指向 input/ 目录
_coord_file_raw = config.get("coord_file", "coord.txt")
coord_path = os.path.join(input_dir, os.path.basename(_coord_file_raw))
if os.path.exists(coord_path):
try:
_ids, _masses, _radii, _pos, _vel, _fixed = compute.load_coord_file(coord_path)
EQ_POS = _pos # (n_atoms, 3)
ATOM_FIXED = _fixed # (n_atoms, 3)
except Exception:
EQ_POS = None
ATOM_FIXED = None
else:
EQ_POS = None
ATOM_FIXED = None
# ── 从 connection.txt 读取成键信息(若 meta 中没有)──
BOND_PAIRS = disp_data.get("bond_pairs", [])
if not BOND_PAIRS and 'bond_pairs' not in disp_data:
conn_path = os.path.join(input_dir, os.path.basename(config.get("connection_file", "connection.txt")))
if os.path.exists(conn_path):
try:
_pairs = []
with open(conn_path, "r", encoding="utf-8") as _f:
_f.readline() # skip header
for _line in _f:
_line = _line.strip()
if not _line or _line.startswith("#"):
continue
_parts = _line.split()
if len(_parts) >= 2:
_pairs.append([int(_parts[0]) - 1, int(_parts[1]) - 1])
BOND_PAIRS = np.array(_pairs, dtype=np.int32) if _pairs else []
except Exception:
BOND_PAIRS = []
BOND_PAIRS = BOND_PAIRS.tolist() if hasattr(BOND_PAIRS, 'tolist') else BOND_PAIRS
# 全原子帧数据
DISP_ALL_X = disp_data["frames_x"] # (n_frames, n_atoms)
@@ -66,64 +124,220 @@ DISP_VZ = DISP_ALL_VZ[:, 0]
N_FRAMES = DISP_ALL_X.shape[0]
NT = int(disp_data["n_total_frames"])
N_ATOMS = int(disp_data["n_total_particles"])
DT = float(h.get("DT", 0.001))
DT = float(config.get("DT", 0.001))
# 视觉位移放大:display_amp: [ax, ay, az],对偏离第0帧的位移乘以倍数
_damp_raw = h.get("display_amp", "")
if _damp_raw.strip():
# 视觉位移放大:display_amp: [ax, ay, az],对偏离平衡位置的位移乘以倍数
_damp_raw = config.get("display_amp", "")
if isinstance(_damp_raw, (list, tuple)):
_damp = np.array(_damp_raw, dtype=np.float64)
elif isinstance(_damp_raw, str) and _damp_raw.strip():
import ast as _ast
_damp_vals = _ast.literal_eval(_damp_raw.strip())
_damp = np.array(_damp_vals, dtype=np.float64)
if _damp.shape == (3,) and not np.allclose(_damp, 1.0):
_eq_x = DISP_ALL_X[0:1, :] # 第0帧作为平衡位置参考
_eq_y = DISP_ALL_Y[0:1, :]
_eq_z = DISP_ALL_Z[0:1, :]
_damp = np.array(_ast.literal_eval(_damp_raw.strip()), dtype=np.float64)
else:
_damp = None
if _damp is not None and _damp.shape == (3,) and not np.allclose(_damp, 1.0):
if EQ_POS is not None:
_eq_x = EQ_POS[None, :, 0] # coord.txt 平衡位置
_eq_y = EQ_POS[None, :, 1]
_eq_z = EQ_POS[None, :, 2]
else:
_eq_x = DISP_ALL_X[0:1, :] # 第0帧作为平衡位置参考
_eq_y = DISP_ALL_Y[0:1, :]
_eq_z = DISP_ALL_Z[0:1, :]
DISP_ALL_X = _eq_x + (DISP_ALL_X - _eq_x) * _damp[0]
DISP_ALL_Y = _eq_y + (DISP_ALL_Y - _eq_y) * _damp[1]
DISP_ALL_Z = _eq_z + (DISP_ALL_Z - _eq_z) * _damp[2]
NSTEP = int(h.get("NSTEP", 1))
# ── 位移颜色映射 ──────────────────────────────
# display_color: { mode: [enabled, [R,G,B]], ... }
# mode: x, y, z, xy, yz, zx, xyz
# enabled: 0=off, 1=on
# [R,G,B]: 最大位移时的颜色(渐变起点为白色)
# 例:xyz: [1, [1.0,1.0,1.0]] → 三方向合成位移 → 白色渐变
FRAME_COLORS = None
_DC_MODE = None
_DC_COLOR = None
_raw_dc = config.get("display_color", "")
if _raw_dc:
try:
dc = json.loads(_raw_dc) if isinstance(_raw_dc, str) else _raw_dc
if isinstance(dc, dict):
for mode, (enabled, color) in dc.items():
if int(enabled) and mode in ("x","y","z","xy","yz","zx","xyz"):
_DC_MODE = mode
_DC_COLOR = np.array(color, dtype=np.float32)
break
except Exception:
pass
if _DC_MODE is not None and _DC_COLOR is not None:
if EQ_POS is not None:
eq_x = EQ_POS[None, :, 0] # coord.txt 平衡位置
eq_y = EQ_POS[None, :, 1]
eq_z = EQ_POS[None, :, 2]
else:
eq_x = DISP_ALL_X[0:1, :] # 第0帧
eq_y = DISP_ALL_Y[0:1, :]
eq_z = DISP_ALL_Z[0:1, :]
dx = DISP_ALL_X - eq_x
dy = DISP_ALL_Y - eq_y
dz = DISP_ALL_Z - eq_z
if _DC_MODE == "x":
disp_mag = np.abs(dx)
elif _DC_MODE == "y":
disp_mag = np.abs(dy)
elif _DC_MODE == "z":
disp_mag = np.abs(dz)
elif _DC_MODE == "xy":
disp_mag = np.sqrt(dx**2 + dy**2)
elif _DC_MODE == "yz":
disp_mag = np.sqrt(dy**2 + dz**2)
elif _DC_MODE == "zx":
disp_mag = np.sqrt(dz**2 + dx**2)
else: # xyz
disp_mag = np.sqrt(dx**2 + dy**2 + dz**2)
# ── color_xrange: 限定归一化基准的原子范围 ───────────
# 格式: [['min'|'mid'|'max'|数值, 'min'|'mid'|'max'|数值], ...] 对应 x,y,z
# 在此范围内的原子计算最大位移 d_max,所有原子以此基准归一化着色
_cxr = config.get("color_xrange", None)
_d_max_source = disp_mag # 默认:所有原子
_range_label = "全部原子"
if _cxr and isinstance(_cxr, (list, tuple)) and len(_cxr) == 3:
try:
# 获取坐标极值
if EQ_POS is not None:
_eq_all = EQ_POS
else:
_eq_all = np.column_stack([
DISP_ALL_X[0], DISP_ALL_Y[0], DISP_ALL_Z[0]])
_cmin = _eq_all.min(axis=0)
_cmax = _eq_all.max(axis=0)
_cmid = (_cmin + _cmax) / 2
_key_map = {"min": _cmin, "mid": _cmid, "max": _cmax}
_range_lo = np.zeros(3, dtype=np.float64)
_range_hi = np.zeros(3, dtype=np.float64)
for _i in range(3):
_lo = _cxr[_i][0]
_hi = _cxr[_i][1]
_range_lo[_i] = _key_map[_lo][_i] if _lo in _key_map else float(_lo)
_range_hi[_i] = _key_map[_hi][_i] if _hi in _key_map else float(_hi)
_in_x = (_eq_all[:, 0] >= _range_lo[0]) & (_eq_all[:, 0] <= _range_hi[0])
_in_y = (_eq_all[:, 1] >= _range_lo[1]) & (_eq_all[:, 1] <= _range_hi[1])
_in_z = (_eq_all[:, 2] >= _range_lo[2]) & (_eq_all[:, 2] <= _range_hi[2])
_color_mask = _in_x & _in_y & _in_z
_n_in_range = _color_mask.sum()
if _n_in_range > 0:
_d_max_source = disp_mag[:, _color_mask] # 仅在范围内找最大位移
_range_label = (f"x[{_range_lo[0]:.1f},{_range_hi[0]:.1f}] "
f"y[{_range_lo[1]:.1f},{_range_hi[1]:.1f}] "
f"z[{_range_lo[2]:.1f},{_range_hi[2]:.1f}]")
except Exception as _e:
print(f"[draw] color_xrange 解析失败: {_e}")
# 用(范围限定的)最大位移归一化,所有原子统一着色
d_max = _d_max_source.max()
if d_max > 1e-12:
disp_norm = disp_mag / d_max
else:
disp_norm = disp_mag
FRAME_COLORS = np.ones((N_FRAMES, N_ATOMS, 4), dtype=np.float32)
t = disp_norm[..., None]
FRAME_COLORS[..., :3] = 1.0 + (_DC_COLOR - 1.0) * t
print(f"[draw] 位移颜色映射: mode={_DC_MODE}, color={_DC_COLOR.tolist()}, "
f"d_max={d_max:.4f}, 范围: {_range_label}")
# ── color_driver: 驱动原子 → 统一颜色 ──
_cd_raw = config.get("color_driver", None)
if _cd_raw is not None:
try:
_cd = np.array(_cd_raw, dtype=np.float32)
# 读取 driver.txt 获取驱动原子 ID
_driver_file = config.get("driver_file", "driver.txt")
_driver_path = os.path.join(input_dir, os.path.basename(_driver_file))
if os.path.exists(_driver_path):
_driver_ids = []
with open(_driver_path, "r", encoding="utf-8") as _df:
_df.readline() # skip header
for _line in _df:
_line = _line.strip()
if not _line or _line.startswith("#"):
continue
_parts = _line.split()
if _parts:
_driver_ids.append(int(_parts[0]) - 1) # 1-based → 0-based
if _driver_ids:
FRAME_COLORS[:, _driver_ids, :3] = _cd
print(f"[draw] color_driver: {len(_driver_ids)} 驱动原子 → RGB{_cd_raw}")
except Exception as _e:
print(f"[draw] color_driver 解析失败: {_e}")
# ── color_fix: 全固定原子 (fix_x=fix_y=fix_z=1) → 统一颜色 ──
_cf_raw = config.get("color_fix", None)
if _cf_raw is not None and ATOM_FIXED is not None:
try:
_cf = np.array(_cf_raw, dtype=np.float32)
_full_fixed = (ATOM_FIXED[:, 0] == 1) & (ATOM_FIXED[:, 1] == 1) & (ATOM_FIXED[:, 2] == 1)
_n_fix = _full_fixed.sum()
if _n_fix > 0:
FRAME_COLORS[:, _full_fixed, :3] = _cf
print(f"[draw] color_fix: {_n_fix} 全固定原子 → RGB{_cf_raw}")
except Exception as _e:
print(f"[draw] color_fix 解析失败: {_e}")
NSTEP = int(config.get("NSTEP", 1))
DISP_STEP = np.arange(N_FRAMES) * NSTEP
DISP_T = DISP_STEP * DT
# 原子信息
ATOM_IDS = disp_data["atom_ids"]
# 优先使用 per-atom 半径,否则用统一的 ball_radius
_raw_radii = h.get("atom_radii", "")
_raw_radii = config.get("atom_radii", "")
if _raw_radii.strip():
ATOM_RADII = np.array([float(x) for x in _raw_radii.split(",")])
else:
ATOM_RADII = np.full(N_ATOMS, float(h.get("ball_radius", 0.5)))
ATOM_RADII = np.full(N_ATOMS, float(config.get("ball_radius", 0.5)))
PLOT_ATOM_ROW = 0
PLOT_ATOM_ID = int(ATOM_IDS[0])
BOND_PAIRS = [] # display 格式不含成键信息,从原始数据加载
# 成键信息已在上面从 connection.txt 加载
# 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵)
USE_MARKER = int(h.get("use_marker", 0))
USE_MARKER = int(config.get("use_marker", 0))
if N_FRAMES <= 0:
raise ValueError(
"output/display.txt 中没有可播放的帧,请检查 sample_start/sample_end/NSTEP 配置。")
# 保留模拟边界常量(用于场景缩放、相机等),从 output/display.txt 中读取
X_MIN = float(h.get("X_MIN", -10)); X_MAX = float(h.get("X_MAX", 10))
Y_MIN = float(h.get("Y_MIN", -10)); Y_MAX = float(h.get("Y_MAX", 10))
Z_MIN = float(h.get("Z_MIN", -10)); Z_MAX = float(h.get("Z_MAX", 10))
raw_alpha = h.get("alpha", "0.2")
try:
alpha_list = [float(x) for x in raw_alpha.split(",")]
if len(alpha_list) != 6:
alpha_list = alpha_list * 6
except (ValueError, AttributeError):
alpha_list = [float(raw_alpha)] * 6
# 模拟边界(从 input.txt 的 box_a 计算)
_box_a = float(config.get("box_a", 10.0))
X_MIN = -_box_a; X_MAX = _box_a
Y_MIN = -_box_a; Y_MAX = _box_a
Z_MIN = -_box_a; Z_MAX = _box_a
raw_alpha = config.get("alpha", "0.2")
if isinstance(raw_alpha, (list, tuple)):
alpha_list = [float(x) for x in raw_alpha]
else:
try:
alpha_list = [float(x) for x in raw_alpha.split(",")]
except (ValueError, AttributeError):
alpha_list = [float(raw_alpha)] * 6
if len(alpha_list) != 6:
alpha_list = (alpha_list * 6)[:6]
# 绘图参数
ball_radius = float(h.get("ball_radius", 0.5))
ball_color_r = float(h.get("ball_color_r", 0.9))
ball_color_g = float(h.get("ball_color_g", 0.2))
ball_color_b = float(h.get("ball_color_b", 0.2))
box_color_r = float(h.get("box_color_r", 0.8))
box_color_g = float(h.get("box_color_g", 0.8))
box_color_b = float(h.get("box_color_b", 0.85))
ball_radius = float(config.get("ball_radius", 0.5))
ball_color_r = float(config.get("ball_color_r", 0.9))
ball_color_g = float(config.get("ball_color_g", 0.2))
ball_color_b = float(config.get("ball_color_b", 0.2))
box_color_r = float(config.get("box_color_r", 0.8))
box_color_g = float(config.get("box_color_g", 0.8))
box_color_b = float(config.get("box_color_b", 0.85))
# ===========================================================================
@@ -135,23 +349,23 @@ axis_length = 10.0
import math as _math_cam
_cx = float(h.get("camera_center_x", 0.0))
_cy = float(h.get("camera_center_y", 0.0))
_cz = float(h.get("camera_center_z", 0.0))
_cx = float(config.get("camera_center_x", 0.0))
_cy = float(config.get("camera_center_y", 0.0))
_cz = float(config.get("camera_center_z", 0.0))
# 若 input.txt 指定了摄像机自身坐标,则由坐标反推 distance/elevation/azimuth
if h.get("camera_pos_x") is not None:
_px = float(h["camera_pos_x"])
_py = float(h["camera_pos_y"])
_pz = float(h["camera_pos_z"])
if config.get("camera_pos_x") is not None:
_px = float(config.get("camera_pos_x"))
_py = float(config.get("camera_pos_y"))
_pz = float(config.get("camera_pos_z"))
_dx, _dy, _dz = _px - _cx, _py - _cy, _pz - _cz
_dist = _math_cam.sqrt(_dx*_dx + _dy*_dy + _dz*_dz) or 1.0
_elev = _math_cam.degrees(_math_cam.asin(max(-1.0, min(1.0, _dy / _dist))))
_azim = _math_cam.degrees(_math_cam.atan2(_dx, _dz))
else:
_dist = float(h.get("camera_distance", 40.0))
_elev = float(h.get("camera_elevation", 0))
_azim = float(h.get("camera_azimuth", 0))
_dist = float(config.get("camera_distance", 40.0))
_elev = float(config.get("camera_elevation", 0))
_azim = float(config.get("camera_azimuth", 0))
initial_camera = {
"distance": _dist,
@@ -212,11 +426,11 @@ axes_group.append(scene.visuals.Arrow(
parent=view.scene,
))
axes_group.append(scene.visuals.Text(text="x", color=(1.0, 0.2, 0.2, 1.0), font_size=18,
axes_group.append(scene.visuals.Text(text="x", color=(1.0, 0.2, 0.2, 1.0), font_size=14,
pos=(axis_length + 0.2, 0, 0), anchor_x="left", anchor_y="center", parent=view.scene))
axes_group.append(scene.visuals.Text(text="y", color=(0.2, 1.0, 0.2, 1.0), font_size=18,
axes_group.append(scene.visuals.Text(text="y", color=(0.2, 1.0, 0.2, 1.0), font_size=14,
pos=(0, axis_length + 0.2, 0), anchor_x="left", anchor_y="bottom", parent=view.scene))
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=14,
pos=(0, 0, axis_length + 0.2), anchor_x="left", anchor_y="bottom", parent=view.scene))
# ── 原子渲染 ──────────────────────────────────
@@ -235,11 +449,14 @@ TAB10_RGB = np.array([
[0.7373, 0.7412, 0.1333], # 黄绿
[0.0902, 0.7451, 0.8118], # 青
])
# 每个原子的颜色(循环使用 tab10 色板)
# 每个原子的颜色(循环使用 tab10 色板,或按位移着色
atom_colors = np.zeros((N_ATOMS, 4), dtype=np.float32)
for i in range(N_ATOMS):
r, g, b = TAB10_RGB[i % len(TAB10_RGB)]
atom_colors[i] = [r, g, b, 1.0]
if FRAME_COLORS is not None:
atom_colors[:] = FRAME_COLORS[0] # 初始帧颜色
else:
for i in range(N_ATOMS):
r, g, b = TAB10_RGB[i % len(TAB10_RGB)]
atom_colors[i] = [r, g, b, 1.0]
if USE_MARKER:
# ── Marker 模式:GPU 实例化,一次 draw call ──
@@ -295,12 +512,12 @@ for f_idx, (pos, direction) in enumerate(faces):
# 右上角:相机信息
camera_info = scene.visuals.Text(
text="", color="white", font_size=14,
text="", color="white", font_size=12,
pos=(0, 0), anchor_x="right", anchor_y="top", parent=canvas.scene)
# 左上角:小球信息
ball_info = scene.visuals.Text(
text="", color=(0.2, 1.0, 0.2, 1.0), font_size=18,
text="", color=(0.2, 1.0, 0.2, 1.0), font_size=14,
pos=(0, 0), anchor_x="left", anchor_y="top",
face="黑体", bold=True, parent=canvas.scene)
@@ -312,7 +529,7 @@ reset_button = scene.visuals.Rectangle(
radius=6, color=(0.18, 0.35, 0.65, 0.85),
border_color="white", parent=canvas.scene)
reset_button_label = scene.visuals.Text(
text="reset", color="white", font_size=16,
text="reset", color="white", font_size=13,
pos=(reset_btn_size[0] / 2 + 8, reset_btn_size[1] / 2 + 8),
anchor_x="center", anchor_y="center",
bold=True, parent=canvas.scene)
@@ -326,7 +543,7 @@ info_button = scene.visuals.Rectangle(
radius=6, color=(0.9, 0.3, 0.3, 0.9),
border_color="white", parent=canvas.scene)
info_button_label = scene.visuals.Text(
text="info", color="white", font_size=16,
text="info", color="white", font_size=13,
pos=(info_btn_size[0] / 2 + 8, info_btn_size[1] / 2 + 8),
anchor_x="center", anchor_y="center",
bold=True, parent=canvas.scene)
@@ -346,7 +563,7 @@ axes_button = scene.visuals.Rectangle(
radius=6, color=(0.3, 0.7, 0.3, 0.9),
border_color="white", parent=canvas.scene)
axes_button_label = scene.visuals.Text(
text="axes", color="white", font_size=16,
text="axes", color="white", font_size=13,
pos=(axes_btn_size[0] / 2 + 8, axes_btn_size[1] / 2 + 8),
anchor_x="center", anchor_y="center",
bold=True, parent=canvas.scene)
@@ -551,12 +768,15 @@ def handle_mouse_press(event):
# ===========================================================================
def _update_atom_positions(f_idx):
"""更新所有原子到第 f_idx 帧的位置。"""
"""更新所有原子到第 f_idx 帧的位置,必要时更新颜色"""
if USE_MARKER:
marker_pos[:, 0] = DISP_ALL_X[f_idx]
marker_pos[:, 1] = DISP_ALL_Y[f_idx]
marker_pos[:, 2] = DISP_ALL_Z[f_idx]
balls.set_data(pos=marker_pos)
if FRAME_COLORS is not None:
balls.set_data(pos=marker_pos, face_color=FRAME_COLORS[f_idx])
else:
balls.set_data(pos=marker_pos)
else:
for i in range(N_ATOMS):
balls[i].transform = STTransform(translate=(
@@ -630,10 +850,10 @@ def _load_move_camera_txt():
# 先试 move_camera.txt 直读,没有则用 display.txt 缓存
# header 中 camera_keyframes 为空字符串表示 move_camera=0(开关关闭),跳过文件加载
_camera_motion_enabled = bool(h.get("camera_keyframes", ""))
_camera_motion_enabled = bool(config.get("camera_keyframes", ""))
_CAM_MOTION = _load_move_camera_txt() if _camera_motion_enabled else None
if not _CAM_MOTION:
_CAM_MOTION = json.loads(h.get("camera_keyframes", "null")) if h.get("camera_keyframes") else None
_CAM_MOTION = json.loads(config.get("camera_keyframes", "null")) if config.get("camera_keyframes") else None
if _CAM_MOTION:
_cam_center = [0.0, 0.0, 0.0]
_cam_elev = initial_camera["elevation"]
+22 -14
View File
@@ -187,12 +187,11 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
disp_path = os.path.join(output_dir_abs, "display.txt")
# ── 自动缓存检测 ───────────────────────────────────────
# force_calc=1: 强制重新计算,忽略缓存
# force_calc=0: 尊重 step_simulate 设置,不自动覆盖
# force_calc=1: 强制重新计算,忽略缓存(仅在 step_simulate=1 时生效)
# force_calc=0: 尊重 step_simulate 设置
force_calc = int(config.get("force_calc", 0))
if force_calc:
if force_calc and config.get("step_simulate", 1):
print(f"[run] force_calc=1,跳过缓存,强制重新计算")
config["step_simulate"] = 1
config["step_sample"] = 1
elif config.get("step_simulate", 1):
# step_simulate=1 且 force_calc=0 → 按用户要求执行计算
@@ -225,8 +224,12 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
print(f"[run] 没有可用的缓存输出,但 step_simulate=0,将跳过模拟")
# 2. 运行物理模拟 → output/trajectory.txt
_engine_aliases = {"c++": "cpp", "f90": "fortran", "f": "fortran"}
engine = _engine_aliases.get(
str(config.get("engine", "python")).lower(),
str(config.get("engine", "python")).lower()
)
if config.get("step_simulate", 1):
engine = config.get("engine", "python")
total_steps = config["NT"]
record_steps = total_steps - (config.get("warmup_steps") or 0)
print(f"[run] 开始计算 总步数={total_steps} 记录步数={record_steps} DT={config['DT']}")
@@ -243,11 +246,14 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
config.pop("_skip_run", None)
input_dir_abs = str(input_dir_path.resolve())
output_dir_abs = str(output_dir_path.resolve())
# 外部引擎写完整 trajectory.txt,后续抽帧
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine(
engine, input_dir_abs, output_dir_abs, config)
if int(config.get("save_trajectory", 0)):
compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base))
# ── DLL 路径(无文件 I/O,直接输出 display.npz)──
from engines.engine_dll import is_dll_available
if not is_dll_available(engine):
raise FileNotFoundError(
f"DLL 未找到(引擎 {engine})。"
f"请先编译:cd engines/{engine} && make dll")
compute.run_engine_dll(engine, output_dir_abs, config)
_elapsed = _time.time() - _t0
print(f"[run] 引擎: {engine} 计算完成: {record_steps}{_elapsed:.3f} s")
@@ -345,11 +351,13 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
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")
# 检查 display.npz 或 display.txt 是否存在
disp_npz = os.path.join(output_dir_abs, "display.npz")
disp_txt = os.path.join(output_dir_abs, "display.txt")
disp_path = disp_npz if os.path.exists(disp_npz) else disp_txt
if not os.path.exists(disp_path):
print(f"[run] 错误: 找不到 {disp_path}")
print(f"[run] 启动动画需要先运行抽帧step_sample: 1),或手动保留 output/display.txt")
print(f"[run] 错误: 找不到 display.npz 或 display.txt")
print(f"[run] 启动动画需要先运行模拟step_simulate: 1")
else:
try:
print("[run] 正在启动 VisPy 3D 动画窗口…")
View File
-63
View File
@@ -1,63 +0,0 @@
# engines/c/Makefile
# 跨平台编译:make → 本地系统编译
# make linux → Linux 交叉编译(需 x86_64-linux-gnu-gcc
# make windows → Windows 交叉编译(需 x86_64-w64-mingw32-gcc
# make macos → macOS 交叉编译(需 osxcross 工具链)
CC = gcc
CFLAGS = -O3 -march=native -Wall -Wextra
LDFLAGS = -lm
SRCS = main.c
# 自动检测系统
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
# 目标文件名:统一使用 .exe 后缀(方便 Python 跨平台调用)
TARGET = build/dynamics_c.exe
# ── 本地编译 ─────────────────────────────────
.PHONY: all clean linux windows macos
all: $(TARGET)
$(TARGET): $(SRCS) | build
$(CC) $(CFLAGS) -o $@ $(SRCS) $(LDFLAGS)
@echo " === C engine built: $@ ==="
build:
mkdir -p build
# ── 交叉编译 ─────────────────────────────────
# Linux → Linux (x86_64)
linux: CROSS_PREFIX = x86_64-linux-gnu-
linux: CC = $(CROSS_PREFIX)gcc
linux: CFLAGS = -O3 -march=x86-64 -Wall -Wextra
linux: $(SRCS) | build
$(CC) $(CFLAGS) -o build/dynamics_c_linux.exe $(SRCS) $(LDFLAGS)
@echo " === Linux binary: build/dynamics_c_linux.exe ==="
# 任意平台 → Windows (x86_64)
# 需要安装 MinGW 交叉编译器:
# apt install mingw-w64 (Debian/Ubuntu)
# brew install mingw-w64 (macOS)
windows: CROSS_PREFIX = x86_64-w64-mingw32-
windows: CC = $(CROSS_PREFIX)gcc
windows: CFLAGS = -O3 -march=x86-64 -Wall -Wextra
windows: $(SRCS) | build
$(CC) $(CFLAGS) -o build/dynamics_c_win.exe $(SRCS) $(LDFLAGS)
@echo " === Windows binary: build/dynamics_c_win.exe ==="
# 任意平台 → macOS (x86_64)
# 需要安装 osxcross 工具链
macos: CROSS_PREFIX = x86_64-apple-darwin-
macos: CC = $(CROSS_PREFIX)gcc
macos: CFLAGS = -O3 -march=x86-64 -Wall -Wextra
macos: $(SRCS) | build
$(CC) $(CFLAGS) -o build/dynamics_c_mac.exe $(SRCS) $(LDFLAGS)
@echo " === macOS binary: build/dynamics_c_mac.exe ==="
# ── 编译所有平台 ──────────────────────────────
all-platforms: linux windows macos
clean:
rm -rf build *.o
-1114
View File
File diff suppressed because it is too large Load Diff
-1025
View File
File diff suppressed because it is too large Load Diff
+426
View File
@@ -0,0 +1,426 @@
"""
engines/engine_dll.py
---------------------
Python ctypes 包装器:加载 C/C++/Fortran 动态链接库并调用 run_dynamics()。
用法(由 compute.py 内部调用,不直接运行):
from engines.engine_dll import load_dll, run_dynamics_dll
dll = load_dll("c") # 自动查找 engines/c/build/dynamics_c.dll/.so/.dylib
arrays = run_dynamics_dll(dll, config, atom_data, bond_data, driver_data)
# arrays: dict with keys x, y, z, vx, vy, vz shape=(n_frames, n_atoms)
DLL 编译(C 版本):
Windows: gcc -O3 -shared -o engines/release/dynamics_c.dll engines/src/c/dynamics_lib.c -lm
Linux: gcc -O3 -shared -fPIC -o engines/release/dynamics_c.so engines/src/c/dynamics_lib.c -lm
macOS: gcc -O3 -dynamiclib -o engines/release/dynamics_c.dylib engines/src/c/dynamics_lib.c -lm
或用 make dll 一键编译:
cd engines/src/c && make dll
"""
import ctypes
import os
import platform
import numpy as np
# ── DLL 文件名后缀 ─────────────────────────────────────────────
_SUFFIX = {
"windows": ".dll",
"linux": ".so",
"darwin": ".dylib",
}
# ── method 字符串 → 整数 ID ────────────────────────────────────
_METHOD_ID = {
"explicit_euler": 0,
"euler": 0,
"implicit_euler": 1,
"midpoint": 2,
"leapfrog": 3,
}
_HERE = os.path.dirname(os.path.abspath(__file__))
_DLL_NAME = {
"c": "dynamics_c",
"cpp": "dynamics_cpp",
"c++": "dynamics_cpp",
"fortran": "dynamics_f90",
"f90": "dynamics_f90",
# "python" 引擎通过直接 import 调用,不使用 DLL
}
# 引擎名规范化:将别名统一为目录名
_ENGINE_DIR = {
"c": "c",
"cpp": "cpp",
"c++": "cpp",
"fortran": "fortran",
"f90": "fortran",
"python": "python",
}
def _dll_candidates(engine: str) -> list[str]:
"""返回 DLL 候选路径列表(按优先级)。"""
sys = platform.system().lower()
ext = _SUFFIX.get(sys, ".so")
eng_dir = _ENGINE_DIR.get(engine, engine)
name = _DLL_NAME.get(engine, f"dynamics_{engine}")
base = os.path.join(_HERE, "release", name)
return [
base + ext,
base + ".dll",
base + ".so",
base + ".dylib",
]
def load_dll(engine: str = "c"):
"""加载指定引擎。
- C/C++/Fortran: 返回 ctypes.CDLL 对象
- Python: 返回模块对象(直接 import,无需编译)
Args:
engine: "c", "cpp", "fortran", 或 "python"
Raises:
FileNotFoundError: DLL/模块文件不存在
"""
if _ENGINE_DIR.get(engine, engine) == "python":
import importlib.util, sys as _sys
mod_path = os.path.join(_HERE, "python", "dynamics_lib.py")
if not os.path.exists(mod_path):
raise FileNotFoundError(f"Python 引擎未找到: {mod_path}")
spec = importlib.util.spec_from_file_location(
"engines.python.dynamics_lib", mod_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod # 返回模块,不是 CDLL
for p in _dll_candidates(engine):
if os.path.exists(p):
lib = ctypes.CDLL(p)
_setup_prototype(lib)
return lib
raise FileNotFoundError(
f"DLL 未找到(引擎 {engine}),候选路径:\n" +
"\n".join(f" {p}" for p in _dll_candidates(engine)) +
f"\n请先编译:cd engines/{engine} && make dll"
)
def _setup_prototype(lib: ctypes.CDLL) -> None:
"""配置 run_dynamics 的参数类型和返回类型。"""
c_dbl_p = ctypes.POINTER(ctypes.c_double)
c_int_p = ctypes.POINTER(ctypes.c_int)
cb_type = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
lib.run_dynamics.restype = ctypes.c_int
lib.run_dynamics.argtypes = [
ctypes.c_int, # n_atoms
c_dbl_p, # pos_init [n_atoms*3]
c_dbl_p, # vel_init [n_atoms*3]
c_dbl_p, # masses [n_atoms]
c_int_p, # fixed [n_atoms*3]
ctypes.c_int, # n_bonds
c_int_p, # bond_pairs [n_bonds*2]
c_dbl_p, # bond_k [n_bonds]
c_dbl_p, # bond_r0 [n_bonds]
ctypes.c_double, # box_a
ctypes.c_double, # dt
ctypes.c_int, # NT
ctypes.c_int, # NSTEP
ctypes.c_int, # warmup_steps
ctypes.c_int, # method_id
ctypes.c_double, # Gx
ctypes.c_double, # Gy
ctypes.c_double, # Gz
ctypes.c_double, # Bx
ctypes.c_double, # By
ctypes.c_double, # Bz
ctypes.c_int, # gravity_field
ctypes.c_int, # elastic_force
ctypes.c_int, # damping_force
ctypes.c_double, # gravity_strength
ctypes.c_int, # n_drivers
c_int_p, # drv_idx [n_drivers]
c_dbl_p, # drv_amp [n_drivers*3]
c_dbl_p, # drv_freq [n_drivers*3]
c_dbl_p, # drv_phi [n_drivers*3]
c_dbl_p, # drv_eq [n_drivers*3]
c_dbl_p, # drv_ncycles [n_drivers]
c_int_p, # drv_has_period [n_drivers]
ctypes.c_int, # n_frames
c_dbl_p, # out_x
c_dbl_p, # out_y
c_dbl_p, # out_z
c_dbl_p, # out_vx
c_dbl_p, # out_vy
c_dbl_p, # out_vz
cb_type, # progress_cb (可为 NULL)
]
def _c_dbl(arr: np.ndarray):
"""返回 float64 C 连续数组的 ctypes 指针。"""
a = np.ascontiguousarray(arr, dtype=np.float64)
return a.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), a
def _c_int(arr: np.ndarray):
"""返回 int32 C 连续数组的 ctypes 指针。"""
a = np.ascontiguousarray(arr, dtype=np.int32)
return a.ctypes.data_as(ctypes.POINTER(ctypes.c_int)), a
def _is_python_module(lib) -> bool:
"""判断 lib 是否为 Python 引擎模块(而非 ctypes.CDLL)。"""
return not isinstance(lib, ctypes.CDLL)
def _run_dynamics_python(lib, config, atom_positions, atom_velocities, atom_masses,
atom_fixed, bond_pairs, bond_stiffness, bond_rest_lengths,
driver_data, atom_ids, progress_cb=None) -> dict:
"""调用 Python 引擎的 run_dynamics(),参数/返回值格式与 ctypes 版相同。"""
n = len(atom_masses)
NT = int(config["NT"])
NSTEP = int(config.get("NSTEP", 1))
warmup = int(config.get("warmup_steps", 0))
dt = float(config["DT"])
box_a = float(config.get("box_a", 300.0))
method_str = str(config.get("method", "leapfrog")).lower().replace(" ", "_")
method_id = _METHOD_ID.get(method_str, 3)
G = config.get("G", [0.0, 0.0, 0.0])
B = config.get("B", [0.0, 0.0, 0.0])
if hasattr(G, "tolist"): G = G.tolist()
if hasattr(B, "tolist"): B = B.tolist()
gravity_field = int(config.get("gravity_field", 0))
elastic_force = int(config.get("elastic_force", 1))
damping_force = int(config.get("damping_force", 0))
gravity_strength = float(config.get("gravity_strength", 1.0))
record_steps = NT - warmup
n_frames = max(1, record_steps // NSTEP)
nd = len(driver_data) if driver_data else 0
if nd > 0:
drv_idx = np.array([d["local_idx"] for d in driver_data], dtype=np.int64)
drv_amp = np.array([d["amp"] for d in driver_data], dtype=np.float64)
drv_freq = np.array([d["freq"] for d in driver_data], dtype=np.float64)
drv_phi = np.array([d["phi"] for d in driver_data], dtype=np.float64)
drv_eq = np.array([d["eq_pos"] for d in driver_data], dtype=np.float64)
drv_nc = np.array([d["n_cycles"] for d in driver_data], dtype=np.float64)
drv_hp = np.array([d["has_period"]for d in driver_data], dtype=np.int32)
else:
drv_idx = drv_amp = drv_freq = drv_phi = drv_eq = drv_nc = drv_hp = \
np.zeros(0, dtype=np.int64)
out_x, out_y, out_z, out_vx, out_vy, out_vz = lib.run_dynamics(
n_atoms=n,
pos_init=atom_positions,
vel_init=atom_velocities,
masses=atom_masses,
fixed=atom_fixed,
n_bonds=len(bond_pairs),
bond_pairs=bond_pairs,
bond_k=bond_stiffness,
bond_r0=bond_rest_lengths,
box_a=box_a,
dt=dt,
NT=NT,
NSTEP=NSTEP,
warmup_steps=warmup,
method_id=method_id,
Gx=float(G[0]), Gy=float(G[1]), Gz=float(G[2]),
Bx=float(B[0]), By=float(B[1]), Bz=float(B[2]),
gravity_field=gravity_field,
elastic_force=elastic_force,
damping_force=damping_force,
gravity_strength=gravity_strength,
n_drivers=nd,
drv_idx=drv_idx,
drv_amp=drv_amp,
drv_freq=drv_freq,
drv_phi=drv_phi,
drv_eq=drv_eq,
drv_ncycles=drv_nc,
drv_has_period=drv_hp,
n_frames=n_frames,
progress_cb=progress_cb,
)
shape = (n_frames, n)
t_arr = np.arange(n_frames) * NSTEP * dt + warmup * dt
return {
"x": out_x.reshape(shape), "y": out_y.reshape(shape),
"z": out_z.reshape(shape), "vx": out_vx.reshape(shape),
"vy": out_vy.reshape(shape), "vz": out_vz.reshape(shape),
"t": t_arr,
}
def run_dynamics_dll(
lib,
config: dict,
atom_positions: np.ndarray, # (n_atoms, 3)
atom_velocities: np.ndarray, # (n_atoms, 3)
atom_masses: np.ndarray, # (n_atoms,)
atom_fixed: np.ndarray, # (n_atoms, 3) int, 1=固定
bond_pairs: np.ndarray, # (n_bonds, 2) int 0-based 局部索引
bond_stiffness: np.ndarray, # (n_bonds,)
bond_rest_lengths: np.ndarray,# (n_bonds,)
driver_data: list, # 驱动原子列表(见下文)
atom_ids: np.ndarray, # (n_atoms,) 全局 atom id(用于驱动原子查找)
progress_cb=None,
) -> dict:
"""调用 DLL 的 run_dynamics(),返回抽帧后的轨迹数组。
driver_data 格式(每个元素对应一个驱动原子):
{
"atom_id": int, # 全局 atom id
"local_idx": int, # 在 atom_ids 数组中的位置(0-based
"amp": [ax, ay, az],
"freq": [fx, fy, fz],
"phi": [px, py, pz],
"eq_pos": [ex, ey, ez],
"n_cycles": float, # 0=不限
"has_period": int, # 0/1
}
返回:
{
"x": np.ndarray (n_frames, n_atoms),
"y": ...,
"z": ...,
"vx": ..., "vy": ..., "vz": ...,
"t": np.ndarray (n_frames,), # 时间轴
}
"""
# Python 引擎:直接调用模块函数,不走 ctypes
if _is_python_module(lib):
return _run_dynamics_python(
lib, config, atom_positions, atom_velocities, atom_masses,
atom_fixed, bond_pairs, bond_stiffness, bond_rest_lengths,
driver_data, atom_ids, progress_cb)
n = len(atom_masses)
NT = int(config["NT"])
NSTEP = int(config.get("NSTEP", 1))
warmup = int(config.get("warmup_steps", 0))
dt = float(config["DT"])
box_a = float(config.get("box_a", 300.0))
method_str = str(config.get("method", "leapfrog")).lower().replace(" ", "_")
method_id = _METHOD_ID.get(method_str, 3)
G = config.get("G", [0.0, 0.0, 0.0])
B = config.get("B", [0.0, 0.0, 0.0])
if hasattr(G, "tolist"): G = G.tolist()
if hasattr(B, "tolist"): B = B.tolist()
gravity_field = int(config.get("gravity_field", 0))
elastic_force = int(config.get("elastic_force", 1))
damping_force = int(config.get("damping_force", 0))
gravity_strength = float(config.get("gravity_strength", 1.0))
# ── 计算帧数 ──────────────────────────────────────────────
record_steps = NT - warmup
n_frames = max(1, record_steps // NSTEP)
# ── 驱动原子数据 ──────────────────────────────────────────
nd = len(driver_data) if driver_data else 0
if nd > 0:
drv_idx_arr = np.array([d["local_idx"] for d in driver_data], dtype=np.int32)
drv_amp_arr = np.array([d["amp"] for d in driver_data], dtype=np.float64).ravel()
drv_freq_arr = np.array([d["freq"] for d in driver_data], dtype=np.float64).ravel()
drv_phi_arr = np.array([d["phi"] for d in driver_data], dtype=np.float64).ravel()
drv_eq_arr = np.array([d["eq_pos"] for d in driver_data], dtype=np.float64).ravel()
drv_nc_arr = np.array([d["n_cycles"] for d in driver_data], dtype=np.float64)
drv_hp_arr = np.array([d["has_period"] for d in driver_data], dtype=np.int32)
else:
drv_idx_arr = np.zeros(1, dtype=np.int32)
drv_amp_arr = np.zeros(3, dtype=np.float64)
drv_freq_arr = np.zeros(3, dtype=np.float64)
drv_phi_arr = np.zeros(3, dtype=np.float64)
drv_eq_arr = np.zeros(3, dtype=np.float64)
drv_nc_arr = np.zeros(1, dtype=np.float64)
drv_hp_arr = np.zeros(1, dtype=np.int32)
# ── 输出缓冲区 ────────────────────────────────────────────
out_x = np.zeros(n_frames * n, dtype=np.float64)
out_y = np.zeros(n_frames * n, dtype=np.float64)
out_z = np.zeros(n_frames * n, dtype=np.float64)
out_vx = np.zeros(n_frames * n, dtype=np.float64)
out_vy = np.zeros(n_frames * n, dtype=np.float64)
out_vz = np.zeros(n_frames * n, dtype=np.float64)
# ── ctypes 指针(保留 arr 引用防止 GC) ──────────────────
p_pos, _pos = _c_dbl(atom_positions.ravel())
p_vel, _vel = _c_dbl(atom_velocities.ravel())
p_mass, _mass = _c_dbl(atom_masses)
p_fixed, _fixed = _c_int(atom_fixed.ravel())
p_bp, _bp = _c_int(bond_pairs.ravel() if len(bond_pairs) else np.zeros(2, dtype=np.int32))
p_bk, _bk = _c_dbl(bond_stiffness if len(bond_stiffness) else np.zeros(1))
p_br0, _br0 = _c_dbl(bond_rest_lengths if len(bond_rest_lengths) else np.zeros(1))
p_didx, _didx = _c_int(drv_idx_arr)
p_damp, _damp = _c_dbl(drv_amp_arr)
p_dfrq, _dfrq = _c_dbl(drv_freq_arr)
p_dphi, _dphi = _c_dbl(drv_phi_arr)
p_deq, _deq = _c_dbl(drv_eq_arr)
p_dnc, _dnc = _c_dbl(drv_nc_arr)
p_dhp, _dhp = _c_int(drv_hp_arr)
p_ox = out_x.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
p_oy = out_y.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
p_oz = out_z.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
p_ovx = out_vx.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
p_ovy = out_vy.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
p_ovz = out_vz.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
# 进度回调
cb_type = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
if progress_cb is not None:
cb = cb_type(progress_cb)
else:
cb = ctypes.cast(None, cb_type)
ret = lib.run_dynamics(
n,
p_pos, p_vel, p_mass, p_fixed,
len(bond_pairs), p_bp, p_bk, p_br0,
box_a, dt, NT, NSTEP, warmup, method_id,
float(G[0]), float(G[1]), float(G[2]),
float(B[0]), float(B[1]), float(B[2]),
gravity_field, elastic_force, damping_force, gravity_strength,
nd, p_didx, p_damp, p_dfrq, p_dphi, p_deq, p_dnc, p_dhp,
n_frames,
p_ox, p_oy, p_oz, p_ovx, p_ovy, p_ovz,
cb,
)
if ret != 0:
raise RuntimeError(f"run_dynamics() returned error code {ret}")
shape = (n_frames, n)
t_arr = np.arange(n_frames) * NSTEP * dt + warmup * dt
return {
"x": out_x.reshape(shape),
"y": out_y.reshape(shape),
"z": out_z.reshape(shape),
"vx": out_vx.reshape(shape),
"vy": out_vy.reshape(shape),
"vz": out_vz.reshape(shape),
"t": t_arr,
}
def is_dll_available(engine: str = "c") -> bool:
"""检查指定引擎是否可用(DLL 已编译或 Python 模块存在)。"""
if _ENGINE_DIR.get(engine, engine) == "python":
return os.path.exists(os.path.join(_HERE, "python", "dynamics_lib.py"))
return any(os.path.exists(p) for p in _dll_candidates(engine))
File diff suppressed because it is too large Load Diff
View File
+404
View File
@@ -0,0 +1,404 @@
"""
engines/python/dynamics_lib.py
-------------------------------
NumPy 计算引擎无文件 I/O所有数据以 NumPy 数组传入
结果作为 NumPy 数组返回
接口与 C/C++/Fortran DLL run_dynamics() 完全一致
算法与 compute.py run_simulation() 保持一致
用法 engine_dll.py 内部调用
from engines.python.dynamics_lib import run_dynamics
out_x, out_y, out_z, out_vx, out_vy, out_vz = run_dynamics(...)
"""
import numpy as np
TWO_PI = 2.0 * np.pi
# ── method_id 映射 ──────────────────────────────────────────
# 0=euler 1=implicit_euler 2=midpoint 3=leapfrog
# ── 保守加速度(弹簧键 + 均匀重力场,不含阻尼)──────────────
def _accel_conservative(x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
bond_pairs, bond_k, bond_r0):
ax = np.full_like(x, Gx) if gravity_field else np.zeros_like(x)
ay = np.full_like(y, Gy) if gravity_field else np.zeros_like(y)
az = np.full_like(z, Gz) if gravity_field else np.zeros_like(z)
if elastic_force and len(bond_pairs) > 0:
i1 = bond_pairs[:, 0]
i2 = bond_pairs[:, 1]
dx = x[i2] - x[i1]
dy = y[i2] - y[i1]
dz = z[i2] - z[i1]
dist = np.sqrt(dx*dx + dy*dy + dz*dz)
valid = dist > 1e-12
fac = np.where(valid, bond_k * (dist - bond_r0) / dist, 0.0)
fx = fac * dx
fy = fac * dy
fz_b = fac * dz
np.add.at(ax, i1, fx / m[i1]); np.add.at(ax, i2, -fx / m[i2])
np.add.at(ay, i1, fy / m[i1]); np.add.at(ay, i2, -fy / m[i2])
np.add.at(az, i1, fz_b / m[i1]); np.add.at(az, i2, -fz_b / m[i2])
return ax, ay, az
# ── 完整加速度(含阻尼)──────────────────────────────────────
def _accel_full(x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0):
ax, ay, az = _accel_conservative(x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
bond_pairs, bond_k, bond_r0)
if damping_force:
ax -= Bx * vx / m
ay -= By * vy / m
az -= Bz * vz / m
return ax, ay, az
# ── 蛙跳法(半隐式阻尼,与 compute.py leapfrog_staggered_step 一致)─
def _leapfrog_step(x, y, z, vx, vy, vz, fixed, m,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt):
ax, ay, az = _accel_conservative(x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
bond_pairs, bond_k, bond_r0)
has_damp = damping_force and (Bx != 0.0 or By != 0.0 or Bz != 0.0)
if has_damp:
alpha_x = Bx * dt / (2.0 * m)
alpha_y = By * dt / (2.0 * m)
alpha_z = Bz * dt / (2.0 * m)
vx_new = (vx * (1.0 - alpha_x) + ax * dt) / (1.0 + alpha_x)
vy_new = (vy * (1.0 - alpha_y) + ay * dt) / (1.0 + alpha_y)
vz_new = (vz * (1.0 - alpha_z) + az * dt) / (1.0 + alpha_z)
else:
vx_new = vx + ax * dt
vy_new = vy + ay * dt
vz_new = vz + az * dt
# 全固定原子保持不变
all_fixed = np.all(fixed, axis=1)
vx_new = np.where(all_fixed, vx, vx_new)
vy_new = np.where(all_fixed, vy, vy_new)
vz_new = np.where(all_fixed, vz, vz_new)
x_new = x + vx_new * dt
y_new = y + vy_new * dt
z_new = z + vz_new * dt
return x_new, y_new, z_new, vx_new, vy_new, vz_new
# ── 显式欧拉法 ───────────────────────────────────────────────
def _euler_step(x, y, z, vx, vy, vz, fixed, m,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt):
ax, ay, az = _accel_full(x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0)
all_fixed = np.all(fixed, axis=1)
mask = ~all_fixed
x_new = np.where(mask, x + vx * dt, x)
y_new = np.where(mask, y + vy * dt, y)
z_new = np.where(mask, z + vz * dt, z)
vx_new = np.where(mask, vx + ax * dt, vx)
vy_new = np.where(mask, vy + ay * dt, vy)
vz_new = np.where(mask, vz + az * dt, vz)
return x_new, y_new, z_new, vx_new, vy_new, vz_new
# ── 隐式欧拉法(与 compute.py Implicit_Euler_Method 一致)──────
def _implicit_euler_step(x, y, z, vx, vy, vz, fixed, m,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt):
gamma_x = Bx / m
gamma_y = By / m
gamma_z = Bz / m
vx_next = (vx + Gx * dt) / (1.0 + gamma_x * dt)
vy_next = (vy + Gy * dt) / (1.0 + gamma_y * dt)
vz_next = (vz + Gz * dt) / (1.0 + gamma_z * dt)
ax, ay, az = _accel_full(x, y, z, vx_next, vy_next, vz_next, m,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0)
all_fixed = np.all(fixed, axis=1)
mask = ~all_fixed
vx_new = np.where(mask, vx + ax * dt, vx)
vy_new = np.where(mask, vy + ay * dt, vy)
vz_new = np.where(mask, vz + az * dt, vz)
x_new = np.where(mask, x + vx_new * dt, x)
y_new = np.where(mask, y + vy_new * dt, y)
z_new = np.where(mask, z + vz_new * dt, z)
return x_new, y_new, z_new, vx_new, vy_new, vz_new
# ── 中点法(与 compute.py Midpoint_Method 一致)────────────────
def _midpoint_step(x, y, z, vx, vy, vz, fixed, m,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt):
ax, ay, az = _accel_full(x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0)
all_fixed = np.all(fixed, axis=1)
mask = ~all_fixed
xm = np.where(mask, x + 0.5*vx*dt, x)
ym = np.where(mask, y + 0.5*vy*dt, y)
zm = np.where(mask, z + 0.5*vz*dt, z)
vxm = np.where(mask, vx + 0.5*ax*dt, 0.0)
vym = np.where(mask, vy + 0.5*ay*dt, 0.0)
vzm = np.where(mask, vz + 0.5*az*dt, 0.0)
x_new = np.where(mask, x + vxm * dt, x)
y_new = np.where(mask, y + vym * dt, y)
z_new = np.where(mask, z + vzm * dt, z)
axm, aym, azm = _accel_full(xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0)
vx_new = np.where(mask, vx + axm * dt, vx)
vy_new = np.where(mask, vy + aym * dt, vy)
vz_new = np.where(mask, vz + azm * dt, vz)
return x_new, y_new, z_new, vx_new, vy_new, vz_new
# ── 边界:反弹 + 回绕 + 逐自由度固定约束 ───────────────────────
def _apply_bc(x, y, z, vx, vy, vz, fixed, pos_init, box_a):
lo, hi = -box_a, box_a
# 反弹(全固定原子跳过)
all_fixed = np.all(fixed, axis=1)
do_bc = ~all_fixed
over_x = do_bc & (x > hi); under_x = do_bc & (x < lo)
over_y = do_bc & (y > hi); under_y = do_bc & (y < lo)
over_z = do_bc & (z > hi); under_z = do_bc & (z < lo)
x = np.where(over_x, hi, np.where(under_x, lo, x))
y = np.where(over_y, hi, np.where(under_y, lo, y))
z = np.where(over_z, hi, np.where(under_z, lo, z))
vx = np.where(over_x | under_x, -np.abs(vx)*np.sign(np.where(over_x, 1, -1)), vx)
vy = np.where(over_y | under_y, -np.abs(vy)*np.sign(np.where(over_y, 1, -1)), vy)
vz = np.where(over_z | under_z, -np.abs(vz)*np.sign(np.where(over_z, 1, -1)), vz)
# 反弹速度简化:越界则取反绝对值(与 C 版 _limit1 一致)
vx = np.where(over_x, -np.abs(vx), np.where(under_x, np.abs(vx), vx))
vy = np.where(over_y, -np.abs(vy), np.where(under_y, np.abs(vy), vy))
vz = np.where(over_z, -np.abs(vz), np.where(under_z, np.abs(vz), vz))
# 回绕
x = np.where(x > hi, lo, np.where(x < lo, hi, x))
y = np.where(y > hi, lo, np.where(y < lo, hi, y))
z = np.where(z > hi, lo, np.where(z < lo, hi, z))
# 逐自由度固定约束
fx = fixed[:, 0].astype(bool)
fy = fixed[:, 1].astype(bool)
fz = fixed[:, 2].astype(bool)
x = np.where(fx, pos_init[:, 0], x); vx = np.where(fx, 0.0, vx)
y = np.where(fy, pos_init[:, 1], y); vy = np.where(fy, 0.0, vy)
z = np.where(fz, pos_init[:, 2], z); vz = np.where(fz, 0.0, vz)
return x, y, z, vx, vy, vz
# ── 驱动力(与 compute.py apply_driving_force 逻辑一致)─────────
def _apply_driving(x, y, z, vx, vy, vz, t, step, dt,
drv_idx, drv_amp, drv_freq, drv_phi, drv_eq,
drv_ncycles, drv_has_period, freeze):
"""freeze: (n_drivers, 3) mutable array for frozen positions."""
nd = len(drv_idx)
for d in range(nd):
idx = drv_idx[d]
fx_ = drv_freq[d, 0]; fy_ = drv_freq[d, 1]; fz_ = drv_freq[d, 2]
if drv_has_period[d]:
mf = max(abs(fx_), abs(fy_), abs(fz_))
ps = int(drv_ncycles[d] / mf / dt) if mf > 1e-12 else 0
if step > ps:
x[idx] = freeze[d, 0]; y[idx] = freeze[d, 1]; z[idx] = freeze[d, 2]
vx[idx] = vy[idx] = vz[idx] = 0.0
continue
px = drv_eq[d,0] + drv_amp[d,0]*np.cos(TWO_PI*fx_*t + drv_phi[d,0])
py = drv_eq[d,1] + drv_amp[d,1]*np.cos(TWO_PI*fy_*t + drv_phi[d,1])
pz = drv_eq[d,2] + drv_amp[d,2]*np.cos(TWO_PI*fz_*t + drv_phi[d,2])
if step == ps:
freeze[d, 0] = px; freeze[d, 1] = py; freeze[d, 2] = pz
x[idx] = drv_eq[d,0] + drv_amp[d,0]*np.cos(TWO_PI*fx_*t + drv_phi[d,0])
y[idx] = drv_eq[d,1] + drv_amp[d,1]*np.cos(TWO_PI*fy_*t + drv_phi[d,1])
z[idx] = drv_eq[d,2] + drv_amp[d,2]*np.cos(TWO_PI*fz_*t + drv_phi[d,2])
vx[idx] = -drv_amp[d,0]*TWO_PI*fx_*np.sin(TWO_PI*fx_*t + drv_phi[d,0])
vy[idx] = -drv_amp[d,1]*TWO_PI*fy_*np.sin(TWO_PI*fy_*t + drv_phi[d,1])
vz[idx] = -drv_amp[d,2]*TWO_PI*fz_*np.sin(TWO_PI*fz_*t + drv_phi[d,2])
def _do_step(x, y, z, vx, vy, vz, fixed, masses, method_id,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt, pos_init, box_a):
if method_id == 0:
x, y, z, vx, vy, vz = _euler_step(
x, y, z, vx, vy, vz, fixed, masses,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt)
elif method_id == 1:
x, y, z, vx, vy, vz = _implicit_euler_step(
x, y, z, vx, vy, vz, fixed, masses,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt)
elif method_id == 2:
x, y, z, vx, vy, vz = _midpoint_step(
x, y, z, vx, vy, vz, fixed, masses,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt)
else:
x, y, z, vx, vy, vz = _leapfrog_step(
x, y, z, vx, vy, vz, fixed, masses,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt)
x, y, z, vx, vy, vz = _apply_bc(x, y, z, vx, vy, vz, fixed, pos_init, box_a)
return x, y, z, vx, vy, vz
# ══════════════════════════════════════════════════════════════
# 主函数:run_dynamics
# 接口与 C/C++/Fortran DLL 的 run_dynamics() 对应,
# 参数格式:numpy 数组(替代 ctypes 指针)。
#
# method_id: 0=euler 1=implicit_euler 2=midpoint 3=leapfrog
# drv_amp/freq/phi/eq: (n_drivers, 3) float64
# drv_ncycles: (n_drivers,) float64 0=不限
# drv_has_period: (n_drivers,) int
#
# 返回:(out_x, out_y, out_z, out_vx, out_vy, out_vz)
# 各 shape=(n_frames, n_atoms)
# ══════════════════════════════════════════════════════════════
def run_dynamics(
n_atoms, pos_init, vel_init, masses, fixed,
n_bonds, bond_pairs, bond_k, bond_r0,
box_a, dt,
NT, NSTEP, warmup_steps, method_id,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force, gravity_strength,
n_drivers, drv_idx, drv_amp, drv_freq, drv_phi, drv_eq,
drv_ncycles, drv_has_period,
n_frames,
progress_cb=None,
):
"""运行动力学模拟,返回抽帧轨迹数组。
Args:
pos_init: (n_atoms, 3) float64
vel_init: (n_atoms, 3) float64
masses: (n_atoms,) float64
fixed: (n_atoms, 3) int 1=固定
bond_pairs: (n_bonds, 2) int 0-based 局部索引
bond_k: (n_bonds,) float64
bond_r0: (n_bonds,) float64
drv_idx: (n_drivers,) int 0-based
drv_amp/freq/phi/eq: (n_drivers, 3) float64
drv_ncycles: (n_drivers,) float64
drv_has_period: (n_drivers,) int
n_frames: 预分配的输出帧数
Returns:
out_x, out_y, out_z, out_vx, out_vy, out_vz (n_frames, n_atoms)
"""
pos_init = np.asarray(pos_init, dtype=np.float64)
vel_init = np.asarray(vel_init, dtype=np.float64)
masses = np.asarray(masses, dtype=np.float64)
fixed = np.asarray(fixed, dtype=np.int32)
bond_pairs = np.asarray(bond_pairs, dtype=np.int64).reshape(-1, 2) if n_bonds else np.zeros((0,2), dtype=np.int64)
bond_k = np.asarray(bond_k, dtype=np.float64) if n_bonds else np.zeros(0)
bond_r0 = np.asarray(bond_r0, dtype=np.float64) if n_bonds else np.zeros(0)
n = n_atoms
x = pos_init[:, 0].copy()
y = pos_init[:, 1].copy()
z = pos_init[:, 2].copy()
vx = vel_init[:, 0].copy()
vy = vel_init[:, 1].copy()
vz = vel_init[:, 2].copy()
# 驱动力数据(保证正确形状)
nd = n_drivers
if nd > 0:
drv_idx = np.asarray(drv_idx, dtype=np.int64)
drv_amp = np.asarray(drv_amp, dtype=np.float64).reshape(nd, 3)
drv_freq = np.asarray(drv_freq, dtype=np.float64).reshape(nd, 3)
drv_phi = np.asarray(drv_phi, dtype=np.float64).reshape(nd, 3)
drv_eq = np.asarray(drv_eq, dtype=np.float64).reshape(nd, 3)
drv_nc = np.asarray(drv_ncycles, dtype=np.float64)
drv_hp = np.asarray(drv_has_period, dtype=np.int32)
freeze = np.zeros((nd, 3), dtype=np.float64)
else:
drv_idx = drv_amp = drv_freq = drv_phi = drv_eq = drv_nc = drv_hp = freeze = None
def _drive(t_, step_):
if nd > 0:
_apply_driving(x, y, z, vx, vy, vz, t_, step_, dt,
drv_idx, drv_amp, drv_freq, drv_phi, drv_eq,
drv_nc, drv_hp, freeze)
# ── 蛙跳法:初始化 v(-dt/2) ─────────────────────────────
if method_id == 3:
ax0, ay0, az0 = _accel_conservative(x, y, z, masses, Gx, Gy, Gz,
gravity_field, elastic_force,
bond_pairs, bond_k, bond_r0)
all_fixed = np.all(fixed, axis=1)
vx = np.where(all_fixed, vx, vx - 0.5 * ax0 * dt)
vy = np.where(all_fixed, vy, vy - 0.5 * ay0 * dt)
vz = np.where(all_fixed, vz, vz - 0.5 * az0 * dt)
# ── 初始驱动 t=0 ─────────────────────────────────────────
_drive(0.0, 0)
# ── 预热 ─────────────────────────────────────────────────
for s in range(warmup_steps):
tw = (s + 1) * dt
_drive(tw, s)
x, y, z, vx, vy, vz = _do_step(
x, y, z, vx, vy, vz, fixed, masses, method_id,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt, pos_init, box_a)
# ── 记录循环 ─────────────────────────────────────────────
record_steps = NT - warmup_steps
prog_interval = max(1, record_steps // 100)
out_x = np.zeros((n_frames, n), dtype=np.float64)
out_y = np.zeros((n_frames, n), dtype=np.float64)
out_z = np.zeros((n_frames, n), dtype=np.float64)
out_vx = np.zeros((n_frames, n), dtype=np.float64)
out_vy = np.zeros((n_frames, n), dtype=np.float64)
out_vz = np.zeros((n_frames, n), dtype=np.float64)
frame_idx = 0
for s in range(record_steps):
if progress_cb is not None and s % prog_interval == 0 and s > 0:
progress_cb(s, record_steps)
t = (s + warmup_steps) * dt
_drive(t, s)
if s % NSTEP == 0 and frame_idx < n_frames:
out_x[frame_idx] = x
out_y[frame_idx] = y
out_z[frame_idx] = z
out_vx[frame_idx] = vx
out_vy[frame_idx] = vy
out_vz[frame_idx] = vz
frame_idx += 1
x, y, z, vx, vy, vz = _do_step(
x, y, z, vx, vy, vz, fixed, masses, method_id,
Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
bond_pairs, bond_k, bond_r0, dt, pos_init, box_a)
return out_x, out_y, out_z, out_vx, out_vy, out_vz
+287
View File
@@ -0,0 +1,287 @@
"""
engines/python/main.py
-----------------------
独立 Python 计算引擎
main.c / main.cpp / main.f90 结构一致
输入: <input_dir>/coord.txt, connection.txt, bond.txt, [driver.txt]
<param_json> ( engines/c/param.json 格式)
输出: <output_dir>/display.txt (+ display.npz)
<output_dir>/trajectory.txt ( save_trajectory=1)
用法:
python main.py <input_dir> <output_dir> <param_json>
内部调用 dynamics_lib.run_dynamics()算法与 compute.py 完全一致
"""
import json
import os
import sys
import time
import numpy as np
# 将父目录(engines/python 的上级 engines)加入 sys.path
# 以便在独立运行时也能找到 dynamics_lib
_HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _HERE)
from dynamics_lib import run_dynamics
# 为读取 coord/bond/display,复用 compute.py 中的 I/O 函数
_COMPUTE = os.path.join(_HERE, "..", "..")
sys.path.insert(0, _COMPUTE)
import compute as _c
_METHOD_ID = {
"explicit_euler": 0,
"euler": 0,
"implicit_euler": 1,
"midpoint": 2,
"leapfrog": 3,
}
def _load_params(param_path):
"""读取 param.json(与 C 引擎格式相同)."""
with open(param_path, "r", encoding="utf-8") as f:
p = json.load(f)
return p
def main():
if len(sys.argv) < 4:
print("用法: python main.py <input_dir> <output_dir> <param_json>")
sys.exit(1)
input_dir = sys.argv[1]
output_dir = sys.argv[2]
param_path = sys.argv[3]
os.makedirs(output_dir, exist_ok=True)
# ── 读取参数 ─────────────────────────────────────────────
p = _load_params(param_path)
box_a = float(p.get("box_a", 10.0))
NT = int(p.get("NT", 10000))
dt = float(p.get("DT", 0.001))
NSTEP = int(p.get("NSTEP", 100))
warmup_steps = int(p.get("warmup_steps", 0))
method_str = str(p.get("method", "leapfrog")).lower().replace(" ", "_")
method_id = _METHOD_ID.get(method_str, 3)
G = p.get("G", [0.0, 0.0, -9.8])
B = p.get("B", [0.0, 0.0, 0.0])
gravity_field = int(p.get("gravity_field", 1))
elastic_force = int(p.get("elastic_force", 1))
damping_force = int(p.get("damping_force", 0))
gravity_strength = float(p.get("gravity_strength", 1.0))
driving_force = int(p.get("driving_force", 0))
save_traj = int(p.get("save_trajectory", 0))
# ── 读取原子数据 ──────────────────────────────────────────
coord_path = os.path.join(input_dir, "coord.txt")
atom_ids, masses, radii, positions, velocities, fixed = _c.load_coord_file(coord_path)
# ── 读取键数据 ────────────────────────────────────────────
conn_path = os.path.join(input_dir, "connection.txt")
bond_path = os.path.join(input_dir, "bond.txt")
bond_map = _c.load_bond_parameters(bond_path)
bond_pairs, bond_names, bond_stiffness, bond_rest_lengths = \
_c.load_bond_connections(conn_path, atom_ids, positions, bond_map)
n_bonds = len(bond_pairs)
# ── 读取驱动力 ────────────────────────────────────────────
drv_list = []
if driving_force:
driver_path = os.path.join(input_dir, "driver.txt")
raw_drivers = _c.load_driver_file(driver_path, atom_ids)
if raw_drivers:
atom_id_map = {int(aid): i for i, aid in enumerate(atom_ids)}
for d in raw_drivers:
aid = int(d["atom_id"])
if aid not in atom_id_map:
continue
lidx = atom_id_map[aid]
eq = positions[lidx].tolist()
d["eq_pos"] = np.array(eq)
pc = d.get("period_cycles")
nc = float(pc) if pc is not None else 0.0
hp = 1 if nc > 0 else 0
drv_list.append({
"local_idx": lidx,
"amp": d["amp"].tolist(),
"freq": d["freq"].tolist(),
"phi": d["phi"].tolist(), # radians
"eq": eq,
"nc": nc,
"hp": hp,
})
nd = len(drv_list)
if nd > 0:
drv_idx = np.array([d["local_idx"] for d in drv_list], dtype=np.int64)
drv_amp = np.array([d["amp"] for d in drv_list], dtype=np.float64)
drv_freq = np.array([d["freq"] for d in drv_list], dtype=np.float64)
drv_phi = np.array([d["phi"] for d in drv_list], dtype=np.float64)
drv_eq = np.array([d["eq"] for d in drv_list], dtype=np.float64)
drv_nc = np.array([d["nc"] for d in drv_list], dtype=np.float64)
drv_hp = np.array([d["hp"] for d in drv_list], dtype=np.int32)
else:
drv_idx = drv_amp = drv_freq = drv_phi = drv_eq = drv_nc = drv_hp = \
np.zeros(0, dtype=np.int64)
# ── 计算帧数 ──────────────────────────────────────────────
record_steps = NT - warmup_steps
n_frames = max(1, record_steps // NSTEP)
# ── 进度回调 ──────────────────────────────────────────────
def _progress(step, total):
pct = step * 100 // total
print(f"[python-engine] progress: {step}/{total} ({pct}%)", flush=True)
# ── 运行计算 ──────────────────────────────────────────────
t0 = time.time()
print(f"[python-engine] NT={NT} NSTEP={NSTEP} method={method_str} "
f"n_atoms={len(atom_ids)} n_bonds={n_bonds}")
out_x, out_y, out_z, out_vx, out_vy, out_vz = run_dynamics(
n_atoms=len(atom_ids),
pos_init=positions,
vel_init=velocities,
masses=masses,
fixed=fixed,
n_bonds=n_bonds,
bond_pairs=bond_pairs,
bond_k=bond_stiffness,
bond_r0=bond_rest_lengths,
box_a=box_a,
dt=dt,
NT=NT,
NSTEP=NSTEP,
warmup_steps=warmup_steps,
method_id=method_id,
Gx=float(G[0]), Gy=float(G[1]), Gz=float(G[2]),
Bx=float(B[0]), By=float(B[1]), Bz=float(B[2]),
gravity_field=gravity_field,
elastic_force=elastic_force,
damping_force=damping_force,
gravity_strength=gravity_strength,
n_drivers=nd,
drv_idx=drv_idx,
drv_amp=drv_amp,
drv_freq=drv_freq,
drv_phi=drv_phi,
drv_eq=drv_eq,
drv_ncycles=drv_nc,
drv_has_period=drv_hp,
n_frames=n_frames,
progress_cb=_progress,
)
elapsed = time.time() - t0
print(f"[python-engine] 完成: {n_frames}{elapsed:.3f} s")
# ── 构建 display header ───────────────────────────────────
ball_radius = float(p.get("ball_radius", 0.5))
ball_color = p.get("ball_color", [0.9, 0.2, 0.2])
box_color = p.get("box_color", [0.8, 0.8, 0.85])
use_marker = int(p.get("use_marker", 0))
alpha_val = p.get("alpha", 0.2)
cam_dist = float(p.get("camera_distance", 40.0))
cam_elev = float(p.get("camera_elevation", 0.0))
cam_azim = float(p.get("camera_azimuth", 0.0))
cam_cx = float(p.get("camera_center_x", 0.0))
cam_cy = float(p.get("camera_center_y", 0.0))
cam_cz = float(p.get("camera_center_z", 0.0))
header = {
"DT": str(dt),
"NSTEP": str(NSTEP),
"method": method_str,
"NT": str(NT),
"warmup_steps": str(warmup_steps),
"dynamic_steps": str(record_steps),
"T_total": str(NT * dt),
"box_a": str(box_a),
"gravity_field": str(gravity_field),
"elastic_force": str(elastic_force),
"damping_force": str(damping_force),
"driving_force": str(driving_force),
"gravity_strength": str(gravity_strength),
"G": json.dumps([float(v) for v in G]),
"B": json.dumps([float(v) for v in B]),
"number_of_frames": str(n_frames),
"number_of_particles": str(len(atom_ids)),
"use_marker": str(use_marker),
"display_color": json.dumps(p.get("display_color",
{"x":[0,[1.0,0.0,0.0]],"y":[0,[0.0,1.0,0.0]],"z":[0,[0.0,0.0,1.0]],
"xy":[0,[1.0,1.0,0.0]],"yz":[0,[0.0,1.0,1.0]],"zx":[0,[1.0,0.0,1.0]],
"xyz":[1,[1.0,1.0,1.0]]})),
"ball_radius": str(ball_radius),
"ball_color_r": str(ball_color[0]),
"ball_color_g": str(ball_color[1]),
"ball_color_b": str(ball_color[2]),
"box_color_r": str(box_color[0]),
"box_color_g": str(box_color[1]),
"box_color_b": str(box_color[2]),
"alpha": str(alpha_val) if not isinstance(alpha_val, list)
else ",".join(str(a) for a in alpha_val),
"atom_radii": ",".join(str(r) for r in radii),
"atom_masses": json.dumps([float(m) for m in masses]),
"atom_positions": json.dumps(positions.tolist()),
"bond_pairs": json.dumps(bond_pairs.tolist() if n_bonds else []),
"bond_stiffness": json.dumps(bond_stiffness.tolist() if n_bonds else []),
"bond_rest_lengths": json.dumps(bond_rest_lengths.tolist() if n_bonds else []),
"X_MIN": str(-box_a), "X_MAX": str(box_a),
"Y_MIN": str(-box_a), "Y_MAX": str(box_a),
"Z_MIN": str(-box_a), "Z_MAX": str(box_a),
"camera_distance": str(cam_dist),
"camera_elevation": str(cam_elev),
"camera_azimuth": str(cam_azim),
"camera_center_x": str(cam_cx),
"camera_center_y": str(cam_cy),
"camera_center_z": str(cam_cz),
"camera_keyframes": "",
}
# ── 保存 display.txt + display.npz ───────────────────────
disp_txt = os.path.join(output_dir, "display.txt")
_c.save_display_txt(
disp_txt,
out_x, out_y, out_z, out_vx, out_vy, out_vz,
atom_ids, record_steps, len(atom_ids),
header_fields=header,
)
print(f"[python-engine] display.txt 已保存: {disp_txt}")
disp_npz = os.path.join(output_dir, "display.npz")
_c.save_display_npz(
disp_npz,
out_x, out_y, out_z, out_vx, out_vy, out_vz,
atom_ids, header_fields=header,
)
print(f"[python-engine] display.npz 已保存: {disp_npz}")
# ── 可选:保存 trajectory.txt ─────────────────────────────
if save_traj:
traj_payload = {
"traj_x": out_x, "traj_y": out_y, "traj_z": out_z,
"traj_vx": out_vx, "traj_vy": out_vy, "traj_vz": out_vz,
"NT": record_steps, "DT": dt, "NSTEP": NSTEP,
"method": method_str,
"atom_ids": atom_ids,
"atom_masses": masses,
"atom_radii": radii,
"atom_positions": positions,
"bond_pairs": bond_pairs,
"bond_stiffness": bond_stiffness,
"bond_rest_lengths": bond_rest_lengths,
"G": [float(v) for v in G],
"B": [float(v) for v in B],
}
traj_path = os.path.join(output_dir, "trajectory.txt")
_c.save_text_data(traj_path, traj_payload)
print(f"[python-engine] trajectory.txt 已保存: {traj_path}")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
# engines/src/c/Makefile
# 编译 DLL 到 engines/release/(主程序通过 ctypes 直接调用)
CC = gcc
CFLAGS = -O3 -march=native -Wall -Wextra
LDFLAGS = -lm
LIB_SRC = dynamics_lib.c
# 自动检测系统
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
# Windows 检测:Msys2/MINGW 也视为 Windows
IS_WINDOWS := $(findstring MINGW,$(UNAME_S))
ifneq ($(IS_WINDOWS),)
UNAME_S := Windows
endif
IS_WINDOWS := $(findstring MSYS,$(UNAME_S))
ifneq ($(IS_WINDOWS),)
UNAME_S := Windows
endif
# DLL 输出到 engines/release/
DLL_DIR = ../../release
ifeq ($(UNAME_S),Linux)
DLL_TARGET = $(DLL_DIR)/dynamics_c.so
DLL_FLAGS = -shared -fPIC
else ifeq ($(UNAME_S),Darwin)
DLL_TARGET = $(DLL_DIR)/dynamics_c.dylib
DLL_FLAGS = -dynamiclib
else
# Windows: 静态链接运行时,避免依赖 libgcc_s_seh-1.dll
DLL_TARGET = $(DLL_DIR)/dynamics_c.dll
DLL_FLAGS = -shared -static
endif
.PHONY: all dll clean
all: dll
dll: $(DLL_TARGET)
$(DLL_TARGET): $(LIB_SRC) | $(DLL_DIR)
$(CC) $(CFLAGS) $(DLL_FLAGS) -o $@ $(LIB_SRC) $(LDFLAGS)
@echo " === C DLL built: $@ ==="
$(DLL_DIR):
mkdir -p $(DLL_DIR)
clean:
rm -f $(DLL_TARGET)
+554
View File
@@ -0,0 +1,554 @@
/**
* engines/c/dynamics_lib.c
* -------------------------
* DLL I/O Python NumPy
* Python
* main.c compute.py
*
* Windows DLL:
* gcc -O3 -march=native -shared -o build/dynamics_c.dll dynamics_lib.c -lm
* Linux .so:
* gcc -O3 -march=native -shared -fPIC -o build/dynamics_c.so dynamics_lib.c -lm
* macOS .dylib:
* gcc -O3 -march=native -dynamiclib -o build/dynamics_c.dylib dynamics_lib.c -lm
*/
#ifdef _WIN32
# define EXPORT __declspec(dllexport)
#else
# define EXPORT __attribute__((visibility("default")))
#endif
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* ── 驱动力结构体 ─────────────────────────────────────────── */
typedef struct {
int n_drivers;
const int *idx; /* [n_drivers] 0-based local atom index */
const double *amp; /* [n_drivers*3] (ax,ay,az) interleaved */
const double *freq; /* [n_drivers*3] */
const double *phi; /* [n_drivers*3] radians */
const double *eq; /* [n_drivers*3] equilibrium positions */
const double *ncycles; /* [n_drivers] 0=unlimited */
const int *has_period; /* [n_drivers] */
/* mutable freeze positions (allocated internally) */
double *freeze; /* [n_drivers*3] */
} Drivers;
/* ── 加速度:保守力(弹簧键 + 均匀重力场)────────────────── */
static void accel_conservative(
int n, const double *x, const double *y, const double *z,
const double *m,
double Gx, double Gy, double Gz,
int gravity_field, int elastic_force,
int n_bonds, const int *bond_pairs,
const double *bond_k, const double *bond_r0,
double *ax, double *ay, double *az)
{
for (int i = 0; i < n; i++) {
ax[i] = gravity_field ? Gx : 0.0;
ay[i] = gravity_field ? Gy : 0.0;
az[i] = gravity_field ? Gz : 0.0;
}
if (!elastic_force || n_bonds == 0) return;
for (int b = 0; b < n_bonds; b++) {
int ii = bond_pairs[b*2];
int jj = bond_pairs[b*2+1];
double dx = x[jj] - x[ii];
double dy = y[jj] - y[ii];
double dz = z[jj] - z[ii];
double dist = sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 1e-12) continue;
double k = bond_k[b];
double r0 = bond_r0[b];
double fac = k * (dist - r0) / dist;
double fx = fac * dx, fy = fac * dy, fz_b = fac * dz;
ax[ii] += fx / m[ii]; ay[ii] += fy / m[ii]; az[ii] += fz_b / m[ii];
ax[jj] -= fx / m[jj]; ay[jj] -= fy / m[jj]; az[jj] -= fz_b / m[jj];
}
}
/* ── 完整加速度(含阻尼)────────────────────────────────── */
static void accel_full(
int n, const double *x, const double *y, const double *z,
const double *vx, const double *vy, const double *vz,
const double *m,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bond_pairs,
const double *bond_k, const double *bond_r0,
double *ax, double *ay, double *az)
{
accel_conservative(n, x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
n_bonds, bond_pairs, bond_k, bond_r0,
ax, ay, az);
if (damping_force) {
for (int i = 0; i < n; i++) {
ax[i] -= Bx * vx[i] / m[i];
ay[i] -= By * vy[i] / m[i];
az[i] -= Bz * vz[i] / m[i];
}
}
}
/* ── 边界:反弹(与 main.c limit_in_box 一致)────────────── */
static inline void _limit1(double *p, double *v, double lo, double hi) {
if (*p > hi) { *p = hi; *v = -fabs(*v); }
if (*p < lo) { *p = lo; *v = fabs(*v); }
}
/* ── 边界:回绕(与 main.c wrap_position 一致)──────────── */
static inline void _wrap1(double *p, double lo, double hi) {
if (*p > hi) *p = lo;
if (*p < lo) *p = hi;
}
/* ── 边界 + 固定约束(与 main.c apply_step 末尾一致)──────── */
static void apply_boundary_and_constraints(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const int *fixed, const double *pos_init,
double box_a)
{
double lo = -box_a, hi = box_a;
/* 反弹 */
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
_limit1(&x[i], &vx[i], lo, hi);
_limit1(&y[i], &vy[i], lo, hi);
_limit1(&z[i], &vz[i], lo, hi);
}
/* 回绕 */
for (int i = 0; i < n; i++) {
_wrap1(&x[i], lo, hi);
_wrap1(&y[i], lo, hi);
_wrap1(&z[i], lo, hi);
}
/* 逐自由度固定约束:与 main.c 和 Python apply_fixed_constraints 一致 */
for (int i = 0; i < n; i++) {
if (fixed[i*3+0]) { x[i] = pos_init[i*3+0]; vx[i] = 0.0; }
if (fixed[i*3+1]) { y[i] = pos_init[i*3+1]; vy[i] = 0.0; }
if (fixed[i*3+2]) { z[i] = pos_init[i*3+2]; vz[i] = 0.0; }
}
}
/* ══════════════════════════════════════════════════════════
* main.c leapfrog_step
* x(t), v(t-dt/2) x(t+dt), v(t+dt/2)
* α = B·dt/(2m)
* */
static void leapfrog_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0,
double dt)
{
double *ax = (double*)alloca(n*sizeof(double)*3);
double *ay = ax+n; double *az = ay+n;
accel_conservative(n, x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
n_bonds, bp, bk, br0, ax, ay, az);
int has_damp = damping_force && (Bx != 0.0 || By != 0.0 || Bz != 0.0);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
if (has_damp) {
double ax_ = Bx*dt/(2.0*m[i]);
double ay_ = By*dt/(2.0*m[i]);
double az_ = Bz*dt/(2.0*m[i]);
vx[i] = (vx[i]*(1.0-ax_) + ax[i]*dt) / (1.0+ax_);
vy[i] = (vy[i]*(1.0-ay_) + ay[i]*dt) / (1.0+ay_);
vz[i] = (vz[i]*(1.0-az_) + az[i]*dt) / (1.0+az_);
} else {
vx[i] += ax[i]*dt;
vy[i] += ay[i]*dt;
vz[i] += az[i]*dt;
}
x[i] += vx[i]*dt;
y[i] += vy[i]*dt;
z[i] += vz[i]*dt;
}
}
/* ══════════════════════════════════════════════════════════
* main.c explicit_euler_step
* */
static void euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0,
double dt)
{
double *ax = (double*)alloca(n*sizeof(double)*3);
double *ay = ax+n; double *az = ay+n;
accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
x[i] += vx[i]*dt; y[i] += vy[i]*dt; z[i] += vz[i]*dt;
vx[i]+= ax[i]*dt; vy[i]+= ay[i]*dt; vz[i]+= az[i]*dt;
}
}
/* ══════════════════════════════════════════════════════════
* main.c implicit_euler_step
*
* main.c
* 1. v_next (v + G·dt)/(1 + γ·dt) +
* 2. (x, v_next) a_next
* 3. v += a_next·dt; x += v·dt
* */
static void implicit_euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0,
double dt)
{
double *vxn = (double*)alloca(n*sizeof(double)*3);
double *vyn = vxn+n; double *vzn = vyn+n;
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) {
vxn[i] = vyn[i] = vzn[i] = 0.0; continue;
}
double gx = Bx / m[i], gy = By / m[i], gz = Bz / m[i];
vxn[i] = (vx[i] + Gx*dt) / (1.0 + gx*dt);
vyn[i] = (vy[i] + Gy*dt) / (1.0 + gy*dt);
vzn[i] = (vz[i] + Gz*dt) / (1.0 + gz*dt);
}
double *ax = (double*)alloca(n*sizeof(double)*3);
double *ay = ax+n; double *az = ay+n;
accel_full(n, x, y, z, vxn, vyn, vzn, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i]*dt;
vy[i] += ay[i]*dt;
vz[i] += az[i]*dt;
x[i] += vx[i]*dt;
y[i] += vy[i]*dt;
z[i] += vz[i]*dt;
}
}
/* ══════════════════════════════════════════════════════════
* main.c midpoint_step
*
* main.c
* 1. a = accel(x, v)
* 2. xm = x + 0.5·v·dt; vm = v + 0.5·a·dt
* 3. x = x + vm·dt ( vm)
* 4. am = accel(xm, vm)
* 5. v = v + am·dt
* */
static void midpoint_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0,
double dt)
{
/* Allocate in one block for cache locality */
double *buf = (double*)alloca(n*sizeof(double)*9);
double *ax = buf;
double *ay = ax+n; double *az = ay+n;
double *xm = az+n; double *ym = xm+n; double *zm = ym+n;
double *vxm = zm+n; double *vym = vxm+n; double *vzm = vym+n;
accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) {
xm[i]=x[i]; ym[i]=y[i]; zm[i]=z[i];
vxm[i]=vym[i]=vzm[i]=0.0; continue;
}
xm[i] = x[i] + 0.5*vx[i]*dt;
ym[i] = y[i] + 0.5*vy[i]*dt;
zm[i] = z[i] + 0.5*vz[i]*dt;
vxm[i] = vx[i] + 0.5*ax[i]*dt;
vym[i] = vy[i] + 0.5*ay[i]*dt;
vzm[i] = vz[i] + 0.5*az[i]*dt;
/* position updated with midpoint velocity (same as main.c) */
x[i] = x[i] + vxm[i]*dt;
y[i] = y[i] + vym[i]*dt;
z[i] = z[i] + vzm[i]*dt;
}
double *axm = (double*)alloca(n*sizeof(double)*3);
double *aym = axm+n; double *azm = aym+n;
accel_full(n, xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, axm, aym, azm);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += axm[i]*dt;
vy[i] += aym[i]*dt;
vz[i] += azm[i]*dt;
}
}
/* ── 驱动力(与 main.c apply_driving_force 一致)────────── */
static void apply_driving(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
double t, int step, double dt, Drivers *drv)
{
(void)n;
if (!drv || drv->n_drivers == 0) return;
const double TWO_PI = 2.0 * 3.14159265358979323846;
for (int d = 0; d < drv->n_drivers; d++) {
int idx = drv->idx[d];
double fx = drv->freq[d*3+0];
double fy = drv->freq[d*3+1];
double fz = drv->freq[d*3+2];
if (drv->has_period[d]) {
double mf = fabs(fx) > fabs(fy) ? fabs(fx) : fabs(fy);
if (fabs(fz) > mf) mf = fabs(fz);
int period_steps = 0;
if (mf > 1e-12)
period_steps = (int)(drv->ncycles[d] / mf / dt);
if (step > period_steps) {
x[idx] = drv->freeze[d*3+0];
y[idx] = drv->freeze[d*3+1];
z[idx] = drv->freeze[d*3+2];
vx[idx] = vy[idx] = vz[idx] = 0.0;
continue;
}
double px = drv->eq[d*3+0] + drv->amp[d*3+0]*cos(TWO_PI*fx*t + drv->phi[d*3+0]);
double py = drv->eq[d*3+1] + drv->amp[d*3+1]*cos(TWO_PI*fy*t + drv->phi[d*3+1]);
double pz = drv->eq[d*3+2] + drv->amp[d*3+2]*cos(TWO_PI*fz*t + drv->phi[d*3+2]);
if (step == period_steps) {
drv->freeze[d*3+0] = px;
drv->freeze[d*3+1] = py;
drv->freeze[d*3+2] = pz;
}
}
x[idx] = drv->eq[d*3+0] + drv->amp[d*3+0]*cos(TWO_PI*fx*t + drv->phi[d*3+0]);
y[idx] = drv->eq[d*3+1] + drv->amp[d*3+1]*cos(TWO_PI*fy*t + drv->phi[d*3+1]);
z[idx] = drv->eq[d*3+2] + drv->amp[d*3+2]*cos(TWO_PI*fz*t + drv->phi[d*3+2]);
vx[idx] = -drv->amp[d*3+0]*TWO_PI*fx*sin(TWO_PI*fx*t + drv->phi[d*3+0]);
vy[idx] = -drv->amp[d*3+1]*TWO_PI*fy*sin(TWO_PI*fy*t + drv->phi[d*3+1]);
vz[idx] = -drv->amp[d*3+2]*TWO_PI*fz*sin(TWO_PI*fz*t + drv->phi[d*3+2]);
}
}
/* ══════════════════════════════════════════════════════════
* run_dynamics
*
* main.c
* 1. leapfrog v(-dt/2)
* 2. t=0
* 3.
* 4. drive record step boundary constraints
*
* C-contiguous float64/int32
* n_atoms
* pos_init [n_atoms*3] x0,y0,z0, x1,y1,z1, ...
* vel_init [n_atoms*3]
* masses [n_atoms]
* fixed [n_atoms*3] int32, 1=
* n_bonds
* bond_pairs [n_bonds*2] int32, 0-based local index
* bond_k [n_bonds]
* bond_r0 [n_bonds]
* box_a
* dt
* NT
* NSTEP
* warmup_steps
* method_id 0=euler 1=implicit 2=midpoint 3=leapfrog
* Gx/Gy/Gz
* Bx/By/Bz
* gravity_field / elastic_force / damping_force
* gravity_strength
* n_drivers
* drv_idx [n_drivers] int32
* drv_amp [n_drivers*3]
* drv_freq [n_drivers*3]
* drv_phi [n_drivers*3]
* drv_eq [n_drivers*3]
* drv_ncycles [n_drivers] 0=
* drv_has_period [n_drivers] int32
* n_frames Python (NT-warmup)/NSTEP
* out_x/y/z/vx/vy/vz [n_frames*n_atoms] Python
* progress_cb NULL
*
* 0==
* */
EXPORT int run_dynamics(
int n_atoms,
const double *pos_init,
const double *vel_init,
const double *masses,
const int *fixed,
int n_bonds,
const int *bond_pairs,
const double *bond_k,
const double *bond_r0,
double box_a, double dt,
int NT, int NSTEP, int warmup_steps, int method_id,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
double gravity_strength,
int n_drivers,
const int *drv_idx,
const double *drv_amp,
const double *drv_freq,
const double *drv_phi,
const double *drv_eq,
const double *drv_ncycles,
const int *drv_has_period,
int n_frames,
double *out_x, double *out_y, double *out_z,
double *out_vx, double *out_vy, double *out_vz,
void (*progress_cb)(int step, int total))
{
(void)gravity_strength; /* 原子间引力暂未实现 */
int n = n_atoms;
/* ── 工作数组 ── */
double *x = (double*)malloc(n*sizeof(double));
double *y = (double*)malloc(n*sizeof(double));
double *z = (double*)malloc(n*sizeof(double));
double *vx = (double*)malloc(n*sizeof(double));
double *vy = (double*)malloc(n*sizeof(double));
double *vz = (double*)malloc(n*sizeof(double));
if (!x||!y||!z||!vx||!vy||!vz) return -1;
for (int i = 0; i < n; i++) {
x[i]=pos_init[i*3+0]; y[i]=pos_init[i*3+1]; z[i]=pos_init[i*3+2];
vx[i]=vel_init[i*3+0]; vy[i]=vel_init[i*3+1]; vz[i]=vel_init[i*3+2];
}
/* ── 驱动结构 ── */
Drivers drv;
drv.n_drivers = n_drivers;
drv.idx = drv_idx;
drv.amp = drv_amp;
drv.freq = drv_freq;
drv.phi = drv_phi;
drv.eq = drv_eq;
drv.ncycles = drv_ncycles;
drv.has_period = drv_has_period;
drv.freeze = NULL;
if (n_drivers > 0) {
drv.freeze = (double*)calloc(n_drivers*3, sizeof(double));
if (!drv.freeze) { free(x);free(y);free(z);free(vx);free(vy);free(vz); return -2; }
}
/* ── 内联步进宏 ── */
#define DO_STEP() do { \
switch (method_id) { \
case 0: euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
case 1: implicit_euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
case 2: midpoint_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
default: leapfrog_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
} \
apply_boundary_and_constraints(n,x,y,z,vx,vy,vz,fixed,pos_init,box_a); \
} while(0)
/* ── 蛙跳法:初始化 v(-dt/2) = v(0) - 0.5·a_c(0)·dt ── */
if (method_id == 3) {
double *ax0 = (double*)alloca(n*sizeof(double)*3);
double *ay0 = ax0+n; double *az0 = ay0+n;
accel_conservative(n, x, y, z, masses, Gx, Gy, Gz,
gravity_field, elastic_force,
n_bonds, bond_pairs, bond_k, bond_r0,
ax0, ay0, az0);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] -= 0.5*ax0[i]*dt;
vy[i] -= 0.5*ay0[i]*dt;
vz[i] -= 0.5*az0[i]*dt;
}
}
/* ── 初始驱动 t=0(与 main.c 一致:leapfrog init 之后施加)── */
if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, 0.0, 0, dt, &drv);
/* ── 预热(不记录)── */
for (int s = 0; s < warmup_steps; s++) {
double tw = (s + 1) * dt;
if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, tw, s, dt, &drv);
DO_STEP();
}
/* ── 记录循环 ── */
int record_steps = NT - warmup_steps;
int prog_interval = record_steps / 100;
if (prog_interval < 1) prog_interval = 1;
int frame_idx = 0;
for (int s = 0; s < record_steps; s++) {
if (progress_cb && s % prog_interval == 0 && s > 0)
progress_cb(s, record_steps);
double t = (s + warmup_steps) * dt;
if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, t, s, dt, &drv);
/* 抽帧记录(drive 之后,step 之前,与 main.c 一致)*/
if (s % NSTEP == 0 && frame_idx < n_frames) {
int base = frame_idx * n;
for (int i = 0; i < n; i++) {
out_x [base+i] = x[i]; out_y [base+i] = y[i]; out_z [base+i] = z[i];
out_vx[base+i] = vx[i]; out_vy[base+i] = vy[i]; out_vz[base+i] = vz[i];
}
frame_idx++;
}
DO_STEP();
}
#undef DO_STEP
free(x); free(y); free(z);
free(vx); free(vy); free(vz);
if (drv.freeze) free(drv.freeze);
return 0;
}
+50
View File
@@ -0,0 +1,50 @@
# engines/src/cpp/Makefile
# 编译 DLL 到 engines/release/(主程序通过 ctypes 直接调用)
CXX = g++
LIB_SRC = dynamics_lib.cpp
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
# Windows 检测:Msys2/MINGW 也视为 Windows
IS_WINDOWS := $(findstring MINGW,$(UNAME_S))
ifneq ($(IS_WINDOWS),)
UNAME_S := Windows
endif
IS_WINDOWS := $(findstring MSYS,$(UNAME_S))
ifneq ($(IS_WINDOWS),)
UNAME_S := Windows
endif
CXXFLAGS = -O3 -march=native -std=c++17 -Wall -Wextra -D_USE_MATH_DEFINES
# DLL 输出到 engines/release/
DLL_DIR = ../../release
ifeq ($(UNAME_S),Linux)
DLL_TARGET = $(DLL_DIR)/dynamics_cpp.so
DLL_FLAGS = -shared -fPIC
else ifeq ($(UNAME_S),Darwin)
DLL_TARGET = $(DLL_DIR)/dynamics_cpp.dylib
DLL_FLAGS = -dynamiclib
else
# Windows: 完全静态链接,避免依赖运行时 DLL
DLL_TARGET = $(DLL_DIR)/dynamics_cpp.dll
DLL_FLAGS = -shared -static
endif
.PHONY: all dll clean
all: dll
dll: $(DLL_TARGET)
$(DLL_TARGET): $(LIB_SRC) | $(DLL_DIR)
$(CXX) $(CXXFLAGS) $(DLL_FLAGS) -o $@ $(LIB_SRC)
@echo " === C++ DLL built: $@ ==="
$(DLL_DIR):
mkdir -p $(DLL_DIR)
clean:
rm -f $(DLL_TARGET)
+450
View File
@@ -0,0 +1,450 @@
/**
* engines/cpp/dynamics_lib.cpp
* -----------------------------
* DLLC++ I/O Python NumPy
* main.cpp / compute.py
*
* Windows:
* g++ -O3 -march=native -std=c++17 -shared -o build/dynamics_cpp.dll dynamics_lib.cpp
* Linux:
* g++ -O3 -march=native -std=c++17 -shared -fPIC -o build/dynamics_cpp.so dynamics_lib.cpp
* macOS:
* g++ -O3 -march=native -std=c++17 -dynamiclib -o build/dynamics_cpp.dylib dynamics_lib.cpp
*/
#ifdef _WIN32
# define EXPORT extern "C" __declspec(dllexport)
#else
# define EXPORT extern "C" __attribute__((visibility("default")))
#endif
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <vector>
/* ── 驱动力结构体 ─────────────────────────────────────────── */
struct Drivers {
int n_drivers = 0;
const int *idx = nullptr;
const double *amp = nullptr;
const double *freq = nullptr;
const double *phi = nullptr;
const double *eq = nullptr;
const double *ncycles = nullptr;
const int *has_period = nullptr;
std::vector<double> freeze; /* [n_drivers*3] 冻结位置(period 结束时锁定)*/
};
/* ── 加速度:保守力(弹簧键 + 均匀重力场)────────────────── */
static void accel_conservative(
int n, const double *x, const double *y, const double *z,
const double *m,
double Gx, double Gy, double Gz,
int gravity_field, int elastic_force,
int n_bonds, const int *bond_pairs,
const double *bond_k, const double *bond_r0,
double *ax, double *ay, double *az)
{
for (int i = 0; i < n; i++) {
ax[i] = gravity_field ? Gx : 0.0;
ay[i] = gravity_field ? Gy : 0.0;
az[i] = gravity_field ? Gz : 0.0;
}
if (!elastic_force || n_bonds == 0) return;
for (int b = 0; b < n_bonds; b++) {
int ii = bond_pairs[b*2];
int jj = bond_pairs[b*2+1];
double dx = x[jj]-x[ii], dy = y[jj]-y[ii], dz = z[jj]-z[ii];
double dist = std::sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 1e-12) continue;
double fac = bond_k[b] * (dist - bond_r0[b]) / dist;
double fx = fac*dx, fy = fac*dy, fz_b = fac*dz;
ax[ii] += fx/m[ii]; ay[ii] += fy/m[ii]; az[ii] += fz_b/m[ii];
ax[jj] -= fx/m[jj]; ay[jj] -= fy/m[jj]; az[jj] -= fz_b/m[jj];
}
}
/* ── 完整加速度(含阻尼)────────────────────────────────── */
static void accel_full(
int n, const double *x, const double *y, const double *z,
const double *vx, const double *vy, const double *vz,
const double *m,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bond_pairs,
const double *bond_k, const double *bond_r0,
double *ax, double *ay, double *az)
{
accel_conservative(n, x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
n_bonds, bond_pairs, bond_k, bond_r0,
ax, ay, az);
if (damping_force) {
for (int i = 0; i < n; i++) {
ax[i] -= Bx * vx[i] / m[i];
ay[i] -= By * vy[i] / m[i];
az[i] -= Bz * vz[i] / m[i];
}
}
}
/* ── 边界:反弹 ──────────────────────────────────────────── */
static inline void _limit1(double &p, double &v, double lo, double hi) {
if (p > hi) { p = hi; v = -std::fabs(v); }
if (p < lo) { p = lo; v = std::fabs(v); }
}
/* ── 边界:回绕 ──────────────────────────────────────────── */
static inline void _wrap1(double &p, double lo, double hi) {
if (p > hi) p = lo;
if (p < lo) p = hi;
}
/* ── 边界 + 固定约束 ────────────────────────────────────── */
static void apply_boundary_and_constraints(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const int *fixed, const double *pos_init, double box_a)
{
double lo = -box_a, hi = box_a;
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
_limit1(x[i], vx[i], lo, hi);
_limit1(y[i], vy[i], lo, hi);
_limit1(z[i], vz[i], lo, hi);
}
for (int i = 0; i < n; i++) {
_wrap1(x[i], lo, hi);
_wrap1(y[i], lo, hi);
_wrap1(z[i], lo, hi);
}
for (int i = 0; i < n; i++) {
if (fixed[i*3+0]) { x[i] = pos_init[i*3+0]; vx[i] = 0.0; }
if (fixed[i*3+1]) { y[i] = pos_init[i*3+1]; vy[i] = 0.0; }
if (fixed[i*3+2]) { z[i] = pos_init[i*3+2]; vz[i] = 0.0; }
}
}
/* ══════════════════════════════════════════════════════════
* main.cpp leapfrog_step
* */
static void leapfrog_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0, double dt)
{
std::vector<double> buf(n * 3);
double *ax = buf.data(), *ay = ax+n, *az = ay+n;
accel_conservative(n, x, y, z, m, Gx, Gy, Gz,
gravity_field, elastic_force,
n_bonds, bp, bk, br0, ax, ay, az);
bool has_damp = damping_force && (Bx != 0.0 || By != 0.0 || Bz != 0.0);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
if (has_damp) {
double ax_ = Bx*dt/(2.0*m[i]);
double ay_ = By*dt/(2.0*m[i]);
double az_ = Bz*dt/(2.0*m[i]);
vx[i] = (vx[i]*(1.0-ax_) + ax[i]*dt) / (1.0+ax_);
vy[i] = (vy[i]*(1.0-ay_) + ay[i]*dt) / (1.0+ay_);
vz[i] = (vz[i]*(1.0-az_) + az[i]*dt) / (1.0+az_);
} else {
vx[i] += ax[i]*dt;
vy[i] += ay[i]*dt;
vz[i] += az[i]*dt;
}
x[i] += vx[i]*dt;
y[i] += vy[i]*dt;
z[i] += vz[i]*dt;
}
}
/* ══════════════════════════════════════════════════════════
*
* */
static void euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0, double dt)
{
std::vector<double> buf(n * 3);
double *ax = buf.data(), *ay = ax+n, *az = ay+n;
accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
x[i] += vx[i]*dt; y[i] += vy[i]*dt; z[i] += vz[i]*dt;
vx[i]+= ax[i]*dt; vy[i]+= ay[i]*dt; vz[i]+= az[i]*dt;
}
}
/* ══════════════════════════════════════════════════════════
* main.cpp implicit_euler_step
* */
static void implicit_euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0, double dt)
{
std::vector<double> vbuf(n * 3), abuf(n * 3);
double *vxn = vbuf.data(), *vyn = vxn+n, *vzn = vyn+n;
double *ax = abuf.data(), *ay = ax+n, *az = ay+n;
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) {
vxn[i] = vyn[i] = vzn[i] = 0.0; continue;
}
double gx = Bx/m[i], gy = By/m[i], gz = Bz/m[i];
vxn[i] = (vx[i] + Gx*dt) / (1.0 + gx*dt);
vyn[i] = (vy[i] + Gy*dt) / (1.0 + gy*dt);
vzn[i] = (vz[i] + Gz*dt) / (1.0 + gz*dt);
}
accel_full(n, x, y, z, vxn, vyn, vzn, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i]*dt; vy[i] += ay[i]*dt; vz[i] += az[i]*dt;
x[i] += vx[i]*dt; y[i] += vy[i]*dt; z[i] += vz[i]*dt;
}
}
/* ══════════════════════════════════════════════════════════
* main.cpp midpoint_step
* */
static void midpoint_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const int *fixed,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
int n_bonds, const int *bp, const double *bk, const double *br0, double dt)
{
std::vector<double> buf(n * 9);
double *ax = buf.data();
double *ay = ax+n; double *az = ay+n;
double *xm = az+n; double *ym = xm+n; double *zm = ym+n;
double *vxm = zm+n; double *vym = vxm+n; double *vzm = vym+n;
accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) {
xm[i]=x[i]; ym[i]=y[i]; zm[i]=z[i];
vxm[i]=vym[i]=vzm[i]=0.0; continue;
}
xm[i] = x[i] + 0.5*vx[i]*dt;
ym[i] = y[i] + 0.5*vy[i]*dt;
zm[i] = z[i] + 0.5*vz[i]*dt;
vxm[i] = vx[i] + 0.5*ax[i]*dt;
vym[i] = vy[i] + 0.5*ay[i]*dt;
vzm[i] = vz[i] + 0.5*az[i]*dt;
x[i] = x[i] + vxm[i]*dt;
y[i] = y[i] + vym[i]*dt;
z[i] = z[i] + vzm[i]*dt;
}
std::vector<double> abuf(n * 3);
double *axm = abuf.data(), *aym = axm+n, *azm = aym+n;
accel_full(n, xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz,
gravity_field, elastic_force, damping_force,
n_bonds, bp, bk, br0, axm, aym, azm);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += axm[i]*dt;
vy[i] += aym[i]*dt;
vz[i] += azm[i]*dt;
}
}
/* ── 驱动力 ─────────────────────────────────────────────── */
static void apply_driving(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
double t, int step, double dt, Drivers &drv)
{
(void)n;
if (drv.n_drivers == 0) return;
constexpr double TWO_PI = 2.0 * 3.14159265358979323846;
for (int d = 0; d < drv.n_drivers; d++) {
int idx = drv.idx[d];
double fx = drv.freq[d*3+0];
double fy = drv.freq[d*3+1];
double fz = drv.freq[d*3+2];
if (drv.has_period[d]) {
double mf = std::fabs(fx) > std::fabs(fy) ? std::fabs(fx) : std::fabs(fy);
if (std::fabs(fz) > mf) mf = std::fabs(fz);
int period_steps = 0;
if (mf > 1e-12)
period_steps = (int)(drv.ncycles[d] / mf / dt);
if (step > period_steps) {
x[idx] = drv.freeze[d*3+0];
y[idx] = drv.freeze[d*3+1];
z[idx] = drv.freeze[d*3+2];
vx[idx] = vy[idx] = vz[idx] = 0.0;
continue;
}
double px = drv.eq[d*3+0] + drv.amp[d*3+0]*std::cos(TWO_PI*fx*t + drv.phi[d*3+0]);
double py = drv.eq[d*3+1] + drv.amp[d*3+1]*std::cos(TWO_PI*fy*t + drv.phi[d*3+1]);
double pz = drv.eq[d*3+2] + drv.amp[d*3+2]*std::cos(TWO_PI*fz*t + drv.phi[d*3+2]);
if (step == period_steps) {
drv.freeze[d*3+0] = px;
drv.freeze[d*3+1] = py;
drv.freeze[d*3+2] = pz;
}
}
x[idx] = drv.eq[d*3+0] + drv.amp[d*3+0]*std::cos(TWO_PI*fx*t + drv.phi[d*3+0]);
y[idx] = drv.eq[d*3+1] + drv.amp[d*3+1]*std::cos(TWO_PI*fy*t + drv.phi[d*3+1]);
z[idx] = drv.eq[d*3+2] + drv.amp[d*3+2]*std::cos(TWO_PI*fz*t + drv.phi[d*3+2]);
vx[idx] = -drv.amp[d*3+0]*TWO_PI*fx*std::sin(TWO_PI*fx*t + drv.phi[d*3+0]);
vy[idx] = -drv.amp[d*3+1]*TWO_PI*fy*std::sin(TWO_PI*fy*t + drv.phi[d*3+1]);
vz[idx] = -drv.amp[d*3+2]*TWO_PI*fz*std::sin(TWO_PI*fz*t + drv.phi[d*3+2]);
}
}
/* ══════════════════════════════════════════════════════════
* run_dynamics C
* */
EXPORT int run_dynamics(
int n_atoms,
const double *pos_init,
const double *vel_init,
const double *masses,
const int *fixed,
int n_bonds,
const int *bond_pairs,
const double *bond_k,
const double *bond_r0,
double box_a, double dt,
int NT, int NSTEP, int warmup_steps, int method_id,
double Gx, double Gy, double Gz,
double Bx, double By, double Bz,
int gravity_field, int elastic_force, int damping_force,
double gravity_strength,
int n_drivers,
const int *drv_idx,
const double *drv_amp,
const double *drv_freq,
const double *drv_phi,
const double *drv_eq,
const double *drv_ncycles,
const int *drv_has_period,
int n_frames,
double *out_x, double *out_y, double *out_z,
double *out_vx, double *out_vy, double *out_vz,
void (*progress_cb)(int step, int total))
{
(void)gravity_strength;
int n = n_atoms;
std::vector<double> xv(n), yv(n), zv(n);
std::vector<double> vxv(n), vyv(n), vzv(n);
for (int i = 0; i < n; i++) {
xv[i]=pos_init[i*3+0]; yv[i]=pos_init[i*3+1]; zv[i]=pos_init[i*3+2];
vxv[i]=vel_init[i*3+0]; vyv[i]=vel_init[i*3+1]; vzv[i]=vel_init[i*3+2];
}
double *x=xv.data(), *y=yv.data(), *z=zv.data();
double *vx=vxv.data(), *vy=vyv.data(), *vz=vzv.data();
Drivers drv;
drv.n_drivers = n_drivers;
drv.idx = drv_idx;
drv.amp = drv_amp;
drv.freq = drv_freq;
drv.phi = drv_phi;
drv.eq = drv_eq;
drv.ncycles = drv_ncycles;
drv.has_period = drv_has_period;
if (n_drivers > 0)
drv.freeze.assign(n_drivers * 3, 0.0);
#define DO_STEP() do { \
switch (method_id) { \
case 0: euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
case 1: implicit_euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
case 2: midpoint_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
default: leapfrog_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \
gravity_field,elastic_force,damping_force, \
n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \
} \
apply_boundary_and_constraints(n,x,y,z,vx,vy,vz,fixed,pos_init,box_a); \
} while(0)
/* 蛙跳法:初始化 v(-dt/2) */
if (method_id == 3) {
std::vector<double> ibuf(n * 3);
double *ax0=ibuf.data(), *ay0=ax0+n, *az0=ay0+n;
accel_conservative(n, x, y, z, masses, Gx, Gy, Gz,
gravity_field, elastic_force,
n_bonds, bond_pairs, bond_k, bond_r0,
ax0, ay0, az0);
for (int i = 0; i < n; i++) {
if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] -= 0.5*ax0[i]*dt;
vy[i] -= 0.5*ay0[i]*dt;
vz[i] -= 0.5*az0[i]*dt;
}
}
/* 初始驱动 t=0 */
if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, 0.0, 0, dt, drv);
/* 预热 */
for (int s = 0; s < warmup_steps; s++) {
double tw = (s + 1) * dt;
if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, tw, s, dt, drv);
DO_STEP();
}
/* 记录循环 */
int record_steps = NT - warmup_steps;
int prog_interval = std::max(1, record_steps / 100);
int frame_idx = 0;
for (int s = 0; s < record_steps; s++) {
if (progress_cb && s % prog_interval == 0 && s > 0)
progress_cb(s, record_steps);
double t = (s + warmup_steps) * dt;
if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, t, s, dt, drv);
if (s % NSTEP == 0 && frame_idx < n_frames) {
int base = frame_idx * n;
for (int i = 0; i < n; i++) {
out_x [base+i] = x[i]; out_y [base+i] = y[i]; out_z [base+i] = z[i];
out_vx[base+i] = vx[i]; out_vy[base+i] = vy[i]; out_vz[base+i] = vz[i];
}
frame_idx++;
}
DO_STEP();
}
#undef DO_STEP
return 0;
}
+49
View File
@@ -0,0 +1,49 @@
# engines/src/fortran/Makefile
# 编译 DLL 到 engines/release/(主程序通过 ctypes 直接调用)
FC = gfortran
FFLAGS = -O3 -march=native -Wall -Wextra
LIB_SRC = dynamics_lib.f90
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)
# Windows 检测:Msys2/MINGW 也视为 Windows
IS_WINDOWS := $(findstring MINGW,$(UNAME_S))
ifneq ($(IS_WINDOWS),)
UNAME_S := Windows
endif
IS_WINDOWS := $(findstring MSYS,$(UNAME_S))
ifneq ($(IS_WINDOWS),)
UNAME_S := Windows
endif
# DLL 输出到 engines/release/
DLL_DIR = ../../release
ifeq ($(UNAME_S),Linux)
DLL_TARGET = $(DLL_DIR)/dynamics_f90.so
DLL_FLAGS = -shared -fPIC
else ifeq ($(UNAME_S),Darwin)
DLL_TARGET = $(DLL_DIR)/dynamics_f90.dylib
DLL_FLAGS = -dynamiclib
else
# Windows: 完全静态链接,避免依赖 libgfortran-5.dll
DLL_TARGET = $(DLL_DIR)/dynamics_f90.dll
DLL_FLAGS = -shared -fPIC -static
endif
.PHONY: all dll clean
all: dll
dll: $(DLL_TARGET)
$(DLL_TARGET): $(LIB_SRC) | $(DLL_DIR)
$(FC) $(FFLAGS) $(DLL_FLAGS) -o $@ $(LIB_SRC)
@echo " === Fortran DLL built: $@ ==="
$(DLL_DIR):
mkdir -p $(DLL_DIR)
clean:
rm -f $(DLL_TARGET)
+483
View File
@@ -0,0 +1,483 @@
! engines/fortran/dynamics_lib.f90
! ---------------------------------
! DLLFortran I/O Python ctypes
! main.f90 / compute.py
! 使 iso_c_binding C
!
! Windows:
! gfortran -O3 -march=native -shared -fPIC -o build/dynamics_f90.dll dynamics_lib.f90
! Linux:
! gfortran -O3 -march=native -shared -fPIC -o build/dynamics_f90.so dynamics_lib.f90
! macOS:
! gfortran -O3 -march=native -dynamiclib -o build/dynamics_f90.dylib dynamics_lib.f90
module dynamics_dll
use iso_c_binding, only: c_int, c_double, c_funptr, c_f_procpointer, c_associated
implicit none
private
real(c_double), parameter :: TWO_PI = 2.0d0 * 3.14159265358979323846d0
public :: run_dynamics
contains
!
subroutine accel_conservative(n, x, y, z, m, Gx, Gy, Gz, &
gravity_field, elastic_force, &
n_bonds, bond_pairs, bond_k, bond_r0, &
ax, ay, az)
integer, intent(in) :: n, gravity_field, elastic_force, n_bonds
real(c_double), intent(in) :: x(n), y(n), z(n), m(n)
real(c_double), intent(in) :: Gx, Gy, Gz
integer, intent(in) :: bond_pairs(2, n_bonds)
real(c_double), intent(in) :: bond_k(n_bonds), bond_r0(n_bonds)
real(c_double), intent(out) :: ax(n), ay(n), az(n)
integer :: b, ii, jj
real(c_double) :: dx, dy, dz, dist, fac, fx, fy, fz_b
if (gravity_field /= 0) then
ax = Gx; ay = Gy; az = Gz
else
ax = 0.0d0; ay = 0.0d0; az = 0.0d0
end if
if (elastic_force == 0 .or. n_bonds == 0) return
do b = 1, n_bonds
ii = bond_pairs(1, b) + 1 ! 0-based 1-based
jj = bond_pairs(2, b) + 1
dx = x(jj)-x(ii); dy = y(jj)-y(ii); dz = z(jj)-z(ii)
dist = sqrt(dx*dx + dy*dy + dz*dz)
if (dist < 1.0d-12) cycle
fac = bond_k(b) * (dist - bond_r0(b)) / dist
fx = fac*dx; fy = fac*dy; fz_b = fac*dz
ax(ii) = ax(ii) + fx/m(ii); ay(ii) = ay(ii) + fy/m(ii); az(ii) = az(ii) + fz_b/m(ii)
ax(jj) = ax(jj) - fx/m(jj); ay(jj) = ay(jj) - fy/m(jj); az(jj) = az(jj) - fz_b/m(jj)
end do
end subroutine
!
subroutine accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az)
integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds
real(c_double), intent(in) :: x(n), y(n), z(n), vx(n), vy(n), vz(n), m(n)
real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz
integer, intent(in) :: bond_pairs(2, n_bonds)
real(c_double), intent(in) :: bond_k(n_bonds), bond_r0(n_bonds)
real(c_double), intent(out) :: ax(n), ay(n), az(n)
integer :: i
call accel_conservative(n, x, y, z, m, Gx, Gy, Gz, &
gravity_field, elastic_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az)
if (damping_force /= 0) then
do i = 1, n
ax(i) = ax(i) - Bx*vx(i)/m(i)
ay(i) = ay(i) - By*vy(i)/m(i)
az(i) = az(i) - Bz*vz(i)/m(i)
end do
end if
end subroutine
! +
subroutine apply_bc(n, x, y, z, vx, vy, vz, fixed, pos_init, box_a)
integer, intent(in) :: n
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
integer, intent(in) :: fixed(3, n)
real(c_double), intent(in) :: pos_init(3, n), box_a
integer :: i
real(c_double) :: lo, hi
lo = -box_a; hi = box_a
!
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle
if (x(i)>hi) then; x(i)=hi; vx(i)=-abs(vx(i)); end if
if (x(i)<lo) then; x(i)=lo; vx(i)= abs(vx(i)); end if
if (y(i)>hi) then; y(i)=hi; vy(i)=-abs(vy(i)); end if
if (y(i)<lo) then; y(i)=lo; vy(i)= abs(vy(i)); end if
if (z(i)>hi) then; z(i)=hi; vz(i)=-abs(vz(i)); end if
if (z(i)<lo) then; z(i)=lo; vz(i)= abs(vz(i)); end if
end do
!
do i = 1, n
if (x(i)>hi) x(i)=lo; if (x(i)<lo) x(i)=hi
if (y(i)>hi) y(i)=lo; if (y(i)<lo) y(i)=hi
if (z(i)>hi) z(i)=lo; if (z(i)<lo) z(i)=hi
end do
!
do i = 1, n
if (fixed(1,i)/=0) then; x(i)=pos_init(1,i); vx(i)=0.0d0; end if
if (fixed(2,i)/=0) then; y(i)=pos_init(2,i); vy(i)=0.0d0; end if
if (fixed(3,i)/=0) then; z(i)=pos_init(3,i); vz(i)=0.0d0; end if
end do
end subroutine
!
subroutine leapfrog_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds)
integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds)
real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt
real(c_double) :: ax(n), ay(n), az(n), ax_, ay_, az_
logical :: has_damp
integer :: i
call accel_conservative(n, x, y, z, m, Gx, Gy, Gz, &
gravity_field, elastic_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az)
has_damp = (damping_force/=0) .and. (abs(Bx)+abs(By)+abs(Bz) > 0.0d0)
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle
if (has_damp) then
ax_ = Bx*dt/(2.0d0*m(i)); ay_ = By*dt/(2.0d0*m(i)); az_ = Bz*dt/(2.0d0*m(i))
vx(i) = (vx(i)*(1.0d0-ax_) + ax(i)*dt)/(1.0d0+ax_)
vy(i) = (vy(i)*(1.0d0-ay_) + ay(i)*dt)/(1.0d0+ay_)
vz(i) = (vz(i)*(1.0d0-az_) + az(i)*dt)/(1.0d0+az_)
else
vx(i) = vx(i)+ax(i)*dt; vy(i) = vy(i)+ay(i)*dt; vz(i) = vz(i)+az(i)*dt
end if
x(i) = x(i)+vx(i)*dt; y(i) = y(i)+vy(i)*dt; z(i) = z(i)+vz(i)*dt
end do
end subroutine
!
subroutine euler_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds)
integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds)
real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt
real(c_double) :: ax(n), ay(n), az(n)
integer :: i
call accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az)
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle
x(i) = x(i)+vx(i)*dt; y(i) = y(i)+vy(i)*dt; z(i) = z(i)+vz(i)*dt
vx(i)= vx(i)+ax(i)*dt; vy(i)= vy(i)+ay(i)*dt; vz(i)= vz(i)+az(i)*dt
end do
end subroutine
!
subroutine implicit_euler_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds)
integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds)
real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt
real(c_double) :: vxn(n), vyn(n), vzn(n), ax(n), ay(n), az(n)
real(c_double) :: gamma_x, gamma_y, gamma_z
integer :: i
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) then
vxn(i)=0.0d0; vyn(i)=0.0d0; vzn(i)=0.0d0; cycle
end if
gamma_x = Bx/m(i); gamma_y = By/m(i); gamma_z = Bz/m(i)
vxn(i) = (vx(i)+Gx*dt)/(1.0d0+gamma_x*dt)
vyn(i) = (vy(i)+Gy*dt)/(1.0d0+gamma_y*dt)
vzn(i) = (vz(i)+Gz*dt)/(1.0d0+gamma_z*dt)
end do
call accel_full(n, x, y, z, vxn, vyn, vzn, m, Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az)
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle
vx(i)=vx(i)+ax(i)*dt; vy(i)=vy(i)+ay(i)*dt; vz(i)=vz(i)+az(i)*dt
x(i) =x(i) +vx(i)*dt; y(i) =y(i) +vy(i)*dt; z(i) =z(i) +vz(i)*dt
end do
end subroutine
!
subroutine midpoint_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds)
integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds)
real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt
real(c_double) :: ax(n), ay(n), az(n)
real(c_double) :: xm(n), ym(n), zm(n), vxm(n), vym(n), vzm(n)
real(c_double) :: axm(n), aym(n), azm(n)
integer :: i
call accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az)
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) then
xm(i)=x(i); ym(i)=y(i); zm(i)=z(i)
vxm(i)=0.0d0; vym(i)=0.0d0; vzm(i)=0.0d0; cycle
end if
xm(i) = x(i) +0.5d0*vx(i)*dt; ym(i) = y(i) +0.5d0*vy(i)*dt; zm(i) = z(i) +0.5d0*vz(i)*dt
vxm(i) = vx(i)+0.5d0*ax(i)*dt; vym(i) = vy(i)+0.5d0*ay(i)*dt; vzm(i) = vz(i)+0.5d0*az(i)*dt
x(i) = x(i) +vxm(i)*dt; y(i) = y(i) +vym(i)*dt; z(i) = z(i) +vzm(i)*dt
end do
call accel_full(n, xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, axm, aym, azm)
do i = 1, n
if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle
vx(i)=vx(i)+axm(i)*dt; vy(i)=vy(i)+aym(i)*dt; vz(i)=vz(i)+azm(i)*dt
end do
end subroutine
!
subroutine apply_drive(n, x, y, z, vx, vy, vz, t, step, dt, &
nd, drv_idx, drv_amp, drv_freq, drv_phi, &
drv_eq, drv_ncycles, drv_has_period, freeze)
integer, intent(in) :: n, nd, step
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
real(c_double), intent(in) :: t, dt
integer, intent(in) :: drv_idx(nd), drv_has_period(nd)
real(c_double), intent(in) :: drv_amp(3,nd), drv_freq(3,nd)
real(c_double), intent(in) :: drv_phi(3,nd), drv_eq(3,nd)
real(c_double), intent(in) :: drv_ncycles(nd)
real(c_double), intent(inout) :: freeze(3,nd)
integer :: d, idx, ps
real(c_double) :: fx, fy, fz, mf, px, py, pz
do d = 1, nd
idx = drv_idx(d) + 1 ! 0-based 1-based
fx = drv_freq(1,d); fy = drv_freq(2,d); fz = drv_freq(3,d)
if (drv_has_period(d) /= 0) then
mf = max(abs(fx), max(abs(fy), abs(fz)))
ps = 0
if (mf > 1.0d-12) ps = int(drv_ncycles(d)/mf/dt)
if (step > ps) then
x(idx)=freeze(1,d); y(idx)=freeze(2,d); z(idx)=freeze(3,d)
vx(idx)=0.0d0; vy(idx)=0.0d0; vz(idx)=0.0d0
cycle
end if
px = drv_eq(1,d)+drv_amp(1,d)*cos(TWO_PI*fx*t+drv_phi(1,d))
py = drv_eq(2,d)+drv_amp(2,d)*cos(TWO_PI*fy*t+drv_phi(2,d))
pz = drv_eq(3,d)+drv_amp(3,d)*cos(TWO_PI*fz*t+drv_phi(3,d))
if (step == ps) then
freeze(1,d)=px; freeze(2,d)=py; freeze(3,d)=pz
end if
end if
x(idx) = drv_eq(1,d)+drv_amp(1,d)*cos(TWO_PI*fx*t+drv_phi(1,d))
y(idx) = drv_eq(2,d)+drv_amp(2,d)*cos(TWO_PI*fy*t+drv_phi(2,d))
z(idx) = drv_eq(3,d)+drv_amp(3,d)*cos(TWO_PI*fz*t+drv_phi(3,d))
vx(idx) = -drv_amp(1,d)*TWO_PI*fx*sin(TWO_PI*fx*t+drv_phi(1,d))
vy(idx) = -drv_amp(2,d)*TWO_PI*fy*sin(TWO_PI*fy*t+drv_phi(2,d))
vz(idx) = -drv_amp(3,d)*TWO_PI*fz*sin(TWO_PI*fz*t+drv_phi(3,d))
end do
end subroutine
!
! run_dynamicsC bind(C)
! C/C++ DLL C-contiguous
!
integer(c_int) function run_dynamics( &
n_atoms, pos_init, vel_init, masses, fixed, &
n_bonds, bond_pairs, bond_k, bond_r0, &
box_a, dt, NT, NSTEP, warmup_steps, method_id, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, gravity_strength, &
n_drivers, drv_idx, drv_amp, drv_freq, drv_phi, drv_eq, &
drv_ncycles, drv_has_period, &
n_frames, out_x, out_y, out_z, out_vx, out_vy, out_vz, &
progress_cb) &
bind(C, name="run_dynamics")
integer(c_int), value, intent(in) :: n_atoms, n_bonds, NT, NSTEP
integer(c_int), value, intent(in) :: warmup_steps, method_id
integer(c_int), value, intent(in) :: gravity_field, elastic_force, damping_force
integer(c_int), value, intent(in) :: n_drivers, n_frames
real(c_double), value, intent(in) :: box_a, dt
real(c_double), value, intent(in) :: Gx, Gy, Gz, Bx, By, Bz
real(c_double), value, intent(in) :: gravity_strength
! Python C-contiguous int32/float64
! Fortran (3,n) C n×3
real(c_double), intent(in) :: pos_init(3, n_atoms)
real(c_double), intent(in) :: vel_init(3, n_atoms)
real(c_double), intent(in) :: masses(n_atoms)
integer(c_int), intent(in) :: fixed(3, n_atoms)
integer(c_int), intent(in) :: bond_pairs(2, n_bonds)
real(c_double), intent(in) :: bond_k(n_bonds), bond_r0(n_bonds)
integer(c_int), intent(in) :: drv_idx(n_drivers)
real(c_double), intent(in) :: drv_amp(3, n_drivers)
real(c_double), intent(in) :: drv_freq(3, n_drivers)
real(c_double), intent(in) :: drv_phi(3, n_drivers)
real(c_double), intent(in) :: drv_eq(3, n_drivers)
real(c_double), intent(in) :: drv_ncycles(n_drivers)
integer(c_int), intent(in) :: drv_has_period(n_drivers)
real(c_double), intent(out) :: out_x(n_atoms, n_frames)
real(c_double), intent(out) :: out_y(n_atoms, n_frames)
real(c_double), intent(out) :: out_z(n_atoms, n_frames)
real(c_double), intent(out) :: out_vx(n_atoms, n_frames)
real(c_double), intent(out) :: out_vy(n_atoms, n_frames)
real(c_double), intent(out) :: out_vz(n_atoms, n_frames)
type(c_funptr), value, intent(in) :: progress_cb
!
abstract interface
subroutine cb_iface(step, total) bind(C)
use iso_c_binding
integer(c_int), value :: step, total
end subroutine
end interface
procedure(cb_iface), pointer :: cb_ptr
integer :: n, s, frame_idx, record_steps, prog_interval, nd
real(c_double) :: t, tw
real(c_double), allocatable :: x(:), y(:), z(:), vx(:), vy(:), vz(:)
real(c_double), allocatable :: ax0(:), ay0(:), az0(:)
real(c_double), allocatable :: freeze(:,:)
logical :: has_cb
n = n_atoms
nd = n_drivers
allocate(x(n), y(n), z(n), vx(n), vy(n), vz(n))
do s = 1, n
x(s) = pos_init(1,s); y(s) = pos_init(2,s); z(s) = pos_init(3,s)
vx(s) = vel_init(1,s); vy(s) = vel_init(2,s); vz(s) = vel_init(3,s)
end do
allocate(freeze(3, max(nd,1)))
freeze = 0.0d0
has_cb = c_associated(progress_cb)
if (has_cb) call c_f_procpointer(progress_cb, cb_ptr)
! v(-dt/2)
if (method_id == 3) then
allocate(ax0(n), ay0(n), az0(n))
call accel_conservative(n, x, y, z, masses, Gx, Gy, Gz, &
gravity_field, elastic_force, &
n_bonds, bond_pairs, bond_k, bond_r0, ax0, ay0, az0)
do s = 1, n
if (fixed(1,s)/=0 .and. fixed(2,s)/=0 .and. fixed(3,s)/=0) cycle
vx(s)=vx(s)-0.5d0*ax0(s)*dt
vy(s)=vy(s)-0.5d0*ay0(s)*dt
vz(s)=vz(s)-0.5d0*az0(s)*dt
end do
deallocate(ax0, ay0, az0)
end if
! t=0
if (nd > 0) call apply_drive(n, x, y, z, vx, vy, vz, 0.0d0, 0, dt, &
nd, drv_idx, drv_amp, drv_freq, drv_phi, &
drv_eq, drv_ncycles, drv_has_period, freeze)
!
do s = 0, warmup_steps-1
tw = (s+1)*dt
if (nd>0) call apply_drive(n, x, y, z, vx, vy, vz, tw, s, dt, &
nd, drv_idx, drv_amp, drv_freq, drv_phi, &
drv_eq, drv_ncycles, drv_has_period, freeze)
call do_step(n, x, y, z, vx, vy, vz, masses, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt, method_id, &
pos_init, box_a)
end do
!
record_steps = NT - warmup_steps
prog_interval = max(1, record_steps/100)
frame_idx = 0
do s = 0, record_steps-1
if (has_cb .and. mod(s, prog_interval)==0 .and. s>0) call cb_ptr(s, record_steps)
t = (s+warmup_steps)*dt
if (nd>0) call apply_drive(n, x, y, z, vx, vy, vz, t, s, dt, &
nd, drv_idx, drv_amp, drv_freq, drv_phi, &
drv_eq, drv_ncycles, drv_has_period, freeze)
if (mod(s, NSTEP)==0 .and. frame_idx<n_frames) then
frame_idx = frame_idx+1
out_x(:, frame_idx) = x
out_y(:, frame_idx) = y
out_z(:, frame_idx) = z
out_vx(:, frame_idx) = vx
out_vy(:, frame_idx) = vy
out_vz(:, frame_idx) = vz
end if
call do_step(n, x, y, z, vx, vy, vz, masses, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt, method_id, &
pos_init, box_a)
end do
deallocate(x, y, z, vx, vy, vz, freeze)
run_dynamics = 0
contains
subroutine do_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt, method_id, &
pos_init, box_a)
integer, intent(in) :: n, gravity_field, elastic_force, damping_force
integer, intent(in) :: n_bonds, method_id
real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds)
integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds)
real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt, box_a
real(c_double), intent(in) :: pos_init(3, n)
select case (method_id)
case (0)
call euler_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
case (1)
call implicit_euler_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
case (2)
call midpoint_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
case default
call leapfrog_step(n, x, y, z, vx, vy, vz, m, fixed, &
Gx, Gy, Gz, Bx, By, Bz, &
gravity_field, elastic_force, damping_force, &
n_bonds, bond_pairs, bond_k, bond_r0, dt)
end select
call apply_bc(n, x, y, z, vx, vy, vz, fixed, pos_init, box_a)
end subroutine do_step
end function run_dynamics
end module dynamics_dll
+449
View File
@@ -0,0 +1,449 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamics 示例案例总览</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #c9d1d9;
--text-dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 40px 24px;
}
.container { max-width: 960px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 8px; color: #f0f6fc; }
h1 small { font-size: 1rem; color: var(--text-dim); font-weight: 400; }
.subtitle { color: var(--text-dim); margin-bottom: 32px; }
h2 {
font-size: 1.4rem;
margin-top: 40px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
color: #f0f6fc;
}
/* 案例总览表格 */
.case-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.case-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
transition: border-color 0.2s, transform 0.2s;
}
.case-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
}
.case-card .num {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 4px;
background: var(--accent);
color: #0d1117;
margin-bottom: 8px;
}
.case-card h3 {
font-size: 1.05rem;
margin-bottom: 6px;
color: #f0f6fc;
}
.case-card p {
font-size: 0.875rem;
color: var(--text-dim);
margin-bottom: 10px;
}
.case-card .meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
font-size: 0.75rem;
}
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
}
.tag-python { background: #3572A533; color: #3572A5; }
.tag-c { background: #55555533; color: #aaa; }
.tag-fortran { background: #73422233; color: #e9954a; }
.tag-gravity { background: #d2992233; color: var(--orange); }
.tag-spring { background: #3fb95033; color: var(--green); }
.tag-drive { background: #58a6ff33; color: var(--accent); }
.tag-warning { background: #f8514933; color: var(--red); }
/* 详情区域 */
.detail-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px 24px;
margin-bottom: 16px;
}
.detail-card h3 {
font-size: 1.1rem;
margin-bottom: 8px;
color: #f0f6fc;
}
.detail-card h3 a { color: var(--accent); text-decoration: none; }
.detail-card h3 a:hover { text-decoration: underline; }
.detail-card p { color: var(--text-dim); margin-bottom: 8px; }
.detail-card ul {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 6px;
}
.detail-card li { font-size: 0.8rem; }
.detail-card .highlight {
background: #1f242e;
border-left: 3px solid var(--accent);
padding: 8px 12px;
margin-top: 8px;
border-radius: 0 4px 4px 0;
font-size: 0.875rem;
color: var(--text);
}
/* 使用指南 */
.guide {
background: #1f242e;
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px 24px;
margin-bottom: 32px;
}
.guide h3 { color: #f0f6fc; margin-bottom: 12px; }
.guide code {
display: block;
background: #0d1117;
padding: 12px 16px;
border-radius: 6px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 12px;
color: var(--green);
}
.guide table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.guide th, .guide td {
text-align: left;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
}
.guide th { color: var(--text-dim); font-weight: 600; }
.guide td:first-child { color: var(--accent); font-weight: 500; }
@media (max-width: 640px) {
body { padding: 16px; }
.case-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<h1>Dynamics 示例案例 <small>v2.1</small></h1>
<p class="subtitle">10 个从简单到复杂的物理模拟案例,展示分子动力学模拟框架的多种应用场景</p>
<h2>📋 案例总览</h2>
<div class="case-grid">
<div class="case-card">
<span class="num">01</span>
<h3>双粒子弹簧系统</h3>
<p>两个原子由弹簧连接,在重力场中运动</p>
<div class="meta">
<span class="tag tag-python">Python</span>
<span class="tag tag-spring">弹簧</span>
<span class="tag tag-gravity">重力</span>
</div>
</div>
<div class="case-card">
<span class="num">02</span>
<h3>行星运动</h3>
<p>地球绕太阳椭圆公转(万有引力)</p>
<div class="meta">
<span class="tag tag-python">Python</span>
<span class="tag tag-gravity">万有引力</span>
</div>
</div>
<div class="case-card">
<span class="num">03</span>
<h3>日地月系统(失稳)</h3>
<p>三体系统参数不当导致轨道发散</p>
<div class="meta">
<span class="tag tag-python">Python</span>
<span class="tag tag-gravity">万有引力</span>
<span class="tag tag-warning">失败案例</span>
</div>
</div>
<div class="case-card">
<span class="num">04</span>
<h3>日地月系统(稳定)</h3>
<p>三体系统稳定轨道,经希尔半径检验</p>
<div class="meta">
<span class="tag tag-python">Python</span>
<span class="tag tag-gravity">万有引力</span>
</div>
</div>
<div class="case-card">
<span class="num">05</span>
<h3>一维原子链纵波</h3>
<p>驱动原子 1 沿 x 振动,产生纵波传播</p>
<div class="meta">
<span class="tag tag-python">Python</span>
<span class="tag tag-spring">弹簧</span>
<span class="tag tag-drive">驱动力</span>
</div>
</div>
<div class="case-card">
<span class="num">06</span>
<h3>一维原子链横波</h3>
<p>带阻尼的横波传播(FPU 非线性)</p>
<div class="meta">
<span class="tag tag-c">C 引擎</span>
<span class="tag tag-spring">弹簧</span>
<span class="tag tag-drive">驱动力</span>
</div>
</div>
<div class="case-card">
<span class="num">07</span>
<h3>一维链横波·双端驱动</h3>
<p>原子 1 + 原子 120 同时驱动,波相遇干涉</p>
<div class="meta">
<span class="tag tag-c">C 引擎</span>
<span class="tag tag-spring">弹簧</span>
<span class="tag tag-drive">驱动力</span>
</div>
</div>
<div class="case-card">
<span class="num">08</span>
<h3>双原子弹簧·C 引擎测试</h3>
<p>2 原子快速验证 C 引擎正确性</p>
<div class="meta">
<span class="tag tag-c">C 引擎</span>
<span class="tag tag-spring">弹簧</span>
</div>
</div>
<div class="case-card">
<span class="num">09</span>
<h3>一维链纵波·Fortran 引擎</h3>
<p>Fortran 引擎驱动的纵波,GPU 实例化渲染</p>
<div class="meta">
<span class="tag tag-fortran">Fortran</span>
<span class="tag tag-spring">弹簧</span>
<span class="tag tag-drive">驱动力</span>
</div>
</div>
<div class="case-card">
<span class="num">10</span>
<h3>一维链纵波·能量分析</h3>
<p>纵波传播 + 轨迹/能量图绘制</p>
<div class="meta">
<span class="tag tag-c">C 引擎</span>
<span class="tag tag-spring">弹簧</span>
<span class="tag tag-drive">驱动力</span>
</div>
</div>
</div>
<h2>📖 各案例详情</h2>
<div class="detail-card">
<h3><a href="./case01/">case01 — 双粒子弹簧系统</a></h3>
<ul>
<li class="tag tag-python">Python 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
<li class="tag tag-gravity">重力场</li>
</ul>
<p>两个原子通过弹簧连接,在均匀重力场(G=[0,0,-9.8])中自由运动。展示重力作用下的耦合振动与落体运动的复合。</p>
<div class="highlight">🔬 教学案例:算法 leapfrog,渲染 Sphere 模式,建议作为入门第一个案例</div>
</div>
<div class="detail-card">
<h3><a href="./case02/">case02 — 行星运动</a></h3>
<ul>
<li class="tag tag-python">Python 引擎</li>
<li class="tag tag-gravity">万有引力</li>
</ul>
<p>模拟地球绕太阳的椭圆轨道运动。大质量中心体固定,小质量体绕行。</p>
<div class="highlight">🌍 万有引力强度 gravity_strength=100.0leapfrog 算法确保能量守恒</div>
</div>
<div class="detail-card">
<h3><a href="./case03/">case03 — 日地月系统(失败案例)</a></h3>
<ul>
<li class="tag tag-python">Python 引擎</li>
<li class="tag tag-gravity">万有引力</li>
<li class="tag tag-warning">失稳</li>
</ul>
<p>三体系统(太阳-地球-月球)。初始条件或参数设置不当,轨道不稳定。展示数值模拟中参数选择的重要性。</p>
</div>
<div class="detail-card">
<h3><a href="./case04/">case04 — 日地月系统(成功案例)</a></h3>
<ul>
<li class="tag tag-python">Python 引擎</li>
<li class="tag tag-gravity">万有引力</li>
</ul>
<p>与 case03 相同的三体系统,但采用恰当的初始条件:地球置于近日点($r=10$),$v_z=520$ 接近圆轨道;月球缩至希尔半径($r_H\approx1.0$)以内的 $r=0.5$,相对速度 $v_{\text{rel}}\approx127$,确保月球被地球稳定束缚。</p>
<div class="highlight">✅ 与 case03 对比学习:初始条件对数值稳定性的影响。地月距 0.5 在地球希尔半径以内,满足稳定性条件</div>
</div>
<div class="detail-card">
<h3><a href="./case05/">case05 — 一维原子链纵波</a></h3>
<ul>
<li class="tag tag-python">Python 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
<li class="tag tag-drive">驱动力</li>
</ul>
<p>60 原子沿 x 轴排列。原子 1 受 x 方向驱动力,产生沿链传播的纵波(压缩波)。原子 x 自由,y/z 锁定。</p>
<div class="highlight">📈 纵波波速快(x 方向弹簧力线性),T_total=10, NSTEP=50</div>
</div>
<div class="detail-card">
<h3><a href="./case06/">case06 — 一维原子链横波(带阻尼)</a></h3>
<ul>
<li class="tag tag-c">C 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
<li class="tag tag-drive">驱动力</li>
</ul>
<p>120 原子沿 x 轴排列,带横向阻尼。驱动沿 z 方向,原子 z 自由,x/y 锁定。横波传播具有 FPU 型非线性。</p>
<div class="highlight">⚡ C 引擎高性能计算,T_total=1000, NSTEP=500,支持运动相机</div>
</div>
<div class="detail-card">
<h3><a href="./case07/">case07 — 一维链横波·双端驱动</a></h3>
<ul>
<li class="tag tag-c">C 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
<li class="tag tag-drive">驱动力</li>
</ul>
<p>120 原子,原子 1 和原子 120 同时受 z 方向驱动(同频率、相位差 90°),两端向中间传播的横波相遇。</p>
<div class="highlight">🌊 波干涉演示,视觉放大 display_amp=[1,1,10] 便于观察小幅度振动</div>
</div>
<div class="detail-card">
<h3><a href="./case08/">case08 — 双原子弹簧(C 引擎快速测试)</a></h3>
<ul>
<li class="tag tag-c">C 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
</ul>
<p>2 原子弹簧系统,T_total=100 的短时模拟。用于快速验证 C 引擎的正确性和性能。</p>
<div class="highlight">🧪 引擎快速验证用例,无动画输出</div>
</div>
<div class="detail-card">
<h3><a href="./case09/">case09 — 一维链纵波·Fortran 引擎</a></h3>
<ul>
<li class="tag tag-fortran">Fortran 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
<li class="tag tag-drive">驱动力</li>
</ul>
<p>40 原子沿 x 轴排列,Fortran 引擎驱动的纵波传播测试。使用 <code>use_marker: 1</code>GPU 实例化 Marker 模式)加速渲染,验证 Fortran 引擎与其他引擎的输出兼容性。</p>
<div class="highlight">🔧 Fortran 引擎兼容性验证,T_total=200, NSTEP=100。Marker 模式下 40 原子以 GPU 点精灵渲染,帧率高</div>
</div>
<div class="detail-card">
<h3><a href="./case10/">case10 — 一维链纵波·能量分析</a></h3>
<ul>
<li class="tag tag-c">C 引擎</li>
<li class="tag tag-spring">弹簧键力</li>
<li class="tag tag-drive">驱动力</li>
</ul>
<p>40 原子纵波传播,支持轨迹/能量图绘制(step_plot=1)。原子 x/y/z 全部自由。</p>
<div class="highlight">📊 能量分析演示,T_total=10, NSTEP=20</div>
</div>
<h2>🚀 使用方法</h2>
<div class="guide">
<h3>命令行</h3>
<code># 进入案例目录并运行<br>cd examples/case05<br>python run_dynamics.py<br><br># 仅运行模拟,跳过动画<br>python run_dynamics.py --no-plot<br><br># 手动启动 3D 动画<br>python ../../draw.py output/</code>
<h3>案例选择指南</h3>
<table>
<tr><th>目标</th><th>推荐案例</th></tr>
<tr><td>快速上手框架</td><td>case01</td></tr>
<tr><td>天体力学/万有引力</td><td>case02 / case04</td></tr>
<tr><td>波动物理(纵波)</td><td>case05</td></tr>
<tr><td>波动物理(横波/非线性/阻尼)</td><td>case06</td></tr>
<tr><td>双端驱动/波干涉</td><td>case07</td></tr>
<tr><td>引擎性能对比</td><td>case08 (C) / case09 (Fortran)</td></tr>
<tr><td>能量分析</td><td>case10</td></tr>
</table>
<h3>配置</h3>
<p style="color:var(--text-dim); font-size:0.875rem;">
每个案例的 <code>input/input.txt</code> 可配置物理参数、力开关、算法、引擎、渲染方式等。
</p>
<h3>引擎架构</h3>
<p style="color:var(--text-dim); font-size:0.875rem;">
外部引擎(C / C++ / Fortran)以 <strong>DLL 方式</strong> 运行,主程序通过 ctypes 在进程内直接调用,不启动子进程。DLL 预编译在 <code>engines/release/</code> 中,源码位于 <code>engines/src/{c,cpp,fortran}/</code>。重新编译:
<code>cd engines/src/c && make dll</code>
</p>
</div>
<h2>📁 框架结构</h2>
<div class="detail-card">
<pre style="font-size:0.825rem; color:var(--text-dim); line-height:1.5;">
dynamics/
├── dynamics.py # 统一运行入口
├── compute.py # 物理引擎 + 显示数据生成
├── draw.py # VisPy 3D 动画
├── plot_wave.py # 波形能量图
├── .gitattributes # DLL/二进制文件保护
├── engines/
│ ├── engine_dll.py # DLL 加载器(ctypes
│ ├── python/ # Python 引擎(dynamics_lib.py
│ ├── release/ # 预编译 DLLC / C++ / Fortran
│ └── src/ # 引擎源码
│ ├── c/ # C 源码 + Makefile → dynamics_c.dll
│ ├── cpp/ # C++ 源码 + Makefile → dynamics_cpp.dll
│ └── fortran/ # Fortran 源码 + Makefile → dynamics_f90.dll
├── examples/ # 案例目录
│ ├── case01/ ~ case10/
└── output/ # 默认输出目录
</pre>
</div>
</div>
</body>
</html>
+73 -16
View File
@@ -1,19 +1,23 @@
# Dynamics 示例案例
本目录包含 6 个从简单到复杂的物理模拟案例,均基于 `../dynamics.py` 框架运行。
本目录包含 10 个从简单到复杂的物理模拟案例,均基于 `../dynamics.py` 框架运行。
---
## 案例一览
| 案例 | 标题 | 简介 | 原子数 | 力类型 |
|---|---|---|---|---|
| [case01](./case01/) | **双粒子弹簧系统** | 两个原子由弹簧连接,在重力场中运动 | 2 | 重力 + 弹簧 |
| [case02](./case02/) | **行星运动** | 地球绕太阳椭圆公转(万有引力) | 2 | 万有引力 |
| [case03](./case03/) | **日地月系统(失** | 地球绕太阳、月球绕地球,参数不当导致失稳 | 3 | 万有引力 |
| [case04](./case04/) | **日地月系统(成功** | 地球绕太阳、月球绕地球,稳定轨道 | 3 | 万有引力 |
| [case05](./case05/) | **一维原子链纵波** | 驱动原子 1 沿 x 轴振动,产生纵波传播 | 60 | 弹簧 + 驱动力 |
| [case06](./case06/) | **一维原子链横波** | 驱动原子 1 沿 z 轴振动,产生横波传播 | 120 | 弹簧 + 驱动力 |
| 案例 | 标题 | 简介 | 原子数 | 引擎 | 力类型 |
|------|------|------|--------|------|--------|
| [case01](./case01/) | **双粒子弹簧系统** | 两个原子由弹簧连接,在重力场中运动 | 2 | Python | 重力 + 弹簧 |
| [case02](./case02/) | **行星运动** | 地球绕太阳椭圆公转(万有引力) | 2 | Python | 万有引力 |
| [case03](./case03/) | **日地月系统(失** | 地球绕太阳、月球绕地球,参数不当导致失稳 | 3 | Python | 万有引力 |
| [case04](./case04/) | **日地月系统(稳定** | 地球绕太阳、月球绕地球,稳定轨道 | 3 | Python | 万有引力 |
| [case05](./case05/) | **一维原子链纵波** | 驱动原子 1 沿 x 轴振动,产生纵波传播 | 60 | Python | 弹簧 + 驱动力 |
| [case06](./case06/) | **一维原子链横波(阻尼)** | 驱动原子 1 沿 z 轴振动,带阻尼的横波传播 | 120 | C | 弹簧 + 阻尼 + 驱动力 |
| [case07](./case07/) | **一维原子链横波(双端驱动)** | 原子 1 和原子 120 同时受 z 方向驱动 | 120 | C | 弹簧 + 阻尼 + 驱动力 |
| [case08](./case08/) | **双原子弹簧(C 引擎测试)** | 两个原子弹簧系统,C 引擎快速验证 | 2 | C | 弹簧 + 阻尼 + 驱动力 |
| [case09](./case09/) | **一维链纵波(Fortran 引擎)** | Fortran 引擎驱动的纵波传播测试 | 40 | Fortran | 弹簧 + 驱动力 |
| [case10](./case10/) | **一维链纵波(能量分析)** | 纵波传播 + 轨迹/能量图绘制 | 40 | C | 弹簧 + 驱动力 |
---
@@ -26,14 +30,16 @@
- **力开关**:重力场开,弹簧键力开
- **算法**leapfrog(蛙跳法)
- **物理**:重力 m·g + 弹簧胡克力
- **渲染**Sphere 模式(精细网格球体)
### case02 — 行星运动
模拟地球绕太阳的椭圆轨道运动(一个固定大质量中心体 + 一个绕行小质量体)。采用万有引力相互作用。
- **力开关**:万有引力开(含强度参数)
- **力开关**:万有引力开(含强度参数 `gravity_strength: 100.0`
- **算法**leapfrog(蛙跳法)
- **物理**:牛顿万有引力 F = G·m₁·m₂/r²
- **渲染**Sphere 模式
### case03 — 日地月系统(失败案例)
@@ -60,14 +66,53 @@
- **波速**:快(x 方向弹簧力为线性)
- **渲染**Marker 模式(GPU 实例化,60 原子)
### case06 — 一维原子链横波
### case06 — 一维原子链横波(带阻尼)
120 个原子沿 x 轴等间距排列(间距 1),相邻原子用弹簧(k=1.0, L₀=1.0)连接。驱动力沿 z 方向 `z(t)=0.5·cos(2π·0.1·t+90°)`,原子 z 方向自由(fix_z=0),x/y 锁定。振动在横向传播,形成**横波**。
120 个原子沿 x 轴等间距排列(间距 1),相邻原子用弹簧(k=1.0, L₀=1.0)连接,带横向阻尼(B=[0.01, 0, 0.01]。驱动 z 方向 `z(t)=0.5·cos(2π·0.1·t+90°)`,原子 z 方向自由(fix_z=0),x/y 锁定。
- **力开关**:弹簧键力开,驱动力开
- **力开关**:弹簧键力开,驱动力开,阻尼开
- **算法**leapfrog(蛙跳法)
- **引擎**C(高性能)
- **参数**T_total=1000, NSTEP=500
- **波速**:慢(z 方向弹簧力呈几何非线性,类似 FPU 系统)
- **渲染**Marker 模式(GPU 实例化,120 原子)
### case07 — 一维原子链横波(双端驱动)
120 个原子沿 x 轴排列,原子 1 **和**原子 120 同时受 z 方向驱动力驱动(频率相同,初相位错开 90°),产生两端向中间传播的横波相遇。
- **力开关**:弹簧键力开,驱动力开,阻尼开
- **引擎**C
- **参数**T_total=1000, NSTEP=500
- **视觉放大**z 方向位移放大 10 倍(`display_amp: [1, 1, 10]`),便于观察小幅度横波
### case08 — 双原子弹簧(C 引擎快速测试)
2 个原子的简单弹簧系统,用于快速验证 C 引擎的正确性和性能。T_total 仅为 100s。
- **力开关**:弹簧键力开,驱动力开,阻尼开
- **引擎**C
- **用途**:引擎验证 / 调试
- **视觉放大**z 方向位移放大 10 倍
- **动画**:关闭(step_animation=0),仅输出波形图
### case09 — 一维链纵波(Fortran 引擎)
40 个原子沿 x 轴排列,使用 **Fortran 引擎**驱动的纵波传播模拟。验证 Fortran 引擎的输出兼容性和性能。
- **力开关**:弹簧键力开,驱动力开,阻尼关
- **引擎**Fortran
- **参数**T_total=200, NSTEP=100
- **视觉放大**z 方向位移放大 10 倍
### case10 — 一维链纵波(能量分析)
40 个原子沿 x 轴排列,纵波传播。与 case05 相比,x/y/z 全部自由(fix_x/y/z 均为 0),物理行为更复杂。支持轨迹/能量图绘制(step_plot=1)。
- **力开关**:弹簧键力开,驱动力开,阻尼关
- **引擎**C
- **参数**T_total=10, NSTEP=20
- **视觉放大**z 方向位移放大 10 倍
- **动画**:关闭(step_animation=0),仅输出轨迹/能量图
---
@@ -77,7 +122,7 @@
# 进入任意案例目录
cd examples/case05
# 完整运行(模拟 + 采样 + 3D 动画)
# 完整运行(模拟 + 3D 动画)
python run_dynamics.py
# 仅运行模拟,跳过 3D 动画
@@ -89,6 +134,18 @@ python ../../draw.py output/
每个案例的 `input/input.txt` 中可配置所有物理参数、力开关、算法、渲染方式等。
## 案例选择指南
| 你想做什么 | 推荐案例 |
|-----------|---------|
| 快速上手、理解基本框架 | case01 |
| 天体力学 / 万有引力 | case02 / case04 |
| 波动物理(纵波) | case05 |
| 波动物理(横波、非线性、阻尼) | case06 |
| 双端驱动/波干涉 | case07 |
| 对比不同引擎性能 | case08 (C) / case09 (Fortran) |
| 能量分析 | case10 |
## 框架结构
```
@@ -101,6 +158,6 @@ dynamics/
├── examples/ # 案例(本目录)
│ ├── case01/
│ ├── ...
│ └── case06/
│ └── case10/
└── output/ # 默认输出目录
```
+92
View File
@@ -0,0 +1,92 @@
"""
为指定案例添加次紧邻键 (k2, k=100, r0=1.41421356)
用法: python add_k2.py case16
python add_k2.py case16 case17 case18
python add_k2.py --all
"""
import os
import sys
def add_k2(case_dir):
coord_path = os.path.join(case_dir, "input", "coord.txt")
conn_path = os.path.join(case_dir, "input", "connection.txt")
bond_path = os.path.join(case_dir, "input", "bond.txt")
if not os.path.exists(coord_path):
print(f" [跳过] {case_dir}: 找不到 coord.txt")
return False
# 读取 coord.txt 获取网格尺寸
with open(coord_path, "r", encoding="utf-8") as f:
lines = f.readlines()
n_atoms = len(lines) - 1 # 去掉表头
N = int(n_atoms ** 0.5)
if N * N != n_atoms:
print(f" [跳过] {case_dir}: 非正方形网格 (n_atoms={n_atoms})")
return False
print(f" {case_dir}: {N}x{N} 网格")
# 读取现有 connection.txt,检查是否已有 k2
has_k2 = False
if os.path.exists(conn_path):
with open(conn_path, "r") as f:
for line in f:
if "k2" in line:
has_k2 = True
break
if has_k2:
print(f" k2 已存在,跳过 connection.txt")
else:
# 追加 k2 键到 connection.txt
with open(conn_path, "a", encoding="utf-8") as f:
cnt = 0
for row in range(N):
for col in range(N):
id1 = row * N + col + 1
if col + 1 < N and row + 1 < N:
f.write(f"{id1} {(row + 1) * N + (col + 1) + 1} k2\n")
cnt += 1
if col - 1 >= 0 and row + 1 < N:
f.write(f"{id1} {(row + 1) * N + (col - 1) + 1} k2\n")
cnt += 1
print(f" connection.txt: 追加 {cnt} 条 k2 键")
# 检查 bond.txt 是否有 k2
has_bond = False
if os.path.exists(bond_path):
with open(bond_path, "r") as f:
for line in f:
if line.startswith("k2"):
has_bond = True
break
if has_bond:
print(f" bond.txt: k2 已存在")
else:
with open(bond_path, "a", encoding="utf-8") as f:
f.write("k2 100.0 1.41421356\n")
print(f" bond.txt: 追加 k2 定义")
return True
if __name__ == "__main__":
targets = []
if "--all" in sys.argv:
base = os.path.dirname(os.path.abspath(__file__))
for d in sorted(os.listdir(base)):
if d.startswith("case") and os.path.isdir(os.path.join(base, d)):
targets.append(os.path.join(base, d))
else:
for arg in sys.argv[1:]:
if arg.startswith("--"):
continue
p = arg if os.path.isabs(arg) else os.path.join(os.path.dirname(os.path.abspath(__file__)), arg)
targets.append(p)
for t in targets:
add_k2(t)
+1 -1
View File
@@ -6,7 +6,7 @@
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/trajectory.txt
step_sample: 1 # 抽帧 → output/display.txt
step_sample: 0 # 抽帧 → output/display.txt
step_plot: 1 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_plot_wave: 0 # 绘制波形能量动画
plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1
+829
View File
@@ -0,0 +1,829 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case04 — 日地月三体系统</title>
<script>
MathJax = {
tex: { inlineMath: [['$','$'], ['\\(','\\)']], displayMath: [['$$','$$'], ['\\[','\\]']] },
svg: { fontCache: 'global' }
};
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js" async></script>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #c9d1d9;
--text-dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
--teal: #56d4dd;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.8;
padding: 40px 24px;
}
.container { max-width: 860px; margin: 0 auto; }
h1 { font-size: 1.8rem; margin-bottom: 6px; color: #f0f6fc; }
h1 small { font-size: 1rem; color: var(--text-dim); font-weight: 400; }
.subtitle { color: var(--text-dim); margin-bottom: 32px; }
h2 {
font-size: 1.35rem;
margin-top: 36px;
margin-bottom: 14px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border);
color: #f0f6fc;
}
h3 { font-size: 1.1rem; margin-top: 24px; margin-bottom: 10px; color: #f0f6fc; }
p { margin-bottom: 12px; color: var(--text); }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px 24px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 0.9rem;
}
th, td {
text-align: left;
padding: 8px 14px;
border-bottom: 1px solid var(--border);
}
th { color: var(--text-dim); font-weight: 600; background: #1c2128; }
.highlight {
background: #1f242e;
border-left: 3px solid var(--accent);
padding: 10px 16px;
margin: 12px 0;
border-radius: 0 6px 6px 0;
font-size: 0.9rem;
}
.highlight-eq {
background: #1f242e;
border-left: 3px solid var(--teal);
padding: 14px 18px;
margin: 14px 0;
border-radius: 0 6px 6px 0;
}
.highlight-eq p { margin: 6px 0; }
code {
background: #1c2128;
padding: 2px 6px;
border-radius: 4px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.85em;
color: var(--green);
}
.tag {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 0.78rem;
font-weight: 500;
margin: 2px;
}
.tag-python { background: #3572A533; color: #3572A5; }
.tag-gravity { background: #d2992233; color: var(--orange); }
ul, ol { margin: 8px 0 12px 24px; }
li { margin-bottom: 4px; }
.formula-block {
overflow-x: auto;
padding: 8px 0;
}
.diagram-wrap {
background: #0d1117;
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
text-align: center;
}
.diagram-wrap canvas {
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}
.diagram-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 16px;
margin-top: 12px;
}
.diagram-controls label {
font-size: 0.85rem;
color: var(--text-dim);
}
.diagram-controls input[type="range"] {
width: 200px;
accent-color: var(--accent);
}
.diagram-controls .val {
font-size: 0.9rem;
font-weight: 500;
color: var(--teal);
min-width: 48px;
display: inline-block;
text-align: center;
}
.diagram-info {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
margin-top: 10px;
font-size: 0.82rem;
color: var(--text-dim);
}
.diagram-info span {
display: inline-flex;
align-items: center;
gap: 4px;
}
.diagram-info .dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 2px;
}
@media (max-width: 640px) {
body { padding: 16px; }
.diagram-controls { flex-direction: column; gap: 8px; }
}
</style>
</head>
<body>
<div class="container">
<h1>case04 — 日地月三体系统 <small>稳定轨道版本</small></h1>
<p class="subtitle">太阳、地球、月球三体系统,采用真实比例的质量和万有引力模拟,地球和月球维持稳定椭圆轨道。</p>
<div class="card" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-bottom:20px;">
<span class="tag tag-python">Python 引擎</span>
<span class="tag tag-gravity">万有引力</span>
<span style="color:var(--text-dim); font-size:0.85rem; margin-left:8px;">3 粒子 | leapfrog 算法 | 稳定轨道</span>
</div>
<h2>一、物理系统概览</h2>
<div class="card">
<p>本案例模拟真实的日-地-月三体系统:</p>
<ul>
<li><strong>太阳</strong>:位于原点,质量为 $M_\odot$,固定不动(fix_x=fix_y=fix_z=1</li>
<li><strong>地球</strong>:绕太阳公转,质量为 $M_\oplus$,近日点出发</li>
<li><strong>月球</strong>:绕地球公转(同时跟随地球绕太阳),质量为 $M_\text{moon}$</li>
</ul>
<p>原子间万有引力由 <code>gravity_strength</code> 缩放控制,$F = G \dfrac{m_1 m_2}{r^2}$。</p>
</div>
<h2>二、天体真实参数</h2>
<h3>2.1 质量</h3>
<table>
<tr><th>天体</th><th>质量 / kg</th><th>模拟质量(月球=1</th></tr>
<tr><td>太阳</td><td>$1.989 \times 10^{30}$</td><td>$27\,000$</td></tr>
<tr><td>地球</td><td>$5.972 \times 10^{24}$</td><td>$81$</td></tr>
<tr><td>月球</td><td>$7.35 \times 10^{22}$</td><td>$1$</td></tr>
</table>
<div class="highlight">
<strong>质量比</strong>$M_\odot : M_\oplus : M_\text{moon} \approx 27\,100\,000 : 81.3 : 1$<br>
模拟中采用缩放比例 $27\,000 : 81 : 1$(太阳质量缩至 $1/1000$ 以保持数值稳定)。
</div>
<h3>2.2 轨道参数</h3>
<table>
<tr><th>轨道</th><th>半长轴 $a$</th><th>偏心率 $e$</th><th>半短轴 $b$</th><th>周期</th></tr>
<tr><td>地球绕太阳</td><td>$1.4960 \times 10^8$ km (1 AU)</td><td>$0.0167$</td><td>$1.4958 \times 10^8$ km</td><td>365.25 天</td></tr>
<tr><td>月球绕地球</td><td>$3.844 \times 10^5$ km</td><td>$0.0549$</td><td>$3.838 \times 10^5$ km</td><td>27.32 天</td></tr>
</table>
<div class="highlight">
<strong>比例</strong>$R_{\text{日地}} : R_{\text{地月}} \approx 389 : 1$<br>
地月距离约为日地距离的 $1/389$。
</div>
<h2>三、轨道力学</h2>
<h3>3.1 交互式轨道示意图</h3>
<p>拖动下方滑块改变偏心率 $e$,观察轨道形状的变化和参数标注:</p>
<div class="diagram-wrap">
<canvas id="orbitCanvas" width="760" height="400"></canvas>
<div class="diagram-controls">
<label>偏心率 e = <span class="val" id="eDisplay">0.0170</span>(地球真实值 0.0167</label>
<input type="range" id="eSlider" min="0" max="850" value="17">
</div>
<div class="diagram-info" id="diagramInfo"></div>
</div>
<h3>3.2 月球绕地球轨道</h3>
<p>拖动下方滑块改变月球轨道偏心率 $e_\text{moon}$(真实值 0.0549):</p>
<div class="diagram-wrap">
<canvas id="moonCanvas" width="760" height="400"></canvas>
<div class="diagram-controls">
<label>偏心率 e = <span class="val" id="moonEDisplay">0.0549</span>(月球真实值 0.0549</label>
<input type="range" id="moonESlider" min="0" max="850" value="55">
</div>
<div class="diagram-info" id="moonInfo"></div>
</div>
<h3>3.3 偏心率定义</h3>
<div class="highlight-eq">
<p>偏心率 $e$ 描述椭圆轨道偏离正圆的程度:</p>
<div class="formula-block">
$$e = \frac{c}{a}$$
</div>
<p>其中 $c = ea$ 为偏心距(焦点到椭圆中心的距离),$a$ 为半长轴。</p>
</div>
<table>
<tr><th>$e$ 值</th><th>轨道形状</th><th>说明</th></tr>
<tr><td>$e = 0$</td><td>正圆形</td><td>速度恒定,距离恒定</td></tr>
<tr><td>$0 &lt; e &lt; 1$</td><td>椭圆</td><td>近日点/近地点速度最大,远日点/远地点速度最小</td></tr>
<tr><td>$e = 1$</td><td>抛物线</td><td>逃逸轨道,速度恰好达到逃逸速度</td></tr>
<tr><td>$e &gt; 1$</td><td>双曲线</td><td>飞越轨道,速度超过逃逸速度</td></tr>
</table>
<h3>3.4 半长轴与半短轴的关系</h3>
<div class="highlight-eq">
<div class="formula-block">
$$b = a\sqrt{1 - e^2}$$
</div>
<p>当 $e \ll 1$ 时,$b \approx a\left(1 - \dfrac{e^2}{2}\right)$,椭圆度非常微小。</p>
</div>
<h3>3.5 近日点与远日点</h3>
<div class="highlight-eq">
<p>近日点(距太阳最近)和远日点(距太阳最远)的距离分别为:</p>
<div class="formula-block">
$$r_{\text{peri}} = a(1 - e), \qquad r_{\text{ap}} = a(1 + e)$$
</div>
<p>在近日点轨道速度最大,在远日点轨道速度最小:</p>
<div class="formula-block">
$$v_{\text{peri}} = \sqrt{\frac{GM(1+e)}{a(1-e)}}, \qquad
v_{\text{ap}} = \sqrt{\frac{GM(1-e)}{a(1+e)}}$$
</div>
</div>
<h3>3.6 开普勒三大定律</h3>
<ol>
<li><strong>椭圆定律</strong>:行星轨道是椭圆,太阳位于椭圆的一个焦点上。</li>
<li><strong>面积定律</strong>:行星与太阳的连线在相等时间内扫过相等的面积。</li>
<li><strong>周期定律</strong>:公转周期的平方与半长轴的立方成正比:$T^2 \propto a^3$。</li>
</ol>
<div class="highlight">
<strong>验证</strong>:在本模拟中,你可以通过轨迹图观察面积定律是否成立——地球在近日点移动更快,远日点移动更慢。
</div>
<h2>四、模拟参数</h2>
<h3>4.1 缩放说明</h3>
<p>真实尺度无法直接用于模拟(日地距离 1.5 亿 km),因此采用缩放参数。当前 case04 的配置为教学演示而简化:</p>
<table>
<tr><th>参数</th><th></th><th>说明</th></tr>
<tr><td><code>gravity_strength</code></td><td>100.0</td><td>万有引力强度</td></tr>
<tr><td><code>box_a</code></td><td>30.0</td><td>盒子半边长</td></tr>
<tr><td><code>method</code></td><td>leapfrog</td><td>蛙跳法(辛积分器,能量守恒)</td></tr>
<tr><td><code>T_total</code></td><td>10.0 s</td><td>总模拟时间</td></tr>
<tr><td><code>NSTEP</code></td><td>2</td><td>抽帧间隔(密采帧)</td></tr>
</table>
<h3>4.2 初始构型</h3>
<table>
<tr><th>天体</th><th>质量</th><th>位置 $(x,y,z)$</th><th>速度 $(v_x,v_y,v_z)$</th><th>固定约束</th></tr>
<tr><td>太阳</td><td>$27\,000$</td><td>$(0,0,0)$</td><td>$(0,0,0)$</td><td>全部固定</td></tr>
<tr><td>地球</td><td>$81$</td><td>$(10,0,0)$</td><td>$(0,0,520)$</td><td></td></tr>
<tr><td>月球</td><td>$1$</td><td>$(10.5,0,0)$</td><td>$(0,0,647)$</td><td></td></tr>
</table>
<p>地球在 $z$ 方向获得初速度 $v=520$,产生绕太阳的轨道运动(接近圆轨道);月球在地球基础上附加 $v\approx127$ 的绕地速度,形成绕地球的轨道。速度由圆形轨道公式 $v = \sqrt{G_{\text{eff}} M / r}$ 计算,其中 $G_{\text{eff}} = \text{gravity\_strength} = 100$。</p>
<div class="highlight">
<strong>验证</strong>:地球速度 $v_\oplus = \sqrt{100 \times 27\,000 / 10} \approx 519.6$,设 520 正确。月球相对速度 $v_{\text{rel}} = \sqrt{100 \times 81 / 0.5} \approx 127.3$,设 127 正确。地月距离 0.5 在地球希尔半径 $r_H \approx 10 \times (81 / 81\,000)^{1/3} \approx 1.0$ 之内,可确保轨道稳定。
</div>
<h2>五、使用方法</h2>
<div class="card">
<code style="display:block; padding:14px 18px; margin-bottom:10px;">
# 进入 case04 目录并运行<br>
cd examples/case04<br>
python run_dynamics.py<br><br>
# 仅输出轨迹图,跳过动画<br>
python run_dynamics.py --no-plot
</code>
<p>配置文件:<code>input/input.txt</code>(物理参数)、<code>input/coord.txt</code>(初始位置/速度)。</p>
</div>
<h2>六、与 case03 的对比</h2>
<table>
<tr><th></th><th>case03(失败案例)</th><th>case04(成功案例)</th></tr>
<tr><td>轨道状态</td><td>轨道发散或碰撞</td><td>稳定椭圆轨道</td></tr>
<tr><td>关键差异</td><td>初始速度或质量比不恰当</td><td>合理的初值和参数</td></tr>
<tr><td>教学意义</td><td>展示参数选择的重要性</td><td>展示正确的三体运动</td></tr>
</table>
<div class="highlight">
<strong>教学建议</strong>:先运行 case03 观察失稳,再运行 case04 对比稳定轨道,理解初始条件对数值模拟的关键影响。
</div>
<h2>七、已知局限</h2>
<ul>
<li>太阳真实质量为月球 $27\,100\,000$ 倍,模拟中缩至 $27\,000$ 倍以保持数值稳定($1/1000$)</li>
<li>轨道半径未按真实比例缩放(真实日地距是地月距的 389 倍,模拟中约为 20 倍,受希尔半径约束,月球已尽可能放远)</li>
<li>未考虑月球轨道倾角(真实地月轨道有约 5° 的倾角)</li>
<li>leapfrog 算法为能量守恒的辛积分器,长期稳定,但步长过大时仍可能偏离真实轨道</li>
<li>太阳固定不动可作为教学简化,但严格三体模拟中应让所有天体在质心系中自由运动</li>
</ul>
</div>
<script>
(function() {
var canvas = document.getElementById('orbitCanvas');
var ctx = canvas.getContext('2d');
var slider = document.getElementById('eSlider');
var eDisplay = document.getElementById('eDisplay');
var infoDiv = document.getElementById('diagramInfo');
var W = 760, H = 400;
var cx = 380, cy = 200;
// 实际绘图区域半径
var A = 140;
function draw(e) {
ctx.clearRect(0, 0, W, H);
var b = A * Math.sqrt(1 - e * e);
var f = e * A;
// 长轴辅助线
ctx.beginPath();
ctx.moveTo(cx - A - 20, cy);
ctx.lineTo(cx + A + 20, cy);
ctx.strokeStyle = '#30363d';
ctx.lineWidth = 0.5;
ctx.setLineDash([4, 3]);
ctx.stroke();
ctx.setLineDash([]);
// 椭圆轨道(中心在 cx+f,左焦点=cx 为太阳)
ctx.beginPath();
ctx.ellipse(cx + f, cy, A, b, 0, 0, Math.PI * 2);
ctx.strokeStyle = '#58a6ff';
ctx.lineWidth = 1.5;
ctx.stroke();
// 半长轴 a(标注线,从椭圆中心到右顶点)
var aY = cy + 28;
ctx.beginPath();
ctx.moveTo(cx + f, aY);
ctx.lineTo(cx + f + A, aY);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 1.5;
ctx.stroke();
// 箭头(左端:椭圆中心 cx+f)
ctx.beginPath();
ctx.moveTo(cx + f + 6, aY - 4);
ctx.lineTo(cx + f, aY);
ctx.lineTo(cx + f + 6, aY + 4);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 1.5;
ctx.stroke();
// 箭头(右端:右顶点 cx+f+A)
ctx.beginPath();
ctx.moveTo(cx + f + A - 6, aY - 4);
ctx.lineTo(cx + f + A, aY);
ctx.lineTo(cx + f + A - 6, aY + 4);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#c9d1d9';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('a = ' + A.toFixed(0), cx + f + A/2, aY + 6);
// 偏心距 c = ea(从左焦点到椭圆中心)
var cY = cy - b - 20;
ctx.beginPath();
ctx.moveTo(cx, cY);
ctx.lineTo(cx + f, cY);
ctx.strokeStyle = '#d29922';
ctx.lineWidth = 1.5;
ctx.stroke();
// 箭头(左端:左焦点=cx 太阳)
ctx.beginPath();
ctx.moveTo(cx + 5, cY - 4);
ctx.lineTo(cx, cY);
ctx.lineTo(cx + 5, cY + 4);
ctx.strokeStyle = '#d29922';
ctx.lineWidth = 1.5;
ctx.stroke();
// 箭头(右端:椭圆中心 cx+f)
ctx.beginPath();
ctx.moveTo(cx + f - 5, cY - 4);
ctx.lineTo(cx + f, cY);
ctx.lineTo(cx + f - 5, cY + 4);
ctx.strokeStyle = '#d29922';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#d29922';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('c = ea = ' + f.toFixed(1), cx + f/2, cY - 4);
// 半短轴 b 标注(从椭圆中心到上顶点)
var bX = cx + f + A + 18;
ctx.beginPath();
ctx.moveTo(bX, cy);
ctx.lineTo(bX, cy - b);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bX - 4, cy - 6);
ctx.lineTo(bX, cy);
ctx.lineTo(bX + 4, cy - 6);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bX - 4, cy - b + 6);
ctx.lineTo(bX, cy - b);
ctx.lineTo(bX + 4, cy - b + 6);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#3fb950';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('b = ' + b.toFixed(1), bX + 8, cy - b/2);
// 太阳(焦点)
ctx.beginPath();
ctx.arc(cx, cy, 7, 0, Math.PI * 2);
ctx.fillStyle = '#d29922';
ctx.fill();
ctx.strokeStyle = '#f0883e';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#f0f6fc';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('太阳(左焦点)', cx, cy - 12);
// 近日点(左顶点:距左焦点最近)
var periX = cx + f - A;
ctx.fillStyle = '#58a6ff';
ctx.beginPath();
ctx.arc(periX, cy, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#8b949e';
ctx.font = '12px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('近日点 r = a(1-e) = ' + (A * (1 - e)).toFixed(1), periX, cy + 12);
// 远日点标注(右顶点:距左焦点最远)
var apX = cx + f + A;
ctx.fillStyle = '#58a6ff';
ctx.beginPath();
ctx.arc(apX, cy, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#8b949e';
ctx.font = '12px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('远日点 r = a(1+e) = ' + (A * (1 + e)).toFixed(1), apX, cy + 12);
// 地球(轨道上的点)
var angle = 0.4;
var ex = cx + f + A * Math.cos(angle);
var ey = cy - b * Math.sin(angle);
ctx.beginPath();
ctx.arc(ex, ey, 5, 0, Math.PI * 2);
ctx.fillStyle = '#58a6ff';
ctx.fill();
ctx.strokeStyle = '#1f6feb';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#58a6ff';
ctx.font = '12px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillText('地球', ex + 8, ey);
// 速度矢量
var vlen = 32;
var vx = -A * Math.sin(angle) * 0.7;
var vy = b * Math.cos(angle) * 0.7;
var vl = Math.sqrt(vx*vx + vy*vy);
if (vl > 0) {
vx = vx / vl * vlen;
vy = vy / vl * vlen;
}
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex + vx, ey + vy);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
var tipX = ex + vx, tipY = ey + vy;
var a1 = 0.3;
ctx.moveTo(tipX, tipY);
ctx.lineTo(tipX - vx * a1 + vy * a1 * 0.5, tipY - vy * a1 - vx * a1 * 0.5);
ctx.lineTo(tipX - vx * a1 - vy * a1 * 0.5, tipY - vy * a1 + vx * a1 * 0.5);
ctx.closePath();
ctx.fillStyle = '#3fb950';
ctx.fill();
// 信息面板
var roundE = Math.round(e * 10000) / 10000;
infoDiv.innerHTML =
'<span><span class="dot" style="background:#58a6ff"></span>椭圆轨道 e = ' + roundE.toFixed(4) + '</span>' +
'<span><span class="dot" style="background:#d29922"></span>偏心距 c = ' + f.toFixed(1) + '</span>' +
'<span><span class="dot" style="background:#3fb950"></span>b/a = ' + (b / A).toFixed(4) + '</span>' +
'<span>近日点 ' + (A * (1 - e)).toFixed(1) + ' / 远日点 ' + (A * (1 + e)).toFixed(1) + '</span>';
}
function update() {
var val = parseInt(slider.value);
var e = val / 1000;
eDisplay.textContent = e.toFixed(4);
draw(e);
}
slider.addEventListener('input', update);
update();
})();
// ── 月球绕地球轨道图 ──
(function() {
var canvas = document.getElementById('moonCanvas');
var ctx = canvas.getContext('2d');
var slider = document.getElementById('moonESlider');
var eDisplay = document.getElementById('moonEDisplay');
var infoDiv = document.getElementById('moonInfo');
var W = 760, H = 400;
var cx = 380, cy = 200;
var A = 140;
function draw(e) {
ctx.clearRect(0, 0, W, H);
var b = A * Math.sqrt(1 - e * e);
var f = e * A;
// 长轴辅助线
ctx.beginPath();
ctx.moveTo(cx - A - 20, cy);
ctx.lineTo(cx + A + 20, cy);
ctx.strokeStyle = '#30363d';
ctx.lineWidth = 0.5;
ctx.setLineDash([4, 3]);
ctx.stroke();
ctx.setLineDash([]);
// 椭圆轨道(中心在 cx+f,左焦点=cx 为地球)
ctx.beginPath();
ctx.ellipse(cx + f, cy, A, b, 0, 0, Math.PI * 2);
ctx.strokeStyle = '#7ee787';
ctx.lineWidth = 1.5;
ctx.stroke();
// 半长轴 a(标注线,从椭圆中心到右顶点)
var aY = cy + 28;
ctx.beginPath();
ctx.moveTo(cx + f, aY);
ctx.lineTo(cx + f + A, aY);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + f + 6, aY - 4);
ctx.lineTo(cx + f, aY);
ctx.lineTo(cx + f + 6, aY + 4);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + f + A - 6, aY - 4);
ctx.lineTo(cx + f + A, aY);
ctx.lineTo(cx + f + A - 6, aY + 4);
ctx.strokeStyle = '#c9d1d9';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#c9d1d9';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('a = ' + A.toFixed(0), cx + f + A/2, aY + 6);
// 偏心距 c = ea(从左焦点=地球 到椭圆中心)
var cY = cy - b - 20;
ctx.beginPath();
ctx.moveTo(cx, cY);
ctx.lineTo(cx + f, cY);
ctx.strokeStyle = '#d29922';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + 5, cY - 4);
ctx.lineTo(cx, cY);
ctx.lineTo(cx + 5, cY + 4);
ctx.strokeStyle = '#d29922';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + f - 5, cY - 4);
ctx.lineTo(cx + f, cY);
ctx.lineTo(cx + f - 5, cY + 4);
ctx.strokeStyle = '#d29922';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#d29922';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('c = ea = ' + f.toFixed(1), cx + f/2, cY - 4);
// 半短轴 b 标注(从椭圆中心到上顶点)
var bX = cx + f + A + 18;
ctx.beginPath();
ctx.moveTo(bX, cy);
ctx.lineTo(bX, cy - b);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bX - 4, cy - 6);
ctx.lineTo(bX, cy);
ctx.lineTo(bX + 4, cy - 6);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bX - 4, cy - b + 6);
ctx.lineTo(bX, cy - b);
ctx.lineTo(bX + 4, cy - b + 6);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#3fb950';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText('b = ' + b.toFixed(1), bX + 8, cy - b/2);
// 地球(左焦点)
ctx.beginPath();
ctx.arc(cx, cy, 8, 0, Math.PI * 2);
ctx.fillStyle = '#58a6ff';
ctx.fill();
ctx.strokeStyle = '#1f6feb';
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = '#f0f6fc';
ctx.font = '13px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('地球(左焦点)', cx, cy - 14);
// 近地点(左顶点:距地球最近)
var periX = cx + f - A;
ctx.fillStyle = '#7ee787';
ctx.beginPath();
ctx.arc(periX, cy, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#8b949e';
ctx.font = '12px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('近地点 r = a(1-e) = ' + (A * (1 - e)).toFixed(1), periX, cy + 12);
// 远地点(右顶点:距地球最远)
var apX = cx + f + A;
ctx.fillStyle = '#7ee787';
ctx.beginPath();
ctx.arc(apX, cy, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#8b949e';
ctx.font = '12px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText('远地点 r = a(1+e) = ' + (A * (1 + e)).toFixed(1), apX, cy + 12);
// 月球(轨道上的点)
var angle = 0.6;
var mx = cx + f + A * Math.cos(angle);
var my = cy - b * Math.sin(angle);
ctx.beginPath();
ctx.arc(mx, my, 4, 0, Math.PI * 2);
ctx.fillStyle = '#e2e8f0';
ctx.fill();
ctx.strokeStyle = '#8b949e';
ctx.lineWidth = 0.5;
ctx.stroke();
ctx.fillStyle = '#e2e8f0';
ctx.font = '12px "Segoe UI", Arial, sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillText('月球', mx + 8, my);
// 速度矢量
var vlen = 32;
var vx = -A * Math.sin(angle) * 0.7;
var vy = b * Math.cos(angle) * 0.7;
var vl = Math.sqrt(vx*vx + vy*vy);
if (vl > 0) { vx = vx/vl*vlen; vy = vy/vl*vlen; }
ctx.beginPath();
ctx.moveTo(mx, my);
ctx.lineTo(mx + vx, my + vy);
ctx.strokeStyle = '#3fb950';
ctx.lineWidth = 2;
ctx.stroke();
var tipX = mx + vx, tipY = my + vy;
ctx.beginPath();
ctx.moveTo(tipX, tipY);
ctx.lineTo(tipX - vx*0.3 + vy*0.15, tipY - vy*0.3 - vx*0.15);
ctx.lineTo(tipX - vx*0.3 - vy*0.15, tipY - vy*0.3 + vx*0.15);
ctx.closePath();
ctx.fillStyle = '#3fb950';
ctx.fill();
// 信息面板
var roundE = Math.round(e * 10000) / 10000;
infoDiv.innerHTML =
'<span><span class="dot" style="background:#7ee787"></span>椭圆轨道 e = ' + roundE.toFixed(4) + '</span>' +
'<span><span class="dot" style="background:#d29922"></span>偏心距 c = ' + f.toFixed(1) + '</span>' +
'<span><span class="dot" style="background:#3fb950"></span>b/a = ' + (b / A).toFixed(4) + '</span>' +
'<span>近地点 ' + (A * (1 - e)).toFixed(1) + ' / 远地点 ' + (A * (1 + e)).toFixed(1) + '</span>';
}
function update() {
var val = parseInt(slider.value);
var e = val / 1000;
eDisplay.textContent = e.toFixed(4);
draw(e);
}
slider.addEventListener('input', update);
update();
})();
</script>
</body>
</html>
+4 -4
View File
@@ -1,4 +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
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 27000 1.0 0 0 0 0 0 0 1 1 1
2 81 0.2 10 0 0 0 0 520 0 0 0
3 1 0.1 10.5 0 0 0 0 647 0 0 0
+2 -2
View File
@@ -23,7 +23,7 @@ force_calc: 0 # 强制重新计算:1=跳过缓存强算,0=自动使用
engine: python # 默认使用 Python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 20.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
box_a: 30.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
# ── 初始构型 ──────────────────────────────────
# 坐标文件格式:
@@ -74,7 +74,7 @@ T_total: 10.0
NSTEP: 2
# ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s)
DT: 0.0001 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起)
+1 -1
View File
@@ -1,5 +1,5 @@
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 0 1 1
1 1 0.1 0 0 0 0 0 0 0 1 1
2 1 0.1 1 0 0 0 0 0 0 1 1
3 1 0.1 2 0 0 0 0 0 0 1 1
4 1 0.1 3 0 0 0 0 0 0 1 1
+2 -2
View File
@@ -1,2 +1,2 @@
bond_name k rest_length
k1 50.0 1.0
bond_name k rest_length
k1 10.0 1.0
+120 -120
View File
@@ -1,121 +1,121 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 0 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
61 1 0.1 60 0 0 0 0 0 1 1 0
62 1 0.1 61 0 0 0 0 0 1 1 0
63 1 0.1 62 0 0 0 0 0 1 1 0
64 1 0.1 63 0 0 0 0 0 1 1 0
65 1 0.1 64 0 0 0 0 0 1 1 0
66 1 0.1 65 0 0 0 0 0 1 1 0
67 1 0.1 66 0 0 0 0 0 1 1 0
68 1 0.1 67 0 0 0 0 0 1 1 0
69 1 0.1 68 0 0 0 0 0 1 1 0
70 1 0.1 69 0 0 0 0 0 1 1 0
71 1 0.1 70 0 0 0 0 0 1 1 0
72 1 0.1 71 0 0 0 0 0 1 1 0
73 1 0.1 72 0 0 0 0 0 1 1 0
74 1 0.1 73 0 0 0 0 0 1 1 0
75 1 0.1 74 0 0 0 0 0 1 1 0
76 1 0.1 75 0 0 0 0 0 1 1 0
77 1 0.1 76 0 0 0 0 0 1 1 0
78 1 0.1 77 0 0 0 0 0 1 1 0
79 1 0.1 78 0 0 0 0 0 1 1 0
80 1 0.1 79 0 0 0 0 0 1 1 0
81 1 0.1 80 0 0 0 0 0 1 1 0
82 1 0.1 81 0 0 0 0 0 1 1 0
83 1 0.1 82 0 0 0 0 0 1 1 0
84 1 0.1 83 0 0 0 0 0 1 1 0
85 1 0.1 84 0 0 0 0 0 1 1 0
86 1 0.1 85 0 0 0 0 0 1 1 0
87 1 0.1 86 0 0 0 0 0 1 1 0
88 1 0.1 87 0 0 0 0 0 1 1 0
89 1 0.1 88 0 0 0 0 0 1 1 0
90 1 0.1 89 0 0 0 0 0 1 1 0
91 1 0.1 90 0 0 0 0 0 1 1 0
92 1 0.1 91 0 0 0 0 0 1 1 0
93 1 0.1 92 0 0 0 0 0 1 1 0
94 1 0.1 93 0 0 0 0 0 1 1 0
95 1 0.1 94 0 0 0 0 0 1 1 0
96 1 0.1 95 0 0 0 0 0 1 1 0
97 1 0.1 96 0 0 0 0 0 1 1 0
98 1 0.1 97 0 0 0 0 0 1 1 0
99 1 0.1 98 0 0 0 0 0 1 1 0
100 1 0.1 99 0 0 0 0 0 1 1 0
101 1 0.1 100 0 0 0 0 0 1 1 0
102 1 0.1 101 0 0 0 0 0 1 1 0
103 1 0.1 102 0 0 0 0 0 1 1 0
104 1 0.1 103 0 0 0 0 0 1 1 0
105 1 0.1 104 0 0 0 0 0 1 1 0
106 1 0.1 105 0 0 0 0 0 1 1 0
107 1 0.1 106 0 0 0 0 0 1 1 0
108 1 0.1 107 0 0 0 0 0 1 1 0
109 1 0.1 108 0 0 0 0 0 1 1 0
110 1 0.1 109 0 0 0 0 0 1 1 0
111 1 0.1 110 0 0 0 0 0 1 1 0
112 1 0.1 111 0 0 0 0 0 1 1 0
113 1 0.1 112 0 0 0 0 0 1 1 0
114 1 0.1 113 0 0 0 0 0 1 1 0
115 1 0.1 114 0 0 0 0 0 1 1 0
116 1 0.1 115 0 0 0 0 0 1 1 0
117 1 0.1 116 0 0 0 0 0 1 1 0
118 1 0.1 117 0 0 0 0 0 1 1 0
119 1 0.1 118 0 0 0 0 0 1 1 0
120 1 0.1 119 0 0 0 0 0 1 1 1
1 1 0.1 0 0 0 0 0 0 0 1 0
2 1 0.1 1 0 0 0 0 0 0 1 0
3 1 0.1 2 0 0 0 0 0 0 1 0
4 1 0.1 3 0 0 0 0 0 0 1 0
5 1 0.1 4 0 0 0 0 0 0 1 0
6 1 0.1 5 0 0 0 0 0 0 1 0
7 1 0.1 6 0 0 0 0 0 0 1 0
8 1 0.1 7 0 0 0 0 0 0 1 0
9 1 0.1 8 0 0 0 0 0 0 1 0
10 1 0.1 9 0 0 0 0 0 0 1 0
11 1 0.1 10 0 0 0 0 0 0 1 0
12 1 0.1 11 0 0 0 0 0 0 1 0
13 1 0.1 12 0 0 0 0 0 0 1 0
14 1 0.1 13 0 0 0 0 0 0 1 0
15 1 0.1 14 0 0 0 0 0 0 1 0
16 1 0.1 15 0 0 0 0 0 0 1 0
17 1 0.1 16 0 0 0 0 0 0 1 0
18 1 0.1 17 0 0 0 0 0 0 1 0
19 1 0.1 18 0 0 0 0 0 0 1 0
20 1 0.1 19 0 0 0 0 0 0 1 0
21 1 0.1 20 0 0 0 0 0 0 1 0
22 1 0.1 21 0 0 0 0 0 0 1 0
23 1 0.1 22 0 0 0 0 0 0 1 0
24 1 0.1 23 0 0 0 0 0 0 1 0
25 1 0.1 24 0 0 0 0 0 0 1 0
26 1 0.1 25 0 0 0 0 0 0 1 0
27 1 0.1 26 0 0 0 0 0 0 1 0
28 1 0.1 27 0 0 0 0 0 0 1 0
29 1 0.1 28 0 0 0 0 0 0 1 0
30 1 0.1 29 0 0 0 0 0 0 1 0
31 1 0.1 30 0 0 0 0 0 0 1 0
32 1 0.1 31 0 0 0 0 0 0 1 0
33 1 0.1 32 0 0 0 0 0 0 1 0
34 1 0.1 33 0 0 0 0 0 0 1 0
35 1 0.1 34 0 0 0 0 0 0 1 0
36 1 0.1 35 0 0 0 0 0 0 1 0
37 1 0.1 36 0 0 0 0 0 0 1 0
38 1 0.1 37 0 0 0 0 0 0 1 0
39 1 0.1 38 0 0 0 0 0 0 1 0
40 1 0.1 39 0 0 0 0 0 0 1 0
41 1 0.1 40 0 0 0 0 0 0 1 0
42 1 0.1 41 0 0 0 0 0 0 1 0
43 1 0.1 42 0 0 0 0 0 0 1 0
44 1 0.1 43 0 0 0 0 0 0 1 0
45 1 0.1 44 0 0 0 0 0 0 1 0
46 1 0.1 45 0 0 0 0 0 0 1 0
47 1 0.1 46 0 0 0 0 0 0 1 0
48 1 0.1 47 0 0 0 0 0 0 1 0
49 1 0.1 48 0 0 0 0 0 0 1 0
50 1 0.1 49 0 0 0 0 0 0 1 0
51 1 0.1 50 0 0 0 0 0 0 1 0
52 1 0.1 51 0 0 0 0 0 0 1 0
53 1 0.1 52 0 0 0 0 0 0 1 0
54 1 0.1 53 0 0 0 0 0 0 1 0
55 1 0.1 54 0 0 0 0 0 0 1 0
56 1 0.1 55 0 0 0 0 0 0 1 0
57 1 0.1 56 0 0 0 0 0 0 1 0
58 1 0.1 57 0 0 0 0 0 0 1 0
59 1 0.1 58 0 0 0 0 0 0 1 0
60 1 0.1 59 0 0 0 0 0 0 1 0
61 1 0.1 60 0 0 0 0 0 0 1 0
62 1 0.1 61 0 0 0 0 0 0 1 0
63 1 0.1 62 0 0 0 0 0 0 1 0
64 1 0.1 63 0 0 0 0 0 0 1 0
65 1 0.1 64 0 0 0 0 0 0 1 0
66 1 0.1 65 0 0 0 0 0 0 1 0
67 1 0.1 66 0 0 0 0 0 0 1 0
68 1 0.1 67 0 0 0 0 0 0 1 0
69 1 0.1 68 0 0 0 0 0 0 1 0
70 1 0.1 69 0 0 0 0 0 0 1 0
71 1 0.1 70 0 0 0 0 0 0 1 0
72 1 0.1 71 0 0 0 0 0 0 1 0
73 1 0.1 72 0 0 0 0 0 0 1 0
74 1 0.1 73 0 0 0 0 0 0 1 0
75 1 0.1 74 0 0 0 0 0 0 1 0
76 1 0.1 75 0 0 0 0 0 0 1 0
77 1 0.1 76 0 0 0 0 0 0 1 0
78 1 0.1 77 0 0 0 0 0 0 1 0
79 1 0.1 78 0 0 0 0 0 0 1 0
80 1 0.1 79 0 0 0 0 0 0 1 0
81 1 0.1 80 0 0 0 0 0 0 1 0
82 1 0.1 81 0 0 0 0 0 0 1 0
83 1 0.1 82 0 0 0 0 0 0 1 0
84 1 0.1 83 0 0 0 0 0 0 1 0
85 1 0.1 84 0 0 0 0 0 0 1 0
86 1 0.1 85 0 0 0 0 0 0 1 0
87 1 0.1 86 0 0 0 0 0 0 1 0
88 1 0.1 87 0 0 0 0 0 0 1 0
89 1 0.1 88 0 0 0 0 0 0 1 0
90 1 0.1 89 0 0 0 0 0 0 1 0
91 1 0.1 90 0 0 0 0 0 0 1 0
92 1 0.1 91 0 0 0 0 0 0 1 0
93 1 0.1 92 0 0 0 0 0 0 1 0
94 1 0.1 93 0 0 0 0 0 0 1 0
95 1 0.1 94 0 0 0 0 0 0 1 0
96 1 0.1 95 0 0 0 0 0 0 1 0
97 1 0.1 96 0 0 0 0 0 0 1 0
98 1 0.1 97 0 0 0 0 0 0 1 0
99 1 0.1 98 0 0 0 0 0 0 1 0
100 1 0.1 99 0 0 0 0 0 0 1 0
101 1 0.1 100 0 0 0 0 0 0 1 0
102 1 0.1 101 0 0 0 0 0 0 1 0
103 1 0.1 102 0 0 0 0 0 0 1 0
104 1 0.1 103 0 0 0 0 0 0 1 0
105 1 0.1 104 0 0 0 0 0 0 1 0
106 1 0.1 105 0 0 0 0 0 0 1 0
107 1 0.1 106 0 0 0 0 0 0 1 0
108 1 0.1 107 0 0 0 0 0 0 1 0
109 1 0.1 108 0 0 0 0 0 0 1 0
110 1 0.1 109 0 0 0 0 0 0 1 0
111 1 0.1 110 0 0 0 0 0 0 1 0
112 1 0.1 111 0 0 0 0 0 0 1 0
113 1 0.1 112 0 0 0 0 0 0 1 0
114 1 0.1 113 0 0 0 0 0 0 1 0
115 1 0.1 114 0 0 0 0 0 0 1 0
116 1 0.1 115 0 0 0 0 0 0 1 0
117 1 0.1 116 0 0 0 0 0 0 1 0
118 1 0.1 117 0 0 0 0 0 0 1 0
119 1 0.1 118 0 0 0 0 0 0 1 0
120 1 0.1 119 0 0 0 0 0 1 1 1
+13 -10
View File
@@ -19,10 +19,10 @@ save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
engine: python # 默认使用 python 引擎
engine: c # 默认使用 python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 80.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
box_a: 300.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
# ── 初始构型 ──────────────────────────────────
# 坐标文件格式:
@@ -39,7 +39,7 @@ plot_atom: 1
# ── 物理参数 ──────────────────────────────────
# 三个方向分量分别对应 x, y, z
G: [0.00, 0.00, 0.00] # 重力场分量 (m/s²)
B: [0.02, 0.00, 0.02] # 阻尼分量
B: [0.01, 0.00, 0.01] # 阻尼分量
# ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G)
@@ -66,13 +66,13 @@ warmup_steps: 0 # 默认 0(立即开始记录)
# 总模拟时间(秒),程序自动计算 NT = T_total / DT
# 如果同时指定了 NT,以 NT 为准
T_total: 100.0
T_total: 1000.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 10
NSTEP: 500
# ── 时间步长 ──────────────────────────────────
DT: 0.01 # 时间步长 (s)
DT: 0.001 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起)
@@ -102,7 +102,10 @@ box_color_g: 0.80
box_color_b: 0.85
# ── 摄像机初始位置 ────────────────────────────
camera_distance: 40.0 # 摄像机到场景中心的距离
camera_elevation: 0 # 俯仰角(度),负值=俯视
camera_azimuth: 0 # 方位角(度)
move_camera: 1 # 0=固定视角, 1=按 move_camera.txt 运动
camera_distance: 120.0 # 摄像机到场景中心的距离
camera_elevation: 0.0 # 俯仰角(度),负值=俯视
camera_azimuth: 0.0 # 方位角(度)
camera_center_x: 60.0 # 摄像机注视点 x
camera_center_y: 0.0 # 摄像机注视点 y
camera_center_z: 0.0 # 摄像机注视点 z
move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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 500.0 1.0
+120
View File
@@ -0,0 +1,120 @@
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
60 61 k1
61 62 k1
62 63 k1
63 64 k1
64 65 k1
65 66 k1
66 67 k1
67 68 k1
68 69 k1
69 70 k1
70 71 k1
71 72 k1
72 73 k1
73 74 k1
74 75 k1
75 76 k1
76 77 k1
77 78 k1
78 79 k1
79 80 k1
80 81 k1
81 82 k1
82 83 k1
83 84 k1
84 85 k1
85 86 k1
86 87 k1
87 88 k1
88 89 k1
89 90 k1
90 91 k1
91 92 k1
92 93 k1
93 94 k1
94 95 k1
95 96 k1
96 97 k1
97 98 k1
98 99 k1
99 100 k1
100 101 k1
101 102 k1
102 103 k1
103 104 k1
104 105 k1
105 106 k1
106 107 k1
107 108 k1
108 109 k1
109 110 k1
110 111 k1
111 112 k1
112 113 k1
113 114 k1
114 115 k1
115 116 k1
116 117 k1
117 118 k1
118 119 k1
119 120 k1
+121
View File
@@ -0,0 +1,121 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 0 0 0 0 0 1 0
2 1 0.1 1 0 0 0 0 0 0 1 0
3 1 0.1 2 0 0 0 0 0 0 1 0
4 1 0.1 3 0 0 0 0 0 0 1 0
5 1 0.1 4 0 0 0 0 0 0 1 0
6 1 0.1 5 0 0 0 0 0 0 1 0
7 1 0.1 6 0 0 0 0 0 0 1 0
8 1 0.1 7 0 0 0 0 0 0 1 0
9 1 0.1 8 0 0 0 0 0 0 1 0
10 1 0.1 9 0 0 0 0 0 0 1 0
11 1 0.1 10 0 0 0 0 0 0 1 0
12 1 0.1 11 0 0 0 0 0 0 1 0
13 1 0.1 12 0 0 0 0 0 0 1 0
14 1 0.1 13 0 0 0 0 0 0 1 0
15 1 0.1 14 0 0 0 0 0 0 1 0
16 1 0.1 15 0 0 0 0 0 0 1 0
17 1 0.1 16 0 0 0 0 0 0 1 0
18 1 0.1 17 0 0 0 0 0 0 1 0
19 1 0.1 18 0 0 0 0 0 0 1 0
20 1 0.1 19 0 0 0 0 0 0 1 0
21 1 0.1 20 0 0 0 0 0 0 1 0
22 1 0.1 21 0 0 0 0 0 0 1 0
23 1 0.1 22 0 0 0 0 0 0 1 0
24 1 0.1 23 0 0 0 0 0 0 1 0
25 1 0.1 24 0 0 0 0 0 0 1 0
26 1 0.1 25 0 0 0 0 0 0 1 0
27 1 0.1 26 0 0 0 0 0 0 1 0
28 1 0.1 27 0 0 0 0 0 0 1 0
29 1 0.1 28 0 0 0 0 0 0 1 0
30 1 0.1 29 0 0 0 0 0 0 1 0
31 1 0.1 30 0 0 0 0 0 0 1 0
32 1 0.1 31 0 0 0 0 0 0 1 0
33 1 0.1 32 0 0 0 0 0 0 1 0
34 1 0.1 33 0 0 0 0 0 0 1 0
35 1 0.1 34 0 0 0 0 0 0 1 0
36 1 0.1 35 0 0 0 0 0 0 1 0
37 1 0.1 36 0 0 0 0 0 0 1 0
38 1 0.1 37 0 0 0 0 0 0 1 0
39 1 0.1 38 0 0 0 0 0 0 1 0
40 1 0.1 39 0 0 0 0 0 0 1 0
41 1 0.1 40 0 0 0 0 0 0 1 0
42 1 0.1 41 0 0 0 0 0 0 1 0
43 1 0.1 42 0 0 0 0 0 0 1 0
44 1 0.1 43 0 0 0 0 0 0 1 0
45 1 0.1 44 0 0 0 0 0 0 1 0
46 1 0.1 45 0 0 0 0 0 0 1 0
47 1 0.1 46 0 0 0 0 0 0 1 0
48 1 0.1 47 0 0 0 0 0 0 1 0
49 1 0.1 48 0 0 0 0 0 0 1 0
50 1 0.1 49 0 0 0 0 0 0 1 0
51 1 0.1 50 0 0 0 0 0 0 1 0
52 1 0.1 51 0 0 0 0 0 0 1 0
53 1 0.1 52 0 0 0 0 0 0 1 0
54 1 0.1 53 0 0 0 0 0 0 1 0
55 1 0.1 54 0 0 0 0 0 0 1 0
56 1 0.1 55 0 0 0 0 0 0 1 0
57 1 0.1 56 0 0 0 0 0 0 1 0
58 1 0.1 57 0 0 0 0 0 0 1 0
59 1 0.1 58 0 0 0 0 0 0 1 0
60 1 0.1 59 0 0 0 0 0 0 1 0
61 1 0.1 60 0 0 0 0 0 0 1 0
62 1 0.1 61 0 0 0 0 0 0 1 0
63 1 0.1 62 0 0 0 0 0 0 1 0
64 1 0.1 63 0 0 0 0 0 0 1 0
65 1 0.1 64 0 0 0 0 0 0 1 0
66 1 0.1 65 0 0 0 0 0 0 1 0
67 1 0.1 66 0 0 0 0 0 0 1 0
68 1 0.1 67 0 0 0 0 0 0 1 0
69 1 0.1 68 0 0 0 0 0 0 1 0
70 1 0.1 69 0 0 0 0 0 0 1 0
71 1 0.1 70 0 0 0 0 0 0 1 0
72 1 0.1 71 0 0 0 0 0 0 1 0
73 1 0.1 72 0 0 0 0 0 0 1 0
74 1 0.1 73 0 0 0 0 0 0 1 0
75 1 0.1 74 0 0 0 0 0 0 1 0
76 1 0.1 75 0 0 0 0 0 0 1 0
77 1 0.1 76 0 0 0 0 0 0 1 0
78 1 0.1 77 0 0 0 0 0 0 1 0
79 1 0.1 78 0 0 0 0 0 0 1 0
80 1 0.1 79 0 0 0 0 0 0 1 0
81 1 0.1 80 0 0 0 0 0 0 1 0
82 1 0.1 81 0 0 0 0 0 0 1 0
83 1 0.1 82 0 0 0 0 0 0 1 0
84 1 0.1 83 0 0 0 0 0 0 1 0
85 1 0.1 84 0 0 0 0 0 0 1 0
86 1 0.1 85 0 0 0 0 0 0 1 0
87 1 0.1 86 0 0 0 0 0 0 1 0
88 1 0.1 87 0 0 0 0 0 0 1 0
89 1 0.1 88 0 0 0 0 0 0 1 0
90 1 0.1 89 0 0 0 0 0 0 1 0
91 1 0.1 90 0 0 0 0 0 0 1 0
92 1 0.1 91 0 0 0 0 0 0 1 0
93 1 0.1 92 0 0 0 0 0 0 1 0
94 1 0.1 93 0 0 0 0 0 0 1 0
95 1 0.1 94 0 0 0 0 0 0 1 0
96 1 0.1 95 0 0 0 0 0 0 1 0
97 1 0.1 96 0 0 0 0 0 0 1 0
98 1 0.1 97 0 0 0 0 0 0 1 0
99 1 0.1 98 0 0 0 0 0 0 1 0
100 1 0.1 99 0 0 0 0 0 0 1 0
101 1 0.1 100 0 0 0 0 0 0 1 0
102 1 0.1 101 0 0 0 0 0 0 1 0
103 1 0.1 102 0 0 0 0 0 0 1 0
104 1 0.1 103 0 0 0 0 0 0 1 0
105 1 0.1 104 0 0 0 0 0 0 1 0
106 1 0.1 105 0 0 0 0 0 0 1 0
107 1 0.1 106 0 0 0 0 0 0 1 0
108 1 0.1 107 0 0 0 0 0 0 1 0
109 1 0.1 108 0 0 0 0 0 0 1 0
110 1 0.1 109 0 0 0 0 0 0 1 0
111 1 0.1 110 0 0 0 0 0 0 1 0
112 1 0.1 111 0 0 0 0 0 0 1 0
113 1 0.1 112 0 0 0 0 0 0 1 0
114 1 0.1 113 0 0 0 0 0 0 1 0
115 1 0.1 114 0 0 0 0 0 0 1 0
116 1 0.1 115 0 0 0 0 0 0 1 0
117 1 0.1 116 0 0 0 0 0 0 1 0
118 1 0.1 117 0 0 0 0 0 0 1 0
119 1 0.1 118 0 0 0 0 0 0 1 0
120 1 0.1 119 0 0 0 0 0 1 1 1
+3
View File
@@ -0,0 +1,3 @@
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
1 0 0 0.1 0 0 0.01333 0 0 90 all
120 0 0 0.1 0 0 0.01333 0 0 90 all
+114
View File
@@ -0,0 +1,114 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧)
step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 0 # 自动播放 VisPy 3D 动画窗口(需安装 vispy)
step_plot_wave: 1 # 绘制波形能量动画
force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1
plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧)
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
engine: c # 默认使用 python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 300.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.000, 0.000, 0.000] # 重力场分量 (m/s²)
B: [0.005, 0.000, 0.005] # 阻尼分量
# ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G)
gravity_interaction: 0 # 原子间万有引力
elastic_force: 1 # 弹簧键力
damping_force: 1 # 阻尼 (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: 1000.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 500
# ── 时间步长 ──────────────────────────────────
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.0]
# 小球颜色
# 小球半径从 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
# ── 摄像机初始位置 ────────────────────────────
camera_distance: 120.0 # 摄像机到场景中心的距离
camera_elevation: 0.0 # 俯仰角(度),负值=俯视
camera_azimuth: 0.0 # 方位角(度)
camera_center_x: 60.0 # 摄像机注视点 x
camera_center_y: 0.0 # 摄像机注视点 y
camera_center_z: 0.0 # 摄像机注视点 z
move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动
# ── 视觉放大 ──────────────────────────────────
display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理)
+9
View File
@@ -0,0 +1,9 @@
# move_camera.txt — 摄像机速度段驱动
# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d
# vx/vy/vz: 平移速度(每帧移动单位)
# rx/ry/rz: 旋转速度(每帧度数)
# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留)
#
# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转
all vx=0.02
# 30-90 vy=0.02 ry=1 rz=1
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case06 1D atomic chain (transverse wave).
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 示例案例 case06")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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 0.1 1.0
+2
View File
@@ -0,0 +1,2 @@
n1 n2 bond_name
1 2 k1
+3
View File
@@ -0,0 +1,3 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 0 0 0 0 0 1 0
2 1 0.1 1 0 0 0 0 0 0 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.1 0 0.0 0.02 0 0 90 0 0 all
+114
View File
@@ -0,0 +1,114 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧)
step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 0 # 自动播放 VisPy 3D 动画窗口(需安装 vispy)
step_plot_wave: 1 # 绘制波形能量动画
force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1
plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧)
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
engine: c # 默认使用 python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 300.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.000, 0.000, 0.000] # 重力场分量 (m/s²)
B: [0.005, 0.000, 0.005] # 阻尼分量
# ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G)
gravity_interaction: 0 # 原子间万有引力
elastic_force: 1 # 弹簧键力
damping_force: 1 # 阻尼 (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: 500
# ── 时间步长 ──────────────────────────────────
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.0]
# 小球颜色
# 小球半径从 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
# ── 摄像机初始位置 ────────────────────────────
camera_distance: 10.0 # 摄像机到场景中心的距离
camera_elevation: 0.0 # 俯仰角(度),负值=俯视
camera_azimuth: 0.0 # 方位角(度)
camera_center_x: 0.0 # 摄像机注视点 x
camera_center_y: 0.0 # 摄像机注视点 y
camera_center_z: 0.0 # 摄像机注视点 z
move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动
# ── 视觉放大 ──────────────────────────────────
display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理)
+9
View File
@@ -0,0 +1,9 @@
# move_camera.txt — 摄像机速度段驱动
# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d
# vx/vy/vz: 平移速度(每帧移动单位)
# rx/ry/rz: 旋转速度(每帧度数)
# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留)
#
# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转
all vx=0.02
# 30-90 vy=0.02 ry=1 rz=1
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case06 1D atomic chain (transverse wave).
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 示例案例 case06")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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 300.0 1.0
+40
View File
@@ -0,0 +1,40 @@
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
+41
View File
@@ -0,0 +1,41 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 0 0 0 0 0 1 0
2 1 0.1 1 0 0 0 0 0 0 1 0
3 1 0.1 2 0 0 0 0 0 0 1 0
4 1 0.1 3 0 0 0 0 0 0 1 0
5 1 0.1 4 0 0 0 0 0 0 1 0
6 1 0.1 5 0 0 0 0 0 0 1 0
7 1 0.1 6 0 0 0 0 0 0 1 0
8 1 0.1 7 0 0 0 0 0 0 1 0
9 1 0.1 8 0 0 0 0 0 0 1 0
10 1 0.1 9 0 0 0 0 0 0 1 0
11 1 0.1 10 0 0 0 0 0 0 1 0
12 1 0.1 11 0 0 0 0 0 0 1 0
13 1 0.1 12 0 0 0 0 0 0 1 0
14 1 0.1 13 0 0 0 0 0 0 1 0
15 1 0.1 14 0 0 0 0 0 0 1 0
16 1 0.1 15 0 0 0 0 0 0 1 0
17 1 0.1 16 0 0 0 0 0 0 1 0
18 1 0.1 17 0 0 0 0 0 0 1 0
19 1 0.1 18 0 0 0 0 0 0 1 0
20 1 0.1 19 0 0 0 0 0 0 1 0
21 1 0.1 20 0 0 0 0 0 0 1 0
22 1 0.1 21 0 0 0 0 0 0 1 0
23 1 0.1 22 0 0 0 0 0 0 1 0
24 1 0.1 23 0 0 0 0 0 0 1 0
25 1 0.1 24 0 0 0 0 0 0 1 0
26 1 0.1 25 0 0 0 0 0 0 1 0
27 1 0.1 26 0 0 0 0 0 0 1 0
28 1 0.1 27 0 0 0 0 0 0 1 0
29 1 0.1 28 0 0 0 0 0 0 1 0
30 1 0.1 29 0 0 0 0 0 0 1 0
31 1 0.1 30 0 0 0 0 0 0 1 0
32 1 0.1 31 0 0 0 0 0 0 1 0
33 1 0.1 32 0 0 0 0 0 0 1 0
34 1 0.1 33 0 0 0 0 0 0 1 0
35 1 0.1 34 0 0 0 0 0 0 1 0
36 1 0.1 35 0 0 0 0 0 0 1 0
37 1 0.1 36 0 0 0 0 0 0 1 0
38 1 0.1 37 0 0 0 0 0 0 1 0
39 1 0.1 38 0 0 0 0 0 0 1 0
40 1 0.1 39 0 0 0 0 0 1 1 1
+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 0.1 0 0 0.04 0 0 90 all
+114
View File
@@ -0,0 +1,114 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧)
step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy)
step_plot_wave: 1 # 绘制波形能量动画
force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1
plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧)
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
engine: fortran # 默认使用 python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 300.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.000, 0.000, 0.000] # 重力场分量 (m/s²)
B: [0.005, 0.000, 0.005] # 阻尼分量
# ── 力开关(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: 200.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 100
# ── 时间步长 ──────────────────────────────────
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.0]
# 小球颜色
# 小球半径从 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
# ── 摄像机初始位置 ────────────────────────────
camera_distance: 60.0 # 摄像机到场景中心的距离
camera_elevation: 0.0 # 俯仰角(度),负值=俯视
camera_azimuth: 0.0 # 方位角(度)
camera_center_x: 30.0 # 摄像机注视点 x
camera_center_y: 0.0 # 摄像机注视点 y
camera_center_z: 0.0 # 摄像机注视点 z
move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动
# ── 视觉放大 ──────────────────────────────────
display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理)
+9
View File
@@ -0,0 +1,9 @@
# move_camera.txt — 摄像机速度段驱动
# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d
# vx/vy/vz: 平移速度(每帧移动单位)
# rx/ry/rz: 旋转速度(每帧度数)
# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留)
#
# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转
all vx=0.02
# 30-90 vy=0.02 ry=1 rz=1
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case06 1D atomic chain (transverse wave).
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 示例案例 case06")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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 300.0 1.0
+40
View File
@@ -0,0 +1,40 @@
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
+41
View File
@@ -0,0 +1,41 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 0 0 0 0 0 1 1
2 1 0.1 1 0 0 0 0 0 0 1 1
3 1 0.1 2 0 0 0 0 0 0 1 1
4 1 0.1 3 0 0 0 0 0 0 1 1
5 1 0.1 4 0 0 0 0 0 0 1 1
6 1 0.1 5 0 0 0 0 0 0 1 1
7 1 0.1 6 0 0 0 0 0 0 1 1
8 1 0.1 7 0 0 0 0 0 0 1 1
9 1 0.1 8 0 0 0 0 0 0 1 1
10 1 0.1 9 0 0 0 0 0 0 1 1
11 1 0.1 10 0 0 0 0 0 0 1 1
12 1 0.1 11 0 0 0 0 0 0 1 1
13 1 0.1 12 0 0 0 0 0 0 1 1
14 1 0.1 13 0 0 0 0 0 0 1 1
15 1 0.1 14 0 0 0 0 0 0 1 1
16 1 0.1 15 0 0 0 0 0 0 1 1
17 1 0.1 16 0 0 0 0 0 0 1 1
18 1 0.1 17 0 0 0 0 0 0 1 1
19 1 0.1 18 0 0 0 0 0 0 1 1
20 1 0.1 19 0 0 0 0 0 0 1 1
21 1 0.1 20 0 0 0 0 0 0 1 1
22 1 0.1 21 0 0 0 0 0 0 1 1
23 1 0.1 22 0 0 0 0 0 0 1 1
24 1 0.1 23 0 0 0 0 0 0 1 1
25 1 0.1 24 0 0 0 0 0 0 1 1
26 1 0.1 25 0 0 0 0 0 0 1 1
27 1 0.1 26 0 0 0 0 0 0 1 1
28 1 0.1 27 0 0 0 0 0 0 1 1
29 1 0.1 28 0 0 0 0 0 0 1 1
30 1 0.1 29 0 0 0 0 0 0 1 1
31 1 0.1 30 0 0 0 0 0 0 1 1
32 1 0.1 31 0 0 0 0 0 0 1 1
33 1 0.1 32 0 0 0 0 0 0 1 1
34 1 0.1 33 0 0 0 0 0 0 1 1
35 1 0.1 34 0 0 0 0 0 0 1 1
36 1 0.1 35 0 0 0 0 0 0 1 1
37 1 0.1 36 0 0 0 0 0 0 1 1
38 1 0.1 37 0 0 0 0 0 0 1 1
39 1 0.1 38 0 0 0 0 0 0 1 1
40 1 0.1 39 0 0 0 0 0 1 1 1
+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.1 0 0 0.04 0 0 0 0 90 all
+114
View File
@@ -0,0 +1,114 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧)
step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行
step_plot: 1 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 0 # 自动播放 VisPy 3D 动画窗口(需安装 vispy)
step_plot_wave: 1 # 绘制波形能量动画
force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1
plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧)
# ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java
engine: c # 默认使用 python 引擎
# ── 盒子 ──────────────────────────────────────
box_a: 300.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.000, 0.000, 0.000] # 重力场分量 (m/s²)
B: [0.005, 0.000, 0.005] # 阻尼分量
# ── 力开关(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: 10.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 20
# ── 时间步长 ──────────────────────────────────
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.0]
# 小球颜色
# 小球半径从 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
# ── 摄像机初始位置 ────────────────────────────
camera_distance: 120.0 # 摄像机到场景中心的距离
camera_elevation: 0.0 # 俯仰角(度),负值=俯视
camera_azimuth: 0.0 # 方位角(度)
camera_center_x: 60.0 # 摄像机注视点 x
camera_center_y: 0.0 # 摄像机注视点 y
camera_center_z: 0.0 # 摄像机注视点 z
move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动
# ── 视觉放大 ──────────────────────────────────
display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理)
+9
View File
@@ -0,0 +1,9 @@
# move_camera.txt — 摄像机速度段驱动
# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d
# vx/vy/vz: 平移速度(每帧移动单位)
# rx/ry/rz: 旋转速度(每帧度数)
# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留)
#
# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转
all vx=0.02
# 30-90 vy=0.02 ry=1 rz=1
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case06 1D atomic chain (transverse wave).
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 示例案例 case06")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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
h 100.0 1.0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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
5101 0 0 2.0 0 0 0.05 90 90 90 all
+89
View File
@@ -0,0 +1,89 @@
# 物理模拟参数配置
# 格式:YAML
# 用法:python run_dynamics.py
# ── 流程控制 ──────────────────────────────────
step_simulate: 1 # 运行物理模拟
step_sample: 0 # 重新抽帧,默认0=不执行
step_plot: 0 # 绘制轨迹/能量图
step_animation: 1 # 自动播放 VisPy 3D 动画窗口
step_plot_wave: 0 # 绘制波形能量动画
force_calc: 1 # 强制重新计算
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0 # 0=不保留完整轨迹文件
# ── 计算引擎 ──────────────────────────────────
engine: c
# ── 盒子 ──────────────────────────────────────
box_a: 120.0
# ── 初始构型 ──────────────────────────────────
coord_file: input/coord.txt
connection_file: input/connection.txt
bond_file: input/bond.txt
driver_file: input/driver.txt
# 绘图/动画展示的原子序号
plot_atom: 5101 # 中心原子 (0,0)
# ── 物理参数 ──────────────────────────────────
G: [0.000, 0.000, 0.000]
B: [0.000, 0.000, 0.000]
# ── 力开关 ────────────────────────────────────
gravity_field: 0
gravity_interaction: 0
elastic_force: 1
damping_force: 0
driving_force: 1
gravity_strength: 1.0
# ── 数值算法 ──────────────────────────────────
method: leapfrog
# ── 步骤控制 ──────────────────────────────────
warmup_steps: 0 # 受迫波动,无需预热
T_total: 100.0
NSTEP: 10
DT: 0.01
sample_start: null
sample_end: null
# ── 渲染方式 ──────────────────────────────────
use_marker: 1
# ── 位移着色 ──────────────────────────────────
display_color: {
x : [0, [255, 0, 0]],
y : [0, [ 0, 255, 0]],
z : [1, [ 0, 0, 255]],
xy : [0, [255, 255, 0]],
yz : [0, [ 0, 255, 255]],
zx : [0, [255, 0, 255]],
xyz : [0, [ 0, 0, 0]],
}
# ── 显示参数 ──────────────────────────────────
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
ball_color_r: 0.20
ball_color_g: 0.60
ball_color_b: 0.90
box_color_r: 0.80
box_color_g: 0.80
box_color_b: 0.85
# ── 摄像机 ────────────────────────────────────
camera_distance: 120.0
camera_elevation: 60.0
camera_azimuth: -45.0
camera_center_x: 0.0
camera_center_y: 0.0
camera_center_z: 0.0
move_camera: 0
display_amp: [1.0, 1.0, 1.0]
+2
View File
@@ -0,0 +1,2 @@
0 0 50
0 0 80
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case11 2D grid (61x61 atomic mesh).
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 示例案例 case11")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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
h 100.0 1.0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
3081 0 0 2.0 0 0 0.05 90 90 90 all
7121 0 0 2.0 0 0 0.05 90 90 90 all
+83
View File
@@ -0,0 +1,83 @@
# 物理模拟参数配置
# case12 — 二维网格两点干涉(双点源 z 方向驱动)
# 驱动点: (0,-10) 和 (0,10),波从两点向外传播,在中心区域干涉
# ── 流程控制 ──────────────────────────────────
step_simulate: 1 # 运行物理模拟
step_sample: 0 # 重新抽帧,默认0=不执行
step_plot: 0 # 绘制轨迹/能量图
step_animation: 1 # 自动播放 VisPy 3D 动画窗口
step_plot_wave: 0 # 绘制波形能量动画
force_calc: 1 # 强制重新计算
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0
# ── 计算引擎 ──────────────────────────────────
engine: c
# ── 盒子 ──────────────────────────────────────
box_a: 120.0
# ── 初始构型 ──────────────────────────────────
coord_file: input/coord.txt
connection_file: input/connection.txt
bond_file: input/bond.txt
driver_file: input/driver.txt
plot_atom: 5101 # 中心区域用于信息显示
# ── 物理参数 ──────────────────────────────────
G: [0.000, 0.000, 0.000]
B: [0.000, 0.000, 0.000]
gravity_field: 0
gravity_interaction: 0
elastic_force: 1
damping_force: 0
driving_force: 1
gravity_strength: 1.0
method: leapfrog
# ── 步骤控制 ──────────────────────────────────
warmup_steps: 0
T_total: 100.0
NSTEP: 500
DT: 0.001
sample_start: null
sample_end: null
# ── 渲染/着色 ─────────────────────────────────
use_marker: 1
display_color: {
x : [0, [255, 0, 0]],
y : [0, [ 0, 255, 0]],
z : [0, [ 0, 0, 255]],
xy : [0, [255, 255, 0]],
yz : [0, [ 0, 255, 255]],
zx : [0, [255, 0, 255]],
xyz : [1, [255, 255, 255]],
}
# ── 显示参数 ──────────────────────────────────
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
ball_color_r: 0.20
ball_color_g: 0.60
ball_color_b: 0.90
box_color_r: 0.80
box_color_g: 0.80
box_color_b: 0.85
# ── 摄像机 ────────────────────────────────────
camera_distance: 120.0
camera_elevation: 60.0
camera_azimuth: -45.0
camera_center_x: 0.0
camera_center_y: 0.0
camera_center_z: 0.0
move_camera: 0
display_amp: [1.0, 1.0, 1.0]
+2
View File
@@ -0,0 +1,2 @@
0 0 50
0 0 80
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case12 2D grid dual source interference.
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 示例案例 case11")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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
h 100.0 1.0
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
1 0 0 2.0 0 0 0.05 90 90 90 all
102 0 0 2.0 0 0 0.05 90 90 90 all
203 0 0 2.0 0 0 0.05 90 90 90 all
304 0 0 2.0 0 0 0.05 90 90 90 all
405 0 0 2.0 0 0 0.05 90 90 90 all
506 0 0 2.0 0 0 0.05 90 90 90 all
607 0 0 2.0 0 0 0.05 90 90 90 all
708 0 0 2.0 0 0 0.05 90 90 90 all
809 0 0 2.0 0 0 0.05 90 90 90 all
910 0 0 2.0 0 0 0.05 90 90 90 all
1011 0 0 2.0 0 0 0.05 90 90 90 all
1112 0 0 2.0 0 0 0.05 90 90 90 all
1213 0 0 2.0 0 0 0.05 90 90 90 all
1314 0 0 2.0 0 0 0.05 90 90 90 all
1415 0 0 2.0 0 0 0.05 90 90 90 all
1516 0 0 2.0 0 0 0.05 90 90 90 all
1617 0 0 2.0 0 0 0.05 90 90 90 all
1718 0 0 2.0 0 0 0.05 90 90 90 all
1819 0 0 2.0 0 0 0.05 90 90 90 all
1920 0 0 2.0 0 0 0.05 90 90 90 all
2021 0 0 2.0 0 0 0.05 90 90 90 all
2122 0 0 2.0 0 0 0.05 90 90 90 all
2223 0 0 2.0 0 0 0.05 90 90 90 all
2324 0 0 2.0 0 0 0.05 90 90 90 all
2425 0 0 2.0 0 0 0.05 90 90 90 all
2526 0 0 2.0 0 0 0.05 90 90 90 all
2627 0 0 2.0 0 0 0.05 90 90 90 all
2728 0 0 2.0 0 0 0.05 90 90 90 all
2829 0 0 2.0 0 0 0.05 90 90 90 all
2930 0 0 2.0 0 0 0.05 90 90 90 all
3031 0 0 2.0 0 0 0.05 90 90 90 all
3132 0 0 2.0 0 0 0.05 90 90 90 all
3233 0 0 2.0 0 0 0.05 90 90 90 all
3334 0 0 2.0 0 0 0.05 90 90 90 all
3435 0 0 2.0 0 0 0.05 90 90 90 all
3536 0 0 2.0 0 0 0.05 90 90 90 all
3637 0 0 2.0 0 0 0.05 90 90 90 all
3738 0 0 2.0 0 0 0.05 90 90 90 all
3839 0 0 2.0 0 0 0.05 90 90 90 all
3940 0 0 2.0 0 0 0.05 90 90 90 all
4041 0 0 2.0 0 0 0.05 90 90 90 all
4142 0 0 2.0 0 0 0.05 90 90 90 all
4243 0 0 2.0 0 0 0.05 90 90 90 all
4344 0 0 2.0 0 0 0.05 90 90 90 all
4445 0 0 2.0 0 0 0.05 90 90 90 all
4546 0 0 2.0 0 0 0.05 90 90 90 all
4647 0 0 2.0 0 0 0.05 90 90 90 all
4748 0 0 2.0 0 0 0.05 90 90 90 all
4849 0 0 2.0 0 0 0.05 90 90 90 all
4950 0 0 2.0 0 0 0.05 90 90 90 all
5051 0 0 2.0 0 0 0.05 90 90 90 all
5152 0 0 2.0 0 0 0.05 90 90 90 all
5253 0 0 2.0 0 0 0.05 90 90 90 all
5354 0 0 2.0 0 0 0.05 90 90 90 all
5455 0 0 2.0 0 0 0.05 90 90 90 all
5556 0 0 2.0 0 0 0.05 90 90 90 all
5657 0 0 2.0 0 0 0.05 90 90 90 all
5758 0 0 2.0 0 0 0.05 90 90 90 all
5859 0 0 2.0 0 0 0.05 90 90 90 all
5960 0 0 2.0 0 0 0.05 90 90 90 all
6061 0 0 2.0 0 0 0.05 90 90 90 all
6162 0 0 2.0 0 0 0.05 90 90 90 all
6263 0 0 2.0 0 0 0.05 90 90 90 all
6364 0 0 2.0 0 0 0.05 90 90 90 all
6465 0 0 2.0 0 0 0.05 90 90 90 all
6566 0 0 2.0 0 0 0.05 90 90 90 all
6667 0 0 2.0 0 0 0.05 90 90 90 all
6768 0 0 2.0 0 0 0.05 90 90 90 all
6869 0 0 2.0 0 0 0.05 90 90 90 all
6970 0 0 2.0 0 0 0.05 90 90 90 all
7071 0 0 2.0 0 0 0.05 90 90 90 all
7172 0 0 2.0 0 0 0.05 90 90 90 all
7273 0 0 2.0 0 0 0.05 90 90 90 all
7374 0 0 2.0 0 0 0.05 90 90 90 all
7475 0 0 2.0 0 0 0.05 90 90 90 all
7576 0 0 2.0 0 0 0.05 90 90 90 all
7677 0 0 2.0 0 0 0.05 90 90 90 all
7778 0 0 2.0 0 0 0.05 90 90 90 all
7879 0 0 2.0 0 0 0.05 90 90 90 all
7980 0 0 2.0 0 0 0.05 90 90 90 all
8081 0 0 2.0 0 0 0.05 90 90 90 all
8182 0 0 2.0 0 0 0.05 90 90 90 all
8283 0 0 2.0 0 0 0.05 90 90 90 all
8384 0 0 2.0 0 0 0.05 90 90 90 all
8485 0 0 2.0 0 0 0.05 90 90 90 all
8586 0 0 2.0 0 0 0.05 90 90 90 all
8687 0 0 2.0 0 0 0.05 90 90 90 all
8788 0 0 2.0 0 0 0.05 90 90 90 all
8889 0 0 2.0 0 0 0.05 90 90 90 all
8990 0 0 2.0 0 0 0.05 90 90 90 all
9091 0 0 2.0 0 0 0.05 90 90 90 all
9192 0 0 2.0 0 0 0.05 90 90 90 all
9293 0 0 2.0 0 0 0.05 90 90 90 all
9394 0 0 2.0 0 0 0.05 90 90 90 all
9495 0 0 2.0 0 0 0.05 90 90 90 all
9596 0 0 2.0 0 0 0.05 90 90 90 all
9697 0 0 2.0 0 0 0.05 90 90 90 all
9798 0 0 2.0 0 0 0.05 90 90 90 all
9899 0 0 2.0 0 0 0.05 90 90 90 all
10000 0 0 2.0 0 0 0.05 90 90 90 all
10101 0 0 2.0 0 0 0.05 90 90 90 all
+73
View File
@@ -0,0 +1,73 @@
# 物理模拟参数配置
# case13 — 二维网格平面波(左边界驱动,右边界吸收)
# 左边界全部 101 原子齐振驱动 → 波从左向右传播 → 右边界全固定
step_simulate: 1
step_sample: 0
step_plot: 0
step_animation: 1
step_plot_wave: 0
force_calc: 1
save_trajectory: 0
engine: c
box_a: 120.0
coord_file: input/coord.txt
connection_file: input/connection.txt
bond_file: input/bond.txt
driver_file: input/driver.txt
plot_atom: 51 # 左边界中间原子用于信息显示
G: [0.000, 0.000, 0.000]
B: [0.000, 0.000, 0.000]
gravity_field: 0
gravity_interaction: 0
elastic_force: 1
damping_force: 0
driving_force: 1
gravity_strength: 1.0
method: leapfrog
warmup_steps: 0
T_total: 200.0
NSTEP: 100
DT: 0.01
sample_start: null
sample_end: null
# ── 渲染/着色 ─────────────────────────────────
use_marker: 1
display_color: {
x : [1, [255, 0, 0]],
y : [1, [ 0, 255, 0]],
z : [1, [ 0, 0, 255]],
xy : [0, [255, 255, 0]],
yz : [0, [ 0, 255, 255]],
zx : [0, [255, 0, 255]],
xyz : [0, [255, 255, 255]],
}
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
ball_color_r: 0.20
ball_color_g: 0.60
ball_color_b: 0.90
box_color_r: 0.80
box_color_g: 0.80
box_color_b: 0.85
camera_distance: 120.0
camera_elevation: 60.0
camera_azimuth: -45.0
camera_center_x: 0.0
camera_center_y: 0.0
camera_center_z: 0.0
move_camera: 0
display_amp: [1.0, 1.0, 1.0]
+2
View File
@@ -0,0 +1,2 @@
0 0 50
0 0 80
+54
View File
@@ -0,0 +1,54 @@
"""
Case runner for Dynamics case13 2D grid (61x61 atomic mesh).
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 示例案例 case13")
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()
+40
View File
@@ -0,0 +1,40 @@
# case06: 一维原子链横波模拟
60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。
## 物理设定
| 参数 | 值 |
|---|---|
| 原子数 | 120 |
| 排列 | 沿 x 轴等间距排列,间距为 1 |
| 约束 | 原子**沿 z 方向自由振动**fix_x=1, fix_y=1, fix_z=0),x, y 锁定 |
| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 |
| 重力 | 无 |
| 万有引力 | 无 |
| 阻尼 | 无 |
| 驱动力 | 原子 1(z 方向驱动) |
| 算法 | leapfrog(蛙跳法,能量守恒) |
## 驱动力
原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定:
```math
z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z)
```
当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。
## 动力学行为
原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。
## 使用方法
```bash
cd examples/case06
python run_dynamics.py
```
配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`
+477
View File
@@ -0,0 +1,477 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>case06 — 一维原子链驱动力学模拟 | 物理原理 &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">120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动</p>
<span class="badge">case06 · examples/case06</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>120 个原子沿 <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/case06
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>W / S 键</td><td>相机沿 Z 轴向前 / 向后移动(靠近/远离场景)</td></tr>
<tr><td>A / D 键</td><td>视角向右 / 向左平移</td></tr>
<tr><td>E / Q 键</td><td>视角上升 / 下降(屏幕方向)</td></tr>
<tr><td>C / X 键</td><td>增大 / 减小步长</td></tr>
<tr><td>V 键</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>
</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><strong>step_plot_wave</strong></td><td>不生成波形图</td><td>生成波形能量动画 GIF</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>case06/
├── input/
│ ├── input.txt # 主配置文件(YAML 格式)
│ ├── coord.txt # 原子坐标(120 个原子)
│ ├── connection.txt # 弹簧连接关系(59 条键)
│ ├── bond.txt # 弹簧参数(k=1.0, L₀=1.0
│ └── <strong>driver.txt</strong> # <span class="cm">驱动力定义(本案例新增)</span>
├── output/
│ ├── trajectory.txt # 全量轨迹数据(50000 步 × 120 原子)
│ ├── display.txt # 抽帧后的动画数据(500 帧 × 120 原子)
│ ├── dynamics.log # 计算日志
│ ├── animation.log # 动画启动日志(闪退时排查用)
│ └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
├── 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
h 100.0 1.0

Some files were not shown because too many files have changed in this diff Show More