Compare commits

..

64 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
admin ea99f09f9b feat: add energy flux density panel to plot_wave animation
Add compute_energy_flux() using the Hardy formula:
  J_b = 1/2 * F_bond_on_i * (v_i + v_j)

New 4th subplot shows J vs bond position (x coordinate of bond midpoint).
J > 0: energy flows rightward; J = 0: standing wave; J < 0: leftward.
Ideal standing wave would show J ≈ 0 everywhere.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 08:54:01 +08:00
admin b584c4489c refactor: merge wave/energy panels into 3 vertical subplots
- Plot 1: x/y/z displacements overlaid on one axes
- Plot 2: per-atom KE/PE/total energy overlaid on one axes
- Plot 3: system energy vs time (unchanged)
All three stacked vertically. Shared y-axis scale within each panel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 08:32:41 +08:00
admin 2ab3436235 feat: redesign plot_wave with per-atom energy panels
New 4x2 layout: left col = x/y/z displacement waves, right col = per-atom KE/PE/total energy + system energy vs time.
PE split 50/50 for normal bonds; 100% to non-driven atom when bonded to driver; driven atom PE = E_SHO - KE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 08:26:51 +08:00
admin 39ff650539 fix: show interactive window when plot_wave_save_gif/mp4 are both 0
Use Agg backend and show=False only when saving to file.
When neither gif nor mp4 is requested, show the animation window interactively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 08:14:40 +08:00
admin 1cefe184d7 fix: plot_wave plt.show() crash when called non-interactively
Add show=False parameter to plot_wave(); when called from dynamics.py,
pass show=False and set matplotlib Agg backend to avoid NonGuiException.
Also print full traceback on failure for easier debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 08:10:16 +08:00
admin d371b28acc feat: add display_amp parameter for visual displacement amplification
Supports display_amp: [ax, ay, az] in input.txt. On rendering, each atom's
displacement from its frame-0 equilibrium is multiplied by the corresponding
factor. Physics is unchanged; only the rendered positions are scaled.

Useful for visualizing small-amplitude waves that would otherwise be invisible.
Example: display_amp: [1.0, 1.0, 5.0] exaggerates z-direction motion 5x.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 07:28:56 +08:00
admin d489222eaf fix: driven atoms now oscillate around their initial equilibrium position
Previously, apply_driving_force set absolute position to A*cos(2π f t + φ),
ignoring the atom's initial coordinates. For atoms not at the origin (e.g.,
atom 120 at x=119), this incorrectly forced them back toward the origin each
step, causing severe distortion and numerical explosion.

Fix: store each driven atom's initial position as eq_pos/eq_x/eq_y/eq_z at
load time; position is now eq + A*cos(2π f t + φ) in all four engines
(Python, C, C++, Fortran).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 07:23:25 +08:00
admin e62e536cee feat: 真蛙跳法重构(Python/C/C++/Fortran 四引擎统一)
- 新增 compute_accel_conservative / accel_conservative:
  保守力加速度(弹簧+重力+原子间引力),不含阻尼,供蛙跳专用
- 重写 leapfrog_step / leapfrog_full:
  - 无阻尼:纯辛积分器,每步 1 次力计算(原 Velocity-Verlet 需 2 次)
  - 有阻尼:半隐式处理 v(t+dt/2)=[v(t-dt/2)*(1-α)+a_c*dt]/(1+α),无条件稳定
- 主循环加初始化反向半步 v(-dt/2)=v(0)-0.5*a_c(0)*dt
- 修复 C/C++ number of frames 字段写采样帧数而非总积分步数的 bug
- Python 引擎:新增 display.npz 二进制格式,draw.py/plot_wave.py 优先读取
- 编译参数统一为 -O3 -march=native -ffast-math
2026-06-12 18:36:37 +08:00
admin e1ade53fff docs: 综合三工具六版本分析,输出 workbuddy_v2.md
基于 workbuddy.md / claude.md / codex.md 三份原始分析
及 claude_v1.md / codex_v1.md / workbuddy_v1.md 三份综合
版本,输出最终优化方案。

核心改进:
- 三工具角色定位框架(QA/Perf/Architect)
- Bug 三级分类(真 Bug / 已修复 / 疑似误报)
- 三阶段方案(修正确性→做性能→做治理)
- 自我评价与修正(v1→v2 改进表)
- 10 场景工具选择速查表
2026-06-12 15:45:47 +08:00
admin d930fb558c docs: 综合三方工具分析,输出最终优化方案 workbuddy_v1.md
对比 WorkBuddy/Claude/Codex 三款 AI 工具对同一代码库的
优化建议,以表格形式评价各自优劣(Bug 发现/代码质量/战略
思维/代码示例),最终整合为 6 阶段实施计划(11 人天)。
2026-06-12 15:39:31 +08:00
admin 782422e800 docs: 添加优化建议文档 optimization/workbuddy.md
涵盖架构/性能/代码质量/配置/测试/引擎一致性/UX 等
9 个方面、26 条具体建议,标注实施优先级和工作量。
2026-06-12 15:15:10 +08:00
admin e40393d793 fix: display.txt 缺少渲染参数导致盒子不透明
外部引擎(C/C++)直接写 display.txt 时只输出基础物理参数
(DT/NSTEP/method 等),缺少 alpha/ball_color/box_color
等渲染参数,draw.py 读取不到 alpha 回退默认 0.2。

修复:
1. param.json 新增渲染参数(alpha/ball_color/box_color/...)
2. C/C++ 引擎 SimParams 新增对应字段 & JSON 读取
3. C/C++ write_display_txt 写入所有渲染参数 header
4. param.json ball_color/box_color 用数组统一存储
2026-06-12 15:05:46 +08:00
admin dc7bc00616 feat: C/C++ 引擎支持 save_trajectory=0 时直接写 display.txt
所有引擎(Python/C/C++)在 save_trajectory=0 时行为一致:
- 计算时按 NSTEP 抽帧,只存 sampled 缓冲区
- 直接写入 display.txt(新文本格式)
- 不生成 trajectory.txt

Python 引擎:run_simulation 已支持 
C 引擎:采样缓冲区 + write_display_txt 
C++ 引擎:采样缓冲区 + write_display_txt 
Fortran 引擎:待完成

compute.py run_engine:save_trajectory=0 时跳过 trajectory.txt 加载
dynamics.py:引擎直接输出 display.txt 时跳过抽帧步骤
2026-06-12 08:25:27 +08:00
admin 41790a782a fix: save_trajectory=0 时删除 trajectory.txt,所有引擎保持一致
Python 引擎:run_simulation 已正确支持 save_trajectory 
外部引擎(C/C++/Fortran):save_trajectory_txt 仅在
  save_trajectory=1 时调用;display.txt 生成后删除 trajectory.txt
补充:移除 compute.py 中重复的 'global use_marker' 和占位符
2026-06-12 08:18:07 +08:00
admin 6298ed5b34 fix: 外部引擎路径 display.txt 未随 T_total 变更刷新
当 T_total 从 20→100 时,外部引擎写入的 trajectory.txt 是新的,
但 display.txt 缓存未刷新(检查是否存在而非检查是否过期),
导致总帧数仍为 200(NT=20000 时的数据)。

修复:外部引擎路径总是从 trajectory.txt 重新抽帧生成 display.txt。
2026-06-12 08:11:44 +08:00
admin b95a3579fc feat: move_camera.txt 支持 all 关键词表示全程执行
在帧位置写 all 表示该段对所有帧生效:
  all  vx=0.1  ry=0.5    # 全程缓慢平移 + 旋转

等同于 start=0, end=INF,与普通区间段一样支持时间交叠。
2026-06-12 08:08:36 +08:00
admin b4fed4fbb8 fix: 运动相机时间交叠时所有段依次作用
之前只取了第一个活动段 active[0],时间交叠时后面的段被忽略。
改为遍历所有段,按文件顺序依次施加平移和旋转。
排在前面的段优先作用于相机位置(矩阵非对易性保证)。
2026-06-12 08:06:15 +08:00
admin c454162d0b feat: draw.py 直接读取 move_camera.txt,修改后重启即生效
draw.py 启动时优先读取 input/move_camera.txt(实时文件),
不存在或为空时回退到 display.txt header 中的缓存数据。
改动 move_camera.txt 后只需重启动画窗口(关掉旧窗口重新
run_dynamics.py),无需重新跑模拟。
2026-06-12 08:02:50 +08:00
admin e40f7a49e4 feat: move_camera.txt 改为速度段格式驱动相机运动
格式:
  1-60   vx=1.0  rx=10         # 1-60帧:x平移1/帧 + 绕x转10°/帧
  30-90  vy=2.0  ry=20  rz=10  # 30-90帧:y平移2/帧 + 绕y转20°/帧 + 绕z转10°/帧

draw.py 每帧累加平移速度修改center,累加旋转速度修改
elevation/azimuth,实现连续平滑的相机运动。
2026-06-12 07:58:08 +08:00
admin 22b94011ee feat: 运动相机支持 + move_camera.txt 关键帧驱动
input.txt 新增:
  move_camera: 0  # 0=固定视角, 1=按 move_camera.txt 运动

move_camera.txt 格式(4列:帧号 距离 俯仰角 方位角):
  0    40.0   0     0
  100  80.0  -30  180
  200  40.0   0   360

display.txt header 传递 camera_keyframes JSON 数组,
draw.py 按帧时间线性插值驱动相机运动(循环播放)。
2026-06-12 07:52:06 +08:00
admin f1afb7c479 feat: 摄像机初始位置可在 input.txt 配置
新增 input.txt 字段:
  camera_distance:  40.0   # 到场景中心的距离
  camera_elevation: 0      # 俯仰角
  camera_azimuth:   0      # 方位角

通过 display.txt header 传递到 draw.py,
不再硬编码在 draw.py 中。
2026-06-12 07:48:37 +08:00
admin 757a891a43 feat: display.txt 传递原子半径数组,draw.py 读取 per-atom 半径
之前所有原子使用统一的 ball_radius(来自 input.txt),
现在 display.txt header 包含 atom_radii 字段(逗号分隔
的半径数组),draw.py 据此为每个原子设置独立半径。
fallback:若没有 atom_radii 字段,仍使用 ball_radius。
2026-06-12 07:46:28 +08:00
admin 466a301d34 fix: alpha numpy 数组格式化问题导致逗号丢失
trajectory.txt 中的 alpha 以 numpy 数组形式加载,
str() 后变成 "[0. 0. 0. 0. 0. 0.]"(无逗号),
draw.py parse 不成功回退为默认 0.2。
新增 _fmt_alpha() 统一格式化为逗号分隔字符串。
2026-06-12 07:28:05 +08:00
admin 54aa20d7c5 fix: display.txt 缺失 alpha 透明度参数,draw.py 读取不到
alpha(盒子透明度)未写入 display.txt header,
draw.py 回退到默认 0.2 而非 input.txt 配置值。
现在 alpha 通过 header 字段正确传递。

Python 引擎:alpha 支持单值或数组 → 逗号分隔字符串
外部引擎:从 trajectory.txt 读取 alpha 值
2026-06-12 07:24:38 +08:00
admin c3e50d265d ui: 缩小 ball_info 字体 28 → 18,适配更多信息显示 2026-06-12 07:07:26 +08:00
admin 6b5e12da46 fix: display.txt 丢失 use_marker 导致强制使用 Sphere 渲染模式
新格式 display.txt 未包含 use_marker 字段,draw.py 中
USE_MARKER 被硬编码为 0(Sphere 模式),对 120 个原子
每帧更新 120 次 STTransform 极慢。

修复:将 use_marker 加入 display.txt header,draw.py 从
header 读取而非硬编码。case06 配置 use_marker: 1 会
自动启用 Marker(GPU 实例化)模式,动画流畅。
2026-06-12 07:04:29 +08:00
admin 7417d47658 perf: 重写 load_display_txt 使用 np.genfromtxt 批量解析
旧实现逐行 split()+float() 解析数据行要几十秒,
新实现将数据行收集后用 np.genfromtxt 一次性批量解析,
加载 200帧×120原子 仅需 0.087s(比之前快 100x+)。
2026-06-12 07:01:52 +08:00
admin a3fa8b90f6 feat: display.txt 新增 T_total 头字段(总模拟时间=NT×DT) 2026-06-12 06:55:38 +08:00
admin ab3a847483 fix: display.txt 的 number of frames 改为实际抽帧数而非总步数
之前 number of frames 错误地填入了 record_steps(=NT),
实际应该填入 n_frames_actual(=NT/NSTEP)。如 NT=20000,
NSTEP=100 时抽得 200 帧,现在正确显示为 200。
总步数仍在 dynamic_steps 中记录。
2026-06-12 06:51:59 +08:00
admin 0c332b7dfc feat: display.txt 新增 dynamic_steps 头字段记录实际计算步数 2026-06-12 06:47:17 +08:00
admin 9d1f84d2bf refactor: 引擎直接抽帧 + 新 display.txt 纯文本格式 + save_trajectory 开关
核心变更:
1. compute.py: run_simulation 直接按 NSTEP 抽帧写 display.txt(新格式)
   - 新格式:纯文本,帧 1→n 分块,每行: n x y z vx vy vz
   - 新函数 save_display_txt() / load_display_txt()
   - save_trajectory 参数(默认0=不保留 trajectory.txt)
2. dynamics.py: 移除旧 JSON 采样逻辑,自动检测 display.txt
   - Python 引擎直接读取引擎输出的 display.txt
   - 外部引擎仍写 trajectory.txt,自动抽帧转 display.txt
3. draw.py: 适配 load_display_txt() 新格式
4. case06/input.txt: 添加 save_trajectory: 0, step_sample: 0

TODO: 外部引擎(C/C++/Fortran)内部抽帧写 display.txt
TODO: plot_wave.py 适配新格式
TODO: 其他案例 input.txt 更新默认值
2026-06-12 06:36:50 +08:00
admin c158c74609 perf: 降低 JSON 输出精度 15→8 位 + 添加 I/O 阶段提示
- C/C++/Fortran 引擎:%.15g/setprecision(15)/g0 → %.8g/g0.8
- 添加 "正在写入轨迹数据…" 提示,说明 100% 后的等待原因
- trajectory.txt 文件从 419MB → 407MB(仍有优化空间)
2026-06-12 05:53:27 +08:00
admin 52505e9aff fix(compute): 修复进度条跳变(20%→100%)
原因:外部引擎 stdout 管道缓存,Python 每 0.2s 读一行,
引擎结束时管道中大量库存进度消息没被用于更新进度条。

修复:
1. 子进程退出时扫描残留 stdout 中的 progress 消息并更新 pbar
2. sleep 从 0.2s 降到 0.05s,提高读取频率
2026-06-11 22:48:34 +08:00
admin db50ac6d4d feat: 外部引擎实时进度条 + C引擎read_bonds rewind修复
1. 引擎端:C/C++/Fortran 主循环每 1% 输出 progress 到 stdout
2. compute.py:读取 "[xxx] progress: N/total" 行更新 tqdm
3. 移除不准的时间估算逻辑,改用真实引擎进度
4. C引擎 read_bonds:rewind 后补 fgets 跳表头
5. gitignore 添加 output_test/
2026-06-11 21:36:30 +08:00
admin 42c6776eff chore: 添加 output_test 到 gitignore 2026-06-11 19:42:23 +08:00
admin 9d5997afec fix(c): read_bonds 中 rewind 后未跳过表头行导致成键数据全为空
rewind(f) 将文件指针拉回开头(含表头 'n1 n2 bond_name'),
后续 fscanf 试图将 'n1' 解析为 %d 全部失败,导致:
- bond_pairs 直接用未初始化的栈垃圾 → 随机索引
- bond_stiffness/rest_lengths 保持默认值 1.0/2.0
- 弹簧力无法正确传播 → 第一个原子动后后面全不动

修复:rewind 后加 fgets(line, ...) 再次跳过表头。
2026-06-11 19:38:10 +08:00
admin e353e04133 fix(compute): 校准测速使用真实临时目录替代 os.devnull
os.devnull 在 Windows 上为 NUL,外部引擎(C/C++/Fortran)
试图写入 NUL/trajectory.txt 会失败退出,导致校准时间
完全无效,进度条按错误估计跑(例如卡在 59% 不动)。

改为创建 _calib_out 临时目录,校准后清理。
现在进度条显示正确的剩余时间估计(如 [00:00<00:11])。
2026-06-11 19:25:26 +08:00
admin 1fd87cc33b fix(fortran): 修复 JSON 输出缺少逗号导致解析失败
write_json 中 bond_rest_lengths 后面缺少逗号,
导致 JSON 解码器在 driving_force 前报错:
  JSONDecodeError: Expecting ',' delimiter
将 has_next 从 .false. 改为 .true.,空数组版本也补上逗号。
2026-06-11 19:14:54 +08:00
admin b783cbb981 fix(fortran): 修复 read_coord 中 line(0:0) 字符串越界导致崩溃
错误码 3221225785 (0xC0000005 = STATUS_ACCESS_VIOLATION) 由
read_coord 中列数统计的双重条件导致:
  line(i:i) /= ' ' .and. (i == 1 .or. line(i-1:i-1) == ' ')
Fortran 不保证 .or. 短路求值,当 i=1 时 line(0:0) 触发
内存越界。拆分为嵌套 if 块,确保只有在 i>1 时才访问
line(i-1:i-1)。
2026-06-11 19:11:34 +08:00
admin b9ec622808 fix: C/C++/Fortran 引擎补齐 wrap_position 和 t=0 驱动力
- 三引擎均新增 wrap_position 周期边界回绕(调用在 limit_in_box 后)
- 三引擎均新增 t=0 驱动力初始调用(在预热循环前)
- 至此三引擎算法与 Python 完全一致
2026-06-11 18:57:41 +08:00
162 changed files with 372245 additions and 3711 deletions
+8 -6
View File
@@ -1,7 +1,9 @@
# 全部使用 LF 换行,仓库内外一致,不随系统自动转换 # 引擎 DLL 是二进制文件,禁止行尾转换
* text eol=lf engines/release/*.dll binary
# 二进制文件不转换 # Python 源码使用 LF
*.png binary *.py text eol=lf
*.jpg binary
*.ico binary # Makefile 使用 LF
Makefile text eol=lf
*.mk text eol=lf
+8 -6
View File
@@ -18,10 +18,9 @@ pip-wheel-metadata/
venv/ venv/
ENV/ ENV/
# ── C / C++ 编译产物 ───────────────────────────────────────── # ── C / C++ / Fortran 编译产物 ────────────────────────
# Makefile 构建输出(engines/c/build/ # 源码目录 engines/src/*/ 中可能产生的构建输出
engines/c/build/ engines/src/*/build/
engines/cpp/build/
# CMake 构建目录(根目录或自定义 build 目录) # CMake 构建目录(根目录或自定义 build 目录)
CMakeCache.txt CMakeCache.txt
@@ -42,12 +41,11 @@ build_*/
*.so *.so
*.so.* *.so.*
*.dylib *.dylib
*.dll
# 可执行文件(保留源码,排除编译出的二进制) # 可执行文件(保留源码,排除编译出的二进制)
# 注意:Windows 下 .exe 后缀的可执行文件 # 注意:Windows 下 .exe 后缀的可执行文件
*.exe *.exe
# engines/c/Makefile 里指定了 build/ 目录,已由上面覆盖 # 可在 engines/src/*/ 中用 make dll 编译引擎 DLL
# 运行时生成的引擎参数文件(每次运行都会覆盖) # 运行时生成的引擎参数文件(每次运行都会覆盖)
engines/*/param.json engines/*/param.json
@@ -99,3 +97,7 @@ desktop.ini
*.zip *.zip
*.tar.gz *.tar.gz
*.tar.bz2 *.tar.bz2
output_test/
optimization
optimization/*
+870 -67
View File
File diff suppressed because it is too large Load Diff
+412 -72
View File
@@ -1,9 +1,8 @@
"""VisPy 演示:加载预计算轨迹数据,驱动小球运动动画。 """VisPy 演示:加载预计算轨迹数据,驱动小球运动动画。
计算与显示完全分离: 计算与显示完全分离:
1. 运行 compute.py → 生成 output/trajectory.txt(全量 NT 步轨迹 1. 运行 run_dynamics.py → 生成 output/display.txt(新格式,直接抽帧
2. 再运行 sample.py → 从 output/trajectory.txt 抽帧生成 output/display.txt 2. 本文件加载 output/display.txt,按帧播放动画
3. 本文件加载 output/display.txt,按帧播放动画
用法: 用法:
python draw.py # 使用 dynamics 根目录下的 output/ python draw.py # 使用 dynamics 根目录下的 output/
@@ -11,6 +10,7 @@
""" """
import numpy as np import numpy as np
import json
import os import os
import sys import sys
from vispy import app, scene from vispy import app, scene
@@ -30,89 +30,348 @@ else:
output_dir = compute.get_output_dir(script_dir) output_dir = compute.get_output_dir(script_dir)
os.environ["DYNAMICS_OUTPUT_DIR"] = output_dir os.environ["DYNAMICS_OUTPUT_DIR"] = output_dir
disp_path = os.path.join(output_dir, "display.txt") disp_path = os.path.join(output_dir, "display.txt")
npz_path = os.path.join(output_dir, "display.npz")
if not os.path.exists(disp_path): if not os.path.exists(npz_path) and not os.path.exists(disp_path):
raise FileNotFoundError( raise FileNotFoundError(
f"找不到 display.txt\n" f"找不到 display.npz 或 display.txt\n"
f"期望路径: {disp_path}\n" f"期望路径: {output_dir}\n"
f"请先运行 compute.py 计算轨迹,再运行 sample.py 生成显示数组。\n" f"请先运行 compute.py 计算轨迹,再运行 sample.py 生成显示数组。\n"
f"用法: python draw.py [案例输出目录]" f"用法: python draw.py [案例输出目录]"
) )
disp_data = compute.load_text_data(disp_path) # 优先读二进制 npz(加载速度约快 5-10x)
if os.path.exists(npz_path):
disp_data = compute.load_display_npz(npz_path)
else:
disp_data = compute.load_display_txt(disp_path)
# 单原子数据(plot_atom:用于信息显示) # ── 从 input.txt 读取参数(替代 display.npz 中的 meta)──
DISP_X = disp_data["disp_x"] try:
DISP_Y = disp_data["disp_y"] import yaml
DISP_Z = disp_data["disp_z"] _have_yaml = True
DISP_VX = disp_data["disp_vx"] except ImportError:
DISP_VY = disp_data["disp_vy"] _have_yaml = False
DISP_VZ = disp_data["disp_vz"]
# 全原子数据(用于多球绘制) input_dir = os.path.join(os.path.dirname(output_dir), "input")
DISP_ALL_X = disp_data["disp_all_x"] # (n_frames, n_atoms) input_path = os.path.join(input_dir, "input.txt")
DISP_ALL_Y = disp_data["disp_all_y"]
DISP_ALL_Z = disp_data["disp_all_z"]
DISP_ALL_VX = disp_data["disp_all_vx"]
DISP_ALL_VY = disp_data["disp_all_vy"]
DISP_ALL_VZ = disp_data["disp_all_vz"]
DISP_T = disp_data["disp_t"] if _have_yaml and os.path.exists(input_path):
DISP_STEP = disp_data["disp_step"] try:
N_FRAMES = int(disp_data["n_frames"]) with open(input_path, "r", encoding="utf-8") as f:
NT = int(disp_data["NT"]) config = yaml.safe_load(f)
DT = float(disp_data["DT"]) except Exception:
NSTEP = int(disp_data["NSTEP"]) 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)
DISP_ALL_Y = disp_data["frames_y"]
DISP_ALL_Z = disp_data["frames_z"]
DISP_ALL_VX = disp_data["frames_vx"]
DISP_ALL_VY = disp_data["frames_vy"]
DISP_ALL_VZ = disp_data["frames_vz"]
# 第一个原子的轨迹(用于信息显示)
DISP_X = DISP_ALL_X[:, 0]
DISP_Y = DISP_ALL_Y[:, 0]
DISP_Z = DISP_ALL_Z[:, 0]
DISP_VX = DISP_ALL_VX[:, 0]
DISP_VY = DISP_ALL_VY[:, 0]
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(config.get("DT", 0.001))
# 视觉位移放大: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 = 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]
# ── 位移颜色映射 ──────────────────────────────
# 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.get("atom_ids", np.array([1])) ATOM_IDS = disp_data["atom_ids"]
ATOM_RADII = disp_data.get("atom_radii", np.array([float(disp_data["ball_radius"])])) # 优先使用 per-atom 半径,否则用统一的 ball_radius
N_ATOMS = len(ATOM_IDS) _raw_radii = config.get("atom_radii", "")
PLOT_ATOM_ROW = int(disp_data.get("plot_atom_row", 0)) if _raw_radii.strip():
PLOT_ATOM_ID = int(disp_data.get("plot_atom_id", ATOM_IDS[0])) ATOM_RADII = np.array([float(x) for x in _raw_radii.split(",")])
BOND_PAIRS = disp_data.get("bond_pairs", []) else:
ATOM_RADII = np.full(N_ATOMS, float(config.get("ball_radius", 0.5)))
PLOT_ATOM_ROW = 0
PLOT_ATOM_ID = int(ATOM_IDS[0])
# 成键信息已在上面从 connection.txt 加载
# 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵) # 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵)
USE_MARKER = int(disp_data.get("use_marker", 0)) USE_MARKER = int(config.get("use_marker", 0))
if N_FRAMES <= 0: if N_FRAMES <= 0:
raise ValueError( raise ValueError(
"output/display.txt 中没有可播放的帧,请检查 sample_start/sample_end/NSTEP 配置。") "output/display.txt 中没有可播放的帧,请检查 sample_start/sample_end/NSTEP 配置。")
# 保留模拟边界常量(用于场景缩放、相机等),从 output/display.txt 中读取 # 模拟边界(从 input.txt 的 box_a 计算)
X_MIN = float(disp_data["X_MIN"]); X_MAX = float(disp_data["X_MAX"]) _box_a = float(config.get("box_a", 10.0))
Y_MIN = float(disp_data["Y_MIN"]); Y_MAX = float(disp_data["Y_MAX"]) X_MIN = -_box_a; X_MAX = _box_a
Z_MIN = float(disp_data["Z_MIN"]); Z_MAX = float(disp_data["Z_MAX"]) Y_MIN = -_box_a; Y_MAX = _box_a
X0 = float(disp_data["X0"]); Y0 = float(disp_data["Y0"]); Z0 = float(disp_data["Z0"]) Z_MIN = -_box_a; Z_MAX = _box_a
raw_alpha = disp_data["alpha"] raw_alpha = config.get("alpha", "0.2")
if isinstance(raw_alpha, (list, tuple, np.ndarray)): if isinstance(raw_alpha, (list, tuple)):
alpha_list = [float(a) for a in raw_alpha] alpha_list = [float(x) for x in raw_alpha]
if len(alpha_list) != 6:
raise ValueError(f"alpha 数组长度须为 6,实际为 {len(alpha_list)}")
else: else:
try:
alpha_list = [float(x) for x in raw_alpha.split(",")]
except (ValueError, AttributeError):
alpha_list = [float(raw_alpha)] * 6 alpha_list = [float(raw_alpha)] * 6
if len(alpha_list) != 6:
alpha_list = (alpha_list * 6)[:6]
# 绘图参数 # 绘图参数
ball_radius = float(disp_data["ball_radius"]) ball_radius = float(config.get("ball_radius", 0.5))
ball_color_r = float(disp_data["ball_color_r"]) ball_color_r = float(config.get("ball_color_r", 0.9))
ball_color_g = float(disp_data["ball_color_g"]) ball_color_g = float(config.get("ball_color_g", 0.2))
ball_color_b = float(disp_data["ball_color_b"]) ball_color_b = float(config.get("ball_color_b", 0.2))
box_color_r = float(disp_data["box_color_r"]) box_color_r = float(config.get("box_color_r", 0.8))
box_color_g = float(disp_data["box_color_g"]) box_color_g = float(config.get("box_color_g", 0.8))
box_color_b = float(disp_data["box_color_b"]) box_color_b = float(config.get("box_color_b", 0.85))
# =========================================================================== # ===========================================================================
# 图形界面无关的几何参数(不参与物理计算,仅用于场景外观) # 图形界面无关的几何参数(不参与物理计算,仅用于场景外观)
# =========================================================================== # ===========================================================================
info_margin = 36 info_margin = 8
axis_length = 10.0 axis_length = 10.0
import math as _math_cam
_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 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(config.get("camera_distance", 40.0))
_elev = float(config.get("camera_elevation", 0))
_azim = float(config.get("camera_azimuth", 0))
initial_camera = { initial_camera = {
"distance": 40.0, "distance": _dist,
"elevation": 0, "elevation": _elev,
"azimuth": 0, "azimuth": _azim,
"center": (0, 0, 0), "center": (_cx, _cy, _cz),
} }
@@ -167,11 +426,11 @@ axes_group.append(scene.visuals.Arrow(
parent=view.scene, 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)) 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)) 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)) pos=(0, 0, axis_length + 0.2), anchor_x="left", anchor_y="bottom", parent=view.scene))
# ── 原子渲染 ────────────────────────────────── # ── 原子渲染 ──────────────────────────────────
@@ -190,9 +449,12 @@ TAB10_RGB = np.array([
[0.7373, 0.7412, 0.1333], # 黄绿 [0.7373, 0.7412, 0.1333], # 黄绿
[0.0902, 0.7451, 0.8118], # 青 [0.0902, 0.7451, 0.8118], # 青
]) ])
# 每个原子的颜色(循环使用 tab10 色板) # 每个原子的颜色(循环使用 tab10 色板,或按位移着色
atom_colors = np.zeros((N_ATOMS, 4), dtype=np.float32) atom_colors = np.zeros((N_ATOMS, 4), dtype=np.float32)
for i in range(N_ATOMS): 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)] r, g, b = TAB10_RGB[i % len(TAB10_RGB)]
atom_colors[i] = [r, g, b, 1.0] atom_colors[i] = [r, g, b, 1.0]
@@ -250,12 +512,12 @@ for f_idx, (pos, direction) in enumerate(faces):
# 右上角:相机信息 # 右上角:相机信息
camera_info = scene.visuals.Text( 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) pos=(0, 0), anchor_x="right", anchor_y="top", parent=canvas.scene)
# 左上角:小球信息 # 左上角:小球信息
ball_info = scene.visuals.Text( ball_info = scene.visuals.Text(
text="", color=(0.2, 1.0, 0.2, 1.0), font_size=28, text="", color=(0.2, 1.0, 0.2, 1.0), font_size=14,
pos=(0, 0), anchor_x="left", anchor_y="top", pos=(0, 0), anchor_x="left", anchor_y="top",
face="黑体", bold=True, parent=canvas.scene) face="黑体", bold=True, parent=canvas.scene)
@@ -267,7 +529,7 @@ reset_button = scene.visuals.Rectangle(
radius=6, color=(0.18, 0.35, 0.65, 0.85), radius=6, color=(0.18, 0.35, 0.65, 0.85),
border_color="white", parent=canvas.scene) border_color="white", parent=canvas.scene)
reset_button_label = scene.visuals.Text( 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), pos=(reset_btn_size[0] / 2 + 8, reset_btn_size[1] / 2 + 8),
anchor_x="center", anchor_y="center", anchor_x="center", anchor_y="center",
bold=True, parent=canvas.scene) bold=True, parent=canvas.scene)
@@ -281,7 +543,7 @@ info_button = scene.visuals.Rectangle(
radius=6, color=(0.9, 0.3, 0.3, 0.9), radius=6, color=(0.9, 0.3, 0.3, 0.9),
border_color="white", parent=canvas.scene) border_color="white", parent=canvas.scene)
info_button_label = scene.visuals.Text( 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), pos=(info_btn_size[0] / 2 + 8, info_btn_size[1] / 2 + 8),
anchor_x="center", anchor_y="center", anchor_x="center", anchor_y="center",
bold=True, parent=canvas.scene) bold=True, parent=canvas.scene)
@@ -301,7 +563,7 @@ axes_button = scene.visuals.Rectangle(
radius=6, color=(0.3, 0.7, 0.3, 0.9), radius=6, color=(0.3, 0.7, 0.3, 0.9),
border_color="white", parent=canvas.scene) border_color="white", parent=canvas.scene)
axes_button_label = scene.visuals.Text( 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), pos=(axes_btn_size[0] / 2 + 8, axes_btn_size[1] / 2 + 8),
anchor_x="center", anchor_y="center", anchor_x="center", anchor_y="center",
bold=True, parent=canvas.scene) bold=True, parent=canvas.scene)
@@ -506,10 +768,14 @@ def handle_mouse_press(event):
# =========================================================================== # ===========================================================================
def _update_atom_positions(f_idx): def _update_atom_positions(f_idx):
"""更新所有原子到第 f_idx 帧的位置。""" """更新所有原子到第 f_idx 帧的位置,必要时更新颜色"""
if USE_MARKER: if USE_MARKER:
for i in range(N_ATOMS): marker_pos[:, 0] = DISP_ALL_X[f_idx]
marker_pos[i] = [DISP_ALL_X[f_idx, i], DISP_ALL_Y[f_idx, i], DISP_ALL_Z[f_idx, i]] marker_pos[:, 1] = DISP_ALL_Y[f_idx]
marker_pos[:, 2] = DISP_ALL_Z[f_idx]
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) balls.set_data(pos=marker_pos)
else: else:
for i in range(N_ATOMS): for i in range(N_ATOMS):
@@ -549,9 +815,80 @@ print(f"[draw] 渲染方式: {mode_str}")
print(f"[draw] 绘图参数: ball_radius={ball_radius}, box_color=({box_color_r:.2f},{box_color_g:.2f},{box_color_b:.2f}), alpha={alpha_list}") print(f"[draw] 绘图参数: ball_radius={ball_radius}, box_color=({box_color_r:.2f},{box_color_g:.2f},{box_color_b:.2f}), alpha={alpha_list}")
# =========================================================================== # 运动相机(速度段驱动):优先读取 move_camera.txt,其次用 display.txt header 缓存
# 每帧回调:仅推进帧索引,从预存数组读取位置,零物理计算 import re as _re
# ===========================================================================
def _load_move_camera_txt():
"""直接读取 input/move_camera.txt(与 output 同级的 input 目录)。"""
input_dir = os.path.join(os.path.dirname(output_dir), "input")
cam_path = os.path.join(input_dir, "move_camera.txt")
if not os.path.exists(cam_path):
return None
segs = []
with open(cam_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
# 解析帧范围:支持 "all"(全程)或 "N-M"(区间)
if line.lower().startswith("all") or _re.match(r'^\s*all\s', line, _re.IGNORECASE):
start, end = 0, 10**9 # 用极大值表示全程
else:
m = _re.match(r'(\d+)\s*-\s*(\d+)', line)
if not m:
continue
start, end = int(m.group(1)), int(m.group(2))
v, r = [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]
for i, axis in enumerate(['x', 'y', 'z']):
m2 = _re.search(r'v' + axis + r'\s*=\s*([-\d.]+)', line)
if m2: v[i] = float(m2.group(1))
m2 = _re.search(r'r' + axis + r'\s*=\s*([-\d.]+)', line)
if m2: r[i] = float(m2.group(1))
if any(v) or any(r):
segs.append({"start": start, "end": end, "v": v, "r": r})
return segs if segs else None
# 先试 move_camera.txt 直读,没有则用 display.txt 缓存
# header 中 camera_keyframes 为空字符串表示 move_camera=0(开关关闭),跳过文件加载
_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(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"]
_cam_azim = initial_camera["azimuth"]
_cam_dist = initial_camera["distance"]
src = "move_camera.txt" if _load_move_camera_txt() else "display.txt header"
print(f"[draw] 运动相机已启用(数据来源: {src}{len(_CAM_MOTION)} 段)")
def _update_motion_camera(f_idx):
"""速度段驱动:每帧累加平移/旋转。
时间交叠时所有段同时生效,按文件中出现的顺序依次作用。
只有当前帧存在活动段时才覆写相机,否则保留用户键盘/鼠标操作的结果。
"""
if not _CAM_MOTION:
return
global _cam_center, _cam_elev, _cam_azim, _cam_dist
active = False
for seg in _CAM_MOTION:
if seg["start"] <= f_idx < seg["end"]:
_cam_center[0] += seg["v"][0]
_cam_center[1] += seg["v"][1]
_cam_center[2] += seg["v"][2]
_cam_elev += seg["r"][0]
_cam_azim += seg["r"][1]
active = True
if active:
view.camera.center = tuple(_cam_center)
view.camera.distance = _cam_dist
view.camera.elevation = _cam_elev
view.camera.azimuth = _cam_azim
def update(event): def update(event):
global frame_idx global frame_idx
frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放 frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放
@@ -563,6 +900,9 @@ def update(event):
if bond_lines is not None and len(BOND_PAIRS) > 0: if bond_lines is not None and len(BOND_PAIRS) > 0:
_update_bond_positions(frame_idx) _update_bond_positions(frame_idx)
# 运动相机:速度段驱动
_update_motion_camera(frame_idx)
# 信息面板显示 plot_atom 的数据 # 信息面板显示 plot_atom 的数据
x = float(DISP_X[frame_idx]) x = float(DISP_X[frame_idx])
y = float(DISP_Y[frame_idx]) y = float(DISP_Y[frame_idx])
+162 -268
View File
@@ -14,6 +14,7 @@ import sys
import subprocess import subprocess
import time import time
import argparse import argparse
import json
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
@@ -25,6 +26,57 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import compute import compute
def _fmt_alpha(v):
"""将 alpha 值格式化为逗号分隔字符串,兼容 numpy 数组/list/标量。"""
if isinstance(v, (list, tuple, np.ndarray)):
return ",".join(str(float(x)) for x in v)
return str(float(v))
def _json_field(value):
"""Serialize arrays/lists for display header metadata."""
if isinstance(value, np.ndarray):
value = value.tolist()
return json.dumps(value, ensure_ascii=False)
def _load_camera_kf(config, runtime_base):
"""加载 move_camera.txt(速度段格式)→ JSON 字符串。"""
import re, json
if not int(config.get("move_camera", 0)):
return ""
cam_file = str(config.get("move_camera_file",
os.path.join("input", "move_camera.txt")))
cam_path = cam_file
if not os.path.isabs(cam_file):
cam_path = os.path.join(runtime_base, cam_file)
if not os.path.exists(cam_path):
return ""
segments = []
with open(cam_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
# 解析帧范围:支持 "all"(全程)或 "N-M"(区间)
if line.lower().startswith("all") or re.match(r'^\s*all\s', line, re.IGNORECASE):
start, end = 0, 10**9
else:
m = re.match(r'(\d+)\s*-\s*(\d+)', line)
if not m:
continue
start, end = int(m.group(1)), int(m.group(2))
v, r = [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]
for i, axis in enumerate(['x', 'y', 'z']):
m2 = re.search(r'v' + axis + r'\s*=\s*([-\d.]+)', line)
if m2: v[i] = float(m2.group(1))
m2 = re.search(r'r' + axis + r'\s*=\s*([-\d.]+)', line)
if m2: r[i] = float(m2.group(1))
if any(v) or any(r):
segments.append({"start": start, "end": end, "v": v, "r": r})
return json.dumps(segments) if segments else ""
def read_optional_index(data, key, default_value): def read_optional_index(data, key, default_value):
"""Read an optional integer index from structured txt metadata.""" """Read an optional integer index from structured txt metadata."""
if key not in data: if key not in data:
@@ -94,16 +146,6 @@ def build_sample_indices(total_steps, sample_step, sample_start, sample_end):
return indices return indices
def save_display_txt(data, out_dir=None):
"""将抽帧数据保存到 output/display.txt(含所有参数元数据)。"""
if out_dir is None:
out_dir = os.path.dirname(os.path.abspath(__file__))
disp_path = os.path.join(compute.get_output_dir(out_dir), "display.txt")
compute.save_text_data(disp_path, data)
print(f"[sample] 显示数组已保存至: {disp_path}")
return disp_path
def run_case(config_path, runtime_base, input_dir="input", output_dir="output", no_plot=False): def run_case(config_path, runtime_base, input_dir="input", output_dir="output", no_plot=False):
"""Run one case with explicit program path, input path, and output path.""" """Run one case with explicit program path, input path, and output path."""
runtime_base = Path(runtime_base).resolve() runtime_base = Path(runtime_base).resolve()
@@ -145,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") disp_path = os.path.join(output_dir_abs, "display.txt")
# ── 自动缓存检测 ─────────────────────────────────────── # ── 自动缓存检测 ───────────────────────────────────────
# force_calc=1: 强制重新计算,忽略缓存 # force_calc=1: 强制重新计算,忽略缓存(仅在 step_simulate=1 时生效)
# force_calc=0: 尊重 step_simulate 设置,不自动覆盖 # force_calc=0: 尊重 step_simulate 设置
force_calc = int(config.get("force_calc", 0)) 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,跳过缓存,强制重新计算") print(f"[run] force_calc=1,跳过缓存,强制重新计算")
config["step_simulate"] = 1
config["step_sample"] = 1 config["step_sample"] = 1
elif config.get("step_simulate", 1): elif config.get("step_simulate", 1):
# step_simulate=1 且 force_calc=0 → 按用户要求执行计算 # step_simulate=1 且 force_calc=0 → 按用户要求执行计算
@@ -183,8 +224,12 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
print(f"[run] 没有可用的缓存输出,但 step_simulate=0,将跳过模拟") print(f"[run] 没有可用的缓存输出,但 step_simulate=0,将跳过模拟")
# 2. 运行物理模拟 → output/trajectory.txt # 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): if config.get("step_simulate", 1):
engine = config.get("engine", "python")
total_steps = config["NT"] total_steps = config["NT"]
record_steps = total_steps - (config.get("warmup_steps") or 0) record_steps = total_steps - (config.get("warmup_steps") or 0)
print(f"[run] 开始计算 总步数={total_steps} 记录步数={record_steps} DT={config['DT']}") print(f"[run] 开始计算 总步数={total_steps} 记录步数={record_steps} DT={config['DT']}")
@@ -193,273 +238,110 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
_t0 = _time.time() _t0 = _time.time()
if engine == "python": if engine == "python":
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_from_config(config, str(runtime_base)) compute.run_from_config(config, str(runtime_base))
compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base))
else: else:
# 外部引擎:先加载配置到全局变量,再运行引擎,再用 save_trajectory_txt 补全 metadata # 外部引擎:先加载配置到全局变量,再运行引擎
config["_skip_run"] = True config["_skip_run"] = True
compute.run_from_config(config, str(runtime_base)) compute.run_from_config(config, str(runtime_base))
config.pop("_skip_run", None) config.pop("_skip_run", None)
input_dir_abs = str(input_dir_path.resolve()) input_dir_abs = str(input_dir_path.resolve())
output_dir_abs = str(output_dir_path.resolve()) output_dir_abs = str(output_dir_path.resolve())
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine(
engine, input_dir_abs, output_dir_abs, config) # ── DLL 路径(无文件 I/O,直接输出 display.npz)──
compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base)) 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 _elapsed = _time.time() - _t0
print(f"[run] 引擎: {engine} 计算完成: {record_steps}{_elapsed:.3f} s") print(f"[run] 引擎: {engine} 计算完成: {record_steps}{_elapsed:.3f} s")
else: else:
print("[run] 步骤 [模拟] 已跳过,直接加载已有轨迹") print("[run] 步骤 [模拟] 已跳过")
if not os.path.exists(traj_path):
print(f"[run] 错误: trajectory.txt 不存在,无法跳过模拟")
sys.exit(1)
# 3. 抽帧 → output/display.txt # 3. 检查/生成 display.txt
disp_path_new = os.path.join(output_dir_abs, "display.txt")
save_traj = int(config.get("save_trajectory", 0))
if os.path.exists(disp_path_new):
# Python 引擎或新版外部引擎(save_trajectory=0)已直接写入
print(f"[run] 发现已有 display.txt(引擎直接抽帧)")
elif engine != "python" and os.path.exists(os.path.join(output_dir_abs, "trajectory.txt")):
# 旧版外部引擎:从 trajectory.txt 抽帧
traj_path = os.path.join(output_dir_abs, "trajectory.txt") traj_path = os.path.join(output_dir_abs, "trajectory.txt")
if not os.path.exists(traj_path):
print(f"[run] 错误: 找不到 trajectory.txt 或 display.txt")
sys.exit(1)
data = compute.load_text_data(traj_path) data = compute.load_text_data(traj_path)
NT = int(data["NT"]); DT = float(data["DT"]); NSTEP = int(data.get("NSTEP", 1))
record_steps = NT - int(data.get("warmup_steps", 0))
n_atoms = len(data["atom_ids"])
sample_start = 0
sample_end = NT
indices = np.arange(0, record_steps, NSTEP, dtype=np.int64)
if len(indices) == 0:
indices = np.array([0])
NT = int(data["NT"]); DT = float(data["DT"]); NSTEP = int(data["NSTEP"]) traj_x = data["traj_x"]; traj_y = data["traj_y"]; traj_z = data["traj_z"]
warmup_steps = int(data.get("warmup_steps", 0)) traj_vx = data["traj_vx"]; traj_vy = data["traj_vy"]; traj_vz = data["traj_vz"]
plot_atom_row = int(data["plot_atom_row"]) if "plot_atom_row" in data else 0
plot_atom_id = int(data["plot_atom_id"]) if "plot_atom_id" in data else int(data["atom_ids"][plot_atom_row])
# 抽帧范围控制 # 构建 header_fields
sample_start = read_optional_index(data, "sample_start", 0) hf = {"DT": str(DT), "NSTEP": str(NSTEP), "method": str(data.get("method", "")),
sample_end = read_optional_index(data, "sample_end", NT) "warmup_steps": str(data.get("warmup_steps", 0)),
"dynamic_steps": str(record_steps),
"T_total": str(NT * DT),
"X_MAX": str(data.get("X_MAX", 10)), "X_MIN": str(data.get("X_MIN", -10)),
"Y_MAX": str(data.get("Y_MAX", 10)), "Y_MIN": str(data.get("Y_MIN", -10)),
"Z_MAX": str(data.get("Z_MAX", 10)), "Z_MIN": str(data.get("Z_MIN", -10)),
"ball_radius": str(data.get("ball_radius", 0.5)),
"ball_color_r": str(data.get("ball_color_r", 0.9)),
"ball_color_g": str(data.get("ball_color_g", 0.2)),
"ball_color_b": str(data.get("ball_color_b", 0.2)),
"box_color_r": str(data.get("box_color_r", 0.8)),
"box_color_g": str(data.get("box_color_g", 0.8)),
"box_color_b": str(data.get("box_color_b", 0.85)),
"gravity_field": str(data.get("gravity_field", 1)),
"gravity_interaction": str(data.get("gravity_interaction", 0)),
"elastic_force": str(data.get("elastic_force", 1)),
"damping_force": str(data.get("damping_force", 0)),
"gravity_strength": str(data.get("gravity_strength", 1.0)),
"driving_force": str(data.get("driving_force", 0)),
"use_marker": str(config.get("use_marker", 0)),
"alpha": _fmt_alpha(data.get("alpha", 0.2)),
"atom_masses": _json_field(data.get("atom_masses", [])),
"atom_positions": _json_field(data.get("atom_positions", [])),
"bond_pairs": _json_field(data.get("bond_pairs", [])),
"bond_stiffness": _json_field(data.get("bond_stiffness", [])),
"bond_rest_lengths": _json_field(data.get("bond_rest_lengths", [])),
"G": _json_field(data.get("G", [0.0, 0.0, 0.0])),
"atom_radii": _fmt_alpha(data.get("atom_radii", [])),
"camera_distance": str(config.get("camera_distance", 40.0)),
"camera_elevation": str(config.get("camera_elevation", 0)),
"camera_azimuth": str(config.get("camera_azimuth", 0)),
"camera_keyframes": _load_camera_kf(config, str(runtime_base))}
indices = build_sample_indices(NT, NSTEP, sample_start, sample_end)
n_frames = len(indices) n_frames = len(indices)
compute.save_display_txt(
disp_path_new,
traj_x[indices], traj_y[indices], traj_z[indices],
traj_vx[indices], traj_vy[indices], traj_vz[indices],
np.array(data["atom_ids"]), n_frames, n_atoms,
header_fields=hf)
print(f"[run] 从 trajectory.txt 抽帧生成 display.txt ({n_frames} 帧)")
print(f"[run] 抽帧范围: [{sample_start}, {sample_end}), 共 {n_frames}") # save_trajectory=0 时清理 trajectory.txt
if not save_traj:
traj_x = data["traj_x"] try:
traj_y = data["traj_y"] os.remove(traj_path)
traj_z = data["traj_z"] print(f"[run] save_trajectory=0,已删除 {traj_path}")
traj_vx = data["traj_vx"] except OSError:
traj_vy = data["traj_vy"] pass
traj_vz = data["traj_vz"]
if traj_x.ndim == 1:
selected_x = traj_x
selected_y = traj_y
selected_z = traj_z
selected_vx = traj_vx
selected_vy = traj_vy
selected_vz = traj_vz
all_x = traj_x[:, None]
all_y = traj_y[:, None]
all_z = traj_z[:, None]
all_vx = traj_vx[:, None]
all_vy = traj_vy[:, None]
all_vz = traj_vz[:, None]
else:
selected_x = traj_x[:, plot_atom_row]
selected_y = traj_y[:, plot_atom_row]
selected_z = traj_z[:, plot_atom_row]
selected_vx = traj_vx[:, plot_atom_row]
selected_vy = traj_vy[:, plot_atom_row]
selected_vz = traj_vz[:, plot_atom_row]
all_x = traj_x
all_y = traj_y
all_z = traj_z
all_vx = traj_vx
all_vy = traj_vy
all_vz = traj_vz
if config.get("step_sample", 1):
disp_data = {
"disp_x": selected_x[indices],
"disp_y": selected_y[indices],
"disp_z": selected_z[indices],
"disp_vx": selected_vx[indices],
"disp_vy": selected_vy[indices],
"disp_vz": selected_vz[indices],
"disp_all_x": all_x[indices],
"disp_all_y": all_y[indices],
"disp_all_z": all_z[indices],
"disp_all_vx": all_vx[indices],
"disp_all_vy": all_vy[indices],
"disp_all_vz": all_vz[indices],
"disp_t": indices * DT,
"disp_step": indices,
"n_frames": n_frames,
"NT": NT, "DT": DT, "NSTEP": NSTEP,
"plot_atom_id": plot_atom_id,
"plot_atom_row": plot_atom_row,
"method": str(data["method"]) if "method" in data else "explicit_euler",
"coord_file": str(data["coord_file"]) if "coord_file" in data else os.path.join("input", "coord.txt"),
"atom_ids": data["atom_ids"] if "atom_ids" in data else np.array([1]),
"atom_masses": data["atom_masses"] if "atom_masses" in data else np.array([float(data["M"])]),
"atom_radii": data["atom_radii"] if "atom_radii" in data else np.array([float(data["ball_radius"])]),
"atom_positions": data["atom_positions"] if "atom_positions" in data else np.array([[float(data["X0"]), float(data["Y0"]), float(data["Z0"])]]),
"atom_velocities": data["atom_velocities"] if "atom_velocities" in data else np.array([[float(data["VX0"]), float(data["VY0"]), float(data["VZ0"])]]),
"atom_fixed": data["atom_fixed"] if "atom_fixed" in data else np.array([[0, 0, 0]]),
"bond_pairs": data.get("bond_pairs", np.zeros((0, 2), dtype=np.int64)).tolist(),
"bond_stiffness": data.get("bond_stiffness", np.zeros(0, dtype=np.float64)).tolist(),
"bond_rest_lengths": data.get("bond_rest_lengths", np.zeros(0, dtype=np.float64)).tolist(),
"warmup_steps": warmup_steps,
"sample_start": sample_start,
"sample_end": sample_end,
"X_MIN": float(data["X_MIN"]), "X_MAX": float(data["X_MAX"]),
"Y_MIN": float(data["Y_MIN"]), "Y_MAX": float(data["Y_MAX"]),
"Z_MIN": float(data["Z_MIN"]), "Z_MAX": float(data["Z_MAX"]),
"X0": float(data["X0"]), "Y0": float(data["Y0"]), "Z0": float(data["Z0"]),
"VX0": float(data["VX0"]), "VY0": float(data["VY0"]), "VZ0": float(data["VZ0"]),
"M": float(data["M"]) if "M" in data else 1.0,
"alpha": data["alpha"],
"ball_radius": float(data["ball_radius"]),
"ball_color_r": float(data["ball_color_r"]),
"ball_color_g": float(data["ball_color_g"]),
"ball_color_b": float(data["ball_color_b"]),
"box_color_r": float(data["box_color_r"]),
"box_color_g": float(data["box_color_g"]),
"box_color_b": float(data["box_color_b"]),
"gravity_field": int(data.get("gravity_field", 1)),
"gravity_interaction": int(data.get("gravity_interaction", 0)),
"elastic_force": int(data.get("elastic_force", 1)),
"damping_force": int(data.get("damping_force", 0)),
"gravity_strength": float(data.get("gravity_strength", 1.0)),
"driving_force": int(config.get("driving_force", 0)),
"use_marker": int(config.get("use_marker", 0)),
}
save_display_txt(disp_data, str(runtime_base))
print(f"[run] 抽帧完成: {sample_end - sample_start} 步 -> {n_frames}")
else:
print("[run] 步骤 [抽帧] 已跳过")
# 4. 绘图(可选) # 4. 绘图(可选)
if not no_plot and config.get("step_plot", 1): if not no_plot and config.get("step_plot", 1):
try: print("[run] 注意: 旧版 step_plot 绘图路径依赖完整轨迹局部变量,当前已暂时跳过。")
import matplotlib.pyplot as plt print("[run] 如需波形/能量动画,请使用 step_plot_wave: 1。")
# 配置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
time_arr = np.arange(NT) * DT
n_atoms = all_x.shape[1]
atom_ids_list = data.get("atom_ids", np.arange(n_atoms) + 1)
fig, axes = plt.subplots(3, 3, figsize=(15, 13))
fig.suptitle("轨迹与能量分析", fontsize=16)
# ── 位置 / 速度 6 子图(前 2 行,每行 3 列) ──
plot_configs = [
(axes[0, 0], all_x, "x - 时间"),
(axes[0, 1], all_y, "y - 时间"),
(axes[0, 2], all_z, "z - 时间"),
(axes[1, 0], all_vx, "vx - 时间"),
(axes[1, 1], all_vy, "vy - 时间"),
(axes[1, 2], all_vz, "vz - 时间"),
]
colors = plt.cm.tab10(np.linspace(0, 1, n_atoms))
for ax, data_arr, title in plot_configs:
for i in range(n_atoms):
atom_id = int(atom_ids_list[i])
ax.plot(time_arr, data_arr[:, i], color=colors[i], linewidth=1.5, label=f"原子 {atom_id}")
ax.set_title(title)
ax.set_xlabel("时间 (s)")
ax.grid(True, alpha=0.3)
ax.legend()
# ── 能量计算 ─────────────────────────────────────
masses = np.array(data["atom_masses"]) # (n_atoms,)
G_vec = np.array(data.get("G", [0.0, 0.0, -9.8])) # [gx, gy, gz]
gravity_field_enabled = int(data.get("gravity_field", 1))
gravity_interaction_enabled = int(data.get("gravity_interaction", 0))
gravity_strength = float(data.get("gravity_strength", 1.0))
elastic_force_enabled = int(data.get("elastic_force", 1))
damping_force_enabled = int(data.get("damping_force", 0))
# 动能 Ek = ½ m v²
ek = 0.5 * masses[np.newaxis, :] * (all_vx**2 + all_vy**2 + all_vz**2)
# 均匀重力场势能 Ug = -m G·r
ug = np.zeros_like(ek)
if gravity_field_enabled:
ug = -masses[np.newaxis, :] * (
G_vec[0] * all_x + G_vec[1] * all_y + G_vec[2] * all_z
)
# 弹性势能 Us = ½ k (d - d₀)²
us = np.zeros_like(ek)
bond_pairs = data.get("bond_pairs")
bond_stiffness = data.get("bond_stiffness")
bond_rest_lengths = data.get("bond_rest_lengths")
if elastic_force_enabled and bond_pairs is not None and len(bond_pairs) > 0:
for b_idx in range(len(bond_pairs)):
i, j = bond_pairs[b_idx]
dx = all_x[:, j] - all_x[:, i]
dy = all_y[:, j] - all_y[:, i]
dz = all_z[:, j] - all_z[:, i]
dist = np.sqrt(dx**2 + dy**2 + dz**2)
stretch = dist - bond_rest_lengths[b_idx]
us_each = 0.5 * bond_stiffness[b_idx] * stretch**2
us[:, i] += us_each # 将整根键的势能记给 i
# 万有引力势能 Ug_grav = -G_grav * m_i * m_j / r
ug_grav = np.zeros_like(ek)
if gravity_interaction_enabled:
n_atoms_en = len(masses)
for i in range(n_atoms_en):
for j in range(i + 1, n_atoms_en):
dx = all_x[:, j] - all_x[:, i]
dy = all_y[:, j] - all_y[:, i]
dz = all_z[:, j] - all_z[:, i]
dist = np.sqrt(dx**2 + dy**2 + dz**2)
dist = np.maximum(dist, 1e-12)
pair_pe = -gravity_strength * masses[i] * masses[j] / dist
ug_grav[:, i] += 0.5 * pair_pe
ug_grav[:, j] += 0.5 * pair_pe
# 各原子总能量
e_total = ek + ug + us + ug_grav # (NT, n_atoms)
# 系统能量分量
ek_sys = np.sum(ek, axis=1)
ug_sys = np.sum(ug, axis=1)
us_sys = np.sum(us, axis=1)
ug_grav_sys = np.sum(ug_grav, axis=1)
e_sys = ek_sys + ug_sys + us_sys + ug_grav_sys
# ── 第 3 行左:各原子总能量 ──
ax_e = axes[2, 0]
for i in range(n_atoms):
aid = int(atom_ids_list[i])
ax_e.plot(time_arr, e_total[:, i], color=colors[i], linewidth=1.5, label=f"原子 {aid}")
ax_e.set_title("各原子总能量")
ax_e.set_xlabel("时间 (s)")
ax_e.set_ylabel("能量")
ax_e.grid(True, alpha=0.3)
ax_e.legend(loc="upper right")
# ── 第 3 行右:系统总能量 ──
ax_sys = axes[2, 1]
ax_sys.plot(time_arr, ek_sys, 'b-', linewidth=1.5, label="系统动能")
ax_sys.plot(time_arr, ug_sys, 'g-', linewidth=1.5, label="均匀重力势能")
if elastic_force_enabled and bond_pairs is not None and len(bond_pairs) > 0:
ax_sys.plot(time_arr, us_sys, color='orange', linewidth=1.5, label="系统弹性势能")
if gravity_interaction_enabled:
ax_sys.plot(time_arr, ug_grav_sys, color='purple', linewidth=1.5, label="万有引力势能")
ax_sys.plot(time_arr, e_sys, 'r--', linewidth=1.5, label="系统总能量")
ax_sys.set_title("系统总能量")
ax_sys.set_xlabel("时间 (s)")
ax_sys.set_ylabel("能量")
ax_sys.grid(True, alpha=0.3)
ax_sys.legend(loc="upper right")
# 隐藏第 3 行第 3 列空子图
axes[2, 2].set_visible(False)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plot_path = os.path.join(output_dir_abs, "trajectory_plots.png")
plt.savefig(plot_path, dpi=300, bbox_inches="tight")
print(f"[run] 轨迹图表已保存至: {plot_path}")
plt.show()
except ImportError:
print("[run] 警告: 未安装 matplotlib,跳过绘图")
print(f"[run] 完成!输出目录: {output_dir_abs}") print(f"[run] 完成!输出目录: {output_dir_abs}")
@@ -469,11 +351,13 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
if not os.path.exists(draw_script): if not os.path.exists(draw_script):
print(f"[run] 未找到动画脚本: {draw_script}") print(f"[run] 未找到动画脚本: {draw_script}")
else: else:
# 检查 display.txt 是否存在step_sample=0 时可能没有) # 检查 display.npz 或 display.txt 是否存在
disp_path = os.path.join(output_dir_abs, "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): if not os.path.exists(disp_path):
print(f"[run] 错误: 找不到 {disp_path}") print(f"[run] 错误: 找不到 display.npz 或 display.txt")
print(f"[run] 启动动画需要先运行抽帧step_sample: 1),或手动保留 output/display.txt") print(f"[run] 启动动画需要先运行模拟step_simulate: 1")
else: else:
try: try:
print("[run] 正在启动 VisPy 3D 动画窗口…") print("[run] 正在启动 VisPy 3D 动画窗口…")
@@ -505,14 +389,24 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
# 6. 波形能量动画(可选) # 6. 波形能量动画(可选)
if config.get("step_plot_wave", 0): if config.get("step_plot_wave", 0):
try: try:
_save_gif = int(config.get("plot_wave_save_gif", 0))
_save_mp4 = int(config.get("plot_wave_save_mp4", 0))
_to_file = bool(_save_gif or _save_mp4)
if _to_file:
import matplotlib
matplotlib.use("Agg") # 保存文件时用非交互式后端
import plot_wave as pw import plot_wave as pw
print("[run] 正在绘制波形与能量图…") print("[run] 正在绘制波形与能量图…")
pw.plot_wave( gif = pw.plot_wave(
str(output_dir_abs), str(output_dir_abs),
save_gif=int(config.get("plot_wave_save_gif", 0)), save_gif=_save_gif,
save_mp4=int(config.get("plot_wave_save_mp4", 0)), save_mp4=_save_mp4,
show=not _to_file, # 不保存文件时弹出交互窗口
) )
if gif:
print(f"[run] 波形 GIF 已保存: {gif}")
except Exception as e: except Exception as e:
import traceback; traceback.print_exc()
print(f"[run] 绘制波形图失败: {e}") print(f"[run] 绘制波形图失败: {e}")
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
-948
View File
@@ -1,948 +0,0 @@
/**
* engines/c/main.c
* -----------------
* C 语言动力学模拟引擎。
* 与 Python 版 (compute.py) 算法保持一致。
*
* 输入: param.json 数值参数(Python 从 YAML 转换得来)
* <input_dir>/coord.txt
* <input_dir>/connection.txt
* <input_dir>/bond.txt
* 输出: <output_dir>/trajectory.txt (JSON 格式,与 Python 版兼容)
*
* 编译: cmake --build build --target dynamics_c
* 用法: ./build/dynamics_c <input_dir> <output_dir> <param_json_path>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
/* ========================================================================
* 配置参数(从 param.json 读取)
* ======================================================================== */
typedef struct {
double box_a; /* 盒子半边长 */
int NT; /* 总步数 */
double DT; /* 时间步长 */
int NSTEP; /* 抽帧间隔 */
int warmup_steps; /* 预热步数 */
char method[32]; /* 算法名称 */
double G[3]; /* 重力分量 */
double B[3]; /* 阻尼分量 */
int gravity_field; /* 均匀重力场开关 */
int gravity_interaction; /* 原子间万有引力开关 */
int elastic_force; /* 弹簧键力开关 */
int damping_force; /* 阻尼开关 */
double gravity_strength; /* 万有引力强度 */
int driving_force; /* 驱动力开关 */
} SimParams;
/* ========================================================================
* 原子数据
* ======================================================================== */
typedef struct {
int n_atoms;
int *atom_ids;
double *masses;
double *radii;
double *pos_0; /* 初始位置 (n_atoms*3) */
double *vel_0; /* 初始速度 (n_atoms*3) */
int *fixed; /* 固定约束 (n_atoms*3) */
} AtomData;
/* ========================================================================
* 成键数据
* ======================================================================== */
typedef struct {
int n_bonds;
int *pairs; /* (n_bonds*2) */
double *stiffness;
double *rest_lengths;
} BondData;
/* 前向声明 */
static void *xmalloc(size_t sz);
/* ========================================================================
* 驱动力数据
* ======================================================================== */
typedef struct {
int n_drivers;
int *atom_idx;
double *amp_x, *amp_y, *amp_z;
double *freq_x, *freq_y, *freq_z;
double *phi_x, *phi_y, *phi_z; /* radians */
int *has_period; /* 0=all, 1=limited cycles */
double *period_cycles; /* number of cycles */
double *freeze_x, *freeze_y, *freeze_z;
} DriverData;
/* 读取 driver.txt */
static DriverData read_driver(const char *input_dir, const AtomData *atoms) {
DriverData d;
memset(&d, 0, sizeof(d));
char path[512];
snprintf(path, sizeof(path), "%s/driver.txt", input_dir);
FILE *f = fopen(path, "r");
if (!f) return d;
char line[1024];
if (!fgets(line, sizeof(line), f)) { fclose(f); return d; }
/* 第一遍:统计行数 */
int n_lines = 0;
while (fgets(line, sizeof(line), f)) {
char trimmed[1024];
int j = 0;
for (int i = 0; line[i]; i++) {
if (line[i] != ' ' && line[i] != '\t' && line[i] != '\n' && line[i] != '\r')
trimmed[j++] = line[i];
}
trimmed[j] = '\0';
if (strlen(trimmed) > 0 && trimmed[0] != '#') n_lines++;
}
if (n_lines == 0) { fclose(f); return d; }
/* 分配内存 */
d.n_drivers = n_lines;
d.atom_idx = (int*)xmalloc(n_lines * sizeof(int));
d.amp_x = (double*)xmalloc(n_lines * sizeof(double));
d.amp_y = (double*)xmalloc(n_lines * sizeof(double));
d.amp_z = (double*)xmalloc(n_lines * sizeof(double));
d.freq_x = (double*)xmalloc(n_lines * sizeof(double));
d.freq_y = (double*)xmalloc(n_lines * sizeof(double));
d.freq_z = (double*)xmalloc(n_lines * sizeof(double));
d.phi_x = (double*)xmalloc(n_lines * sizeof(double));
d.phi_y = (double*)xmalloc(n_lines * sizeof(double));
d.phi_z = (double*)xmalloc(n_lines * sizeof(double));
d.has_period = (int*)xmalloc(n_lines * sizeof(int));
d.period_cycles = (double*)xmalloc(n_lines * sizeof(double));
d.freeze_x = (double*)xmalloc(n_lines * sizeof(double));
d.freeze_y = (double*)xmalloc(n_lines * sizeof(double));
d.freeze_z = (double*)xmalloc(n_lines * sizeof(double));
/* 初始化 freeze 数组 */
for (int i = 0; i < n_lines; i++) {
d.freeze_x[i] = d.freeze_y[i] = d.freeze_z[i] = 0.0;
}
/* 第二遍:解析 */
rewind(f);
fgets(line, sizeof(line), f); /* 跳过表头 */
int idx = 0;
while (idx < n_lines && fgets(line, sizeof(line), f)) {
char trimmed[1024];
int j = 0;
for (int i = 0; line[i]; i++) {
if (line[i] != ' ' && line[i] != '\t' && line[i] != '\n' && line[i] != '\r')
trimmed[j++] = line[i];
}
trimmed[j] = '\0';
if (strlen(trimmed) == 0 || trimmed[0] == '#') continue;
int atom_id;
double amp_x, amp_y, amp_z;
double freq_x, freq_y, freq_z;
double phi_x, phi_y, phi_z;
char period_str[256] = {0};
int n_parsed = sscanf(line,
"%d %lf %lf %lf %lf %lf %lf %lf %lf %lf %255s",
&atom_id,
&amp_x, &amp_y, &amp_z,
&freq_x, &freq_y, &freq_z,
&phi_x, &phi_y, &phi_z,
period_str);
if (n_parsed < 11) continue;
/* 通过原子 ID 匹配内部索引(线性搜索)*/
int ii = -1;
for (int k = 0; k < atoms->n_atoms; k++) {
if (atoms->atom_ids[k] == atom_id) { ii = k; break; }
}
if (ii < 0) continue;
d.atom_idx[idx] = ii;
d.amp_x[idx] = amp_x;
d.amp_y[idx] = amp_y;
d.amp_z[idx] = amp_z;
d.freq_x[idx] = freq_x;
d.freq_y[idx] = freq_y;
d.freq_z[idx] = freq_z;
/* 角度 → 弧度 */
d.phi_x[idx] = phi_x * M_PI / 180.0;
d.phi_y[idx] = phi_y * M_PI / 180.0;
d.phi_z[idx] = phi_z * M_PI / 180.0;
if (strcmp(period_str, "all") == 0 || strcmp(period_str, "-1") == 0) {
d.has_period[idx] = 0;
d.period_cycles[idx] = -1.0;
} else {
d.has_period[idx] = 1;
d.period_cycles[idx] = strtod(period_str, NULL);
}
idx++;
}
d.n_drivers = idx;
fclose(f);
return d;
}
/* ========================================================================
* 轨迹缓冲区
* ======================================================================== */
typedef struct {
int n_steps;
int n_atoms;
double *x, *y, *z;
double *vx, *vy, *vz;
} Trajectory;
/* ========================================================================
* 辅助函数
* ======================================================================== */
static void die(const char *msg) {
fprintf(stderr, "[C-engine] 错误: %s\n", msg);
exit(1);
}
static void *xmalloc(size_t sz) {
void *p = malloc(sz);
if (!p) die("内存分配失败");
return p;
}
/* 从 JSON 中读取 double 值 */
static double json_read_double(const char *json, const char *key) {
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p) return 0.0;
p = strchr(p, ':');
if (!p) return 0.0;
p++;
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
return strtod(p, NULL);
}
static int json_read_int(const char *json, const char *key) {
return (int)json_read_double(json, key);
}
/* 从 JSON 中读取字符串值(写入 dst,最多 dst_sz 字节) */
static void json_read_string(const char *json, const char *key, char *dst, int dst_sz) {
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p) { dst[0] = '\0'; return; }
p = strchr(p, ':');
if (!p) { dst[0] = '\0'; return; }
p++;
while (*p == ' ' || *p == '\t' || *p == '\n') p++;
if (*p != '"') { dst[0] = '\0'; return; }
p++;
int i = 0;
while (*p && *p != '"' && i < dst_sz - 1) { dst[i++] = *p++; }
dst[i] = '\0';
}
/* 读取 JSON 数组 (如 "G": [0, 0, -9.8]) 到 double[3] */
static void json_read_double3(const char *json, const char *key, double out[3]) {
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p) { out[0]=out[1]=out[2]=0; return; }
p = strchr(p, '[');
if (!p) { out[0]=out[1]=out[2]=0; return; }
p++;
for (int i = 0; i < 3; i++) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == ',') p++;
out[i] = strtod(p, (char**)&p);
}
}
/* 读取 param.json */
static int g_gravity_field = 1;
static int g_gravity_interaction = 0;
static int g_elastic_force = 1;
static int g_damping_force = 0;
static double g_gravity_strength = 1.0;
static SimParams read_params(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) die("无法打开 param.json");
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = (char*)xmalloc(sz + 1);
fread(buf, 1, sz, f);
buf[sz] = '\0';
fclose(f);
SimParams p;
p.box_a = json_read_double(buf, "box_a");
p.NT = json_read_int(buf, "NT");
p.DT = json_read_double(buf, "DT");
p.NSTEP = json_read_int(buf, "NSTEP");
p.warmup_steps = json_read_int(buf, "warmup_steps");
strcpy(p.method, "leapfrog"); /* 默认 */
json_read_string(buf, "method", p.method, sizeof(p.method));
json_read_double3(buf, "G", p.G);
json_read_double3(buf, "B", p.B);
p.gravity_field = json_read_int(buf, "gravity_field");
p.gravity_interaction = json_read_int(buf, "gravity_interaction");
p.elastic_force = json_read_int(buf, "elastic_force");
p.damping_force = json_read_int(buf, "damping_force");
p.gravity_strength = json_read_double(buf, "gravity_strength");
p.driving_force = json_read_int(buf, "driving_force");
g_gravity_field = p.gravity_field;
g_gravity_interaction = p.gravity_interaction;
g_elastic_force = p.elastic_force;
g_damping_force = p.damping_force;
g_gravity_strength = p.gravity_strength;
free(buf);
return p;
}
/* 读取 coord.txt */
static AtomData read_coord(const char *input_dir) {
char path[512];
snprintf(path, sizeof(path), "%s/coord.txt", input_dir);
FILE *f = fopen(path, "r");
if (!f) die("无法打开 coord.txt");
/* 跳过第一行表头 */
char line[1024];
if (!fgets(line, sizeof(line), f)) die("coord.txt 为空");
int capacity = 16;
AtomData a;
a.n_atoms = 0;
a.atom_ids = (int*)xmalloc(capacity * sizeof(int));
a.masses = (double*)xmalloc(capacity * sizeof(double));
a.radii = (double*)xmalloc(capacity * sizeof(double));
a.pos_0 = (double*)xmalloc(capacity * 3 * sizeof(double));
a.vel_0 = (double*)xmalloc(capacity * 3 * sizeof(double));
a.fixed = (int*)xmalloc(capacity * 3 * sizeof(int));
while (fgets(line, sizeof(line), f)) {
if (a.n_atoms >= capacity) {
capacity *= 2;
a.atom_ids = realloc(a.atom_ids, capacity * sizeof(int));
a.masses = realloc(a.masses, capacity * sizeof(double));
a.radii = realloc(a.radii, capacity * sizeof(double));
a.pos_0 = realloc(a.pos_0, capacity * 3 * sizeof(double));
a.vel_0 = realloc(a.vel_0, capacity * 3 * sizeof(double));
a.fixed = realloc(a.fixed, capacity * 3 * sizeof(int));
}
int id, fx, fy, fz;
double mass, rad, px, py, pz, vx, vy, vz;
int n_parsed = sscanf(line, "%d %lf %lf %lf %lf %lf %lf %lf %lf %d %d %d",
&id, &mass, &rad, &px, &py, &pz, &vx, &vy, &vz, &fx, &fy, &fz);
if (n_parsed == 9) {
fx = fy = fz = 0;
} else if (n_parsed != 12) {
continue;
}
int i = a.n_atoms;
a.atom_ids[i] = id;
a.masses[i] = mass;
a.radii[i] = rad;
a.pos_0[i*3+0] = px; a.pos_0[i*3+1] = py; a.pos_0[i*3+2] = pz;
a.vel_0[i*3+0] = vx; a.vel_0[i*3+1] = vy; a.vel_0[i*3+2] = vz;
a.fixed[i*3+0] = fx; a.fixed[i*3+1] = fy; a.fixed[i*3+2] = fz;
a.n_atoms++;
}
fclose(f);
if (a.n_atoms <= 0) die("coord.txt 原子数无效");
return a;
}
/* 读取 connection.txt */
static BondData read_bonds(const char *input_dir, const AtomData *atoms) {
char path[512];
BondData b;
b.n_bonds = 0;
b.pairs = NULL;
b.stiffness = NULL;
b.rest_lengths = NULL;
snprintf(path, sizeof(path), "%s/connection.txt", input_dir);
FILE *f = fopen(path, "r");
if (!f) return b;
char line[256];
if (!fgets(line, sizeof(line), f)) { fclose(f); return b; }
int n_lines = 0, tmp_a, tmp_b;
char bond_name[256];
while (fscanf(f, "%d %d %s", &tmp_a, &tmp_b, bond_name) == 3) n_lines++;
rewind(f);
if (n_lines == 0) { fclose(f); return b; }
b.n_bonds = n_lines;
b.pairs = (int*)xmalloc(n_lines * 2 * sizeof(int));
b.stiffness = (double*)xmalloc(n_lines * sizeof(double));
b.rest_lengths = (double*)xmalloc(n_lines * sizeof(double));
char bond_path[512];
snprintf(bond_path, sizeof(bond_path), "%s/bond.txt", input_dir);
FILE *fb = fopen(bond_path, "r");
for (int i = 0; i < n_lines; i++) {
fscanf(f, "%d %d %s", &tmp_a, &tmp_b, bond_name);
b.pairs[i*2+0] = tmp_a - 1;
b.pairs[i*2+1] = tmp_b - 1;
b.stiffness[i] = 1.0;
b.rest_lengths[i] = 2.0;
if (fb) {
char name[256], header[256];
double k, r0;
rewind(fb);
fgets(header, sizeof(header), fb); // 跳过表头行
while (fscanf(fb, "%s %lf %lf", name, &k, &r0) == 3) {
if (strcmp(name, bond_name) == 0) {
b.stiffness[i] = k;
b.rest_lengths[i] = r0;
break;
}
}
}
}
fclose(f);
if (fb) fclose(fb);
return b;
}
/* ========================================================================
* 物理核心(与 Python compute.py 对应)
* ======================================================================== */
/* 加速度计算(各力独立开关控制) */
static void compute_acceleration(
int n, const double *x, const double *y, const double *z,
const double *vx, const double *vy, const double *vz,
const double *m, const double G[3], const double B[3],
const BondData *bonds,
double *ax, double *ay, double *az)
{
/* 先清零 */
for (int i = 0; i < n; i++) {
ax[i] = 0.0; ay[i] = 0.0; az[i] = 0.0;
}
/* 均匀重力场 */
if (g_gravity_field) {
for (int i = 0; i < n; i++) {
ax[i] += G[0];
ay[i] += G[1];
az[i] += G[2];
}
}
/* 阻尼 */
if (g_damping_force) {
for (int i = 0; i < n; i++) {
ax[i] -= B[0] * vx[i] / m[i];
ay[i] -= B[1] * vy[i] / m[i];
az[i] -= B[2] * vz[i] / m[i];
}
}
/* 弹簧键力 */
if (g_elastic_force) {
for (int b = 0; b < bonds->n_bonds; b++) {
int i = bonds->pairs[b*2+0];
int j = bonds->pairs[b*2+1];
double dx = x[j] - x[i];
double dy = y[j] - y[i];
double dz = z[j] - z[i];
double dist = sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 1e-12) continue;
double stretch = dist - bonds->rest_lengths[b];
double fmag = bonds->stiffness[b] * stretch;
double ux = dx / dist, uy = dy / dist, uz = dz / dist;
double fx = fmag * ux, fy = fmag * uy, fz = fmag * uz;
ax[i] += fx / m[i]; ay[i] += fy / m[i]; az[i] += fz / m[i];
ax[j] -= fx / m[j]; ay[j] -= fy / m[j]; az[j] -= fz / m[j];
}
}
/* 万有引力(所有原子对之间) */
if (g_gravity_interaction) {
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
double dx = x[j] - x[i];
double dy = y[j] - y[i];
double dz = z[j] - z[i];
double r2 = dx*dx + dy*dy + dz*dz;
if (r2 <= 1e-12) continue;
double r = sqrt(r2);
double f_mag = g_gravity_strength * m[i] * m[j] / r2;
double fx_g = f_mag * dx / r;
double fy_g = f_mag * dy / r;
double fz_g = f_mag * dz / r;
ax[i] += fx_g / m[i]; ay[i] += fy_g / m[i]; az[i] += fz_g / m[i];
ax[j] -= fx_g / m[j]; ay[j] -= fy_g / m[j]; az[j] -= fz_g / m[j];
}
}
}
}
/* 边界条件:clamp 位置 + 速度反转 ——与 Python Limit_in_box 一致 */
static void limit_in_box(double *pos, double *vel, double lo, double hi) {
if (*pos > hi) { *pos = hi; *vel = -*vel; }
if (*pos < lo) { *pos = lo; *vel = -*vel; }
}
/* ── 显式欧拉法 ──────────── */
static void explicit_euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData *bonds, const int *fixed, double dt)
{
double *ax = (double*)alloca(n * sizeof(double));
double *ay = (double*)alloca(n * sizeof(double));
double *az = (double*)alloca(n * sizeof(double));
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && 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;
}
}
/* ── 隐式欧拉法 ──────────── */
static void implicit_euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData *bonds, const int *fixed, double dt)
{
double *vxn = (double*)alloca(n * sizeof(double));
double *vyn = (double*)alloca(n * sizeof(double));
double *vzn = (double*)alloca(n * sizeof(double));
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) {
vxn[i] = 0; vyn[i] = 0; vzn[i] = 0; continue;
}
double gx = B[0] / m[i], gy = B[1] / m[i], gz = B[2] / m[i];
vxn[i] = (vx[i] + G[0] * dt) / (1.0 + gx * dt);
vyn[i] = (vy[i] + G[1] * dt) / (1.0 + gy * dt);
vzn[i] = (vz[i] + G[2] * dt) / (1.0 + gz * dt);
}
double *ax = (double*)alloca(n * sizeof(double));
double *ay = (double*)alloca(n * sizeof(double));
double *az = (double*)alloca(n * sizeof(double));
compute_acceleration(n, x, y, z, vxn, vyn, vzn, m, G, B, bonds, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && 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;
}
}
/* ── 中点法 ──────────── */
static void midpoint_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData *bonds, const int *fixed, double dt)
{
double *ax = (double*)alloca(n * sizeof(double));
double *ay = (double*)alloca(n * sizeof(double));
double *az = (double*)alloca(n * sizeof(double));
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds, ax, ay, az);
double *xm = (double*)alloca(n * sizeof(double));
double *ym = (double*)alloca(n * sizeof(double));
double *zm = (double*)alloca(n * sizeof(double));
double *vxm = (double*)alloca(n * sizeof(double));
double *vym = (double*)alloca(n * sizeof(double));
double *vzm = (double*)alloca(n * sizeof(double));
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) {
xm[i]=ym[i]=zm[i]=vxm[i]=vym[i]=vzm[i]=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;
}
compute_acceleration(n, xm, ym, zm, vxm, vym, vzm, m, G, B, bonds, ax, ay, az);
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt;
vy[i] += ay[i] * dt;
vz[i] += az[i] * dt;
}
}
/* ── 蛙跳法(Velocity-Verlet)── */
static void leapfrog_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData *bonds, const int *fixed, double dt)
{
double *ax = (double*)alloca(n * sizeof(double));
double *ay = (double*)alloca(n * sizeof(double));
double *az = (double*)alloca(n * sizeof(double));
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds, ax, ay, az);
/* 半推速度:v_half = v + 0.5*a*dt */
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt * 0.5;
vy[i] += ay[i] * dt * 0.5;
vz[i] += az[i] * dt * 0.5;
}
/* 全推位置(不含边界)*/
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
x[i] += vx[i] * dt; /* vx 此时是 v_half */
y[i] += vy[i] * dt;
z[i] += vz[i] * dt;
}
/* 显式预测器:v_pred = v_half + 0.5*a_old*dt,用第一次加速度外推半步
包含重力+阻尼+弹簧的所有贡献(标准 Velocity-Verlet 预测步)*/
double *pred_vx = (double*)alloca(n * sizeof(double));
double *pred_vy = (double*)alloca(n * sizeof(double));
double *pred_vz = (double*)alloca(n * sizeof(double));
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
pred_vx[i] = vx[i] + 0.5 * ax[i] * dt;
pred_vy[i] = vy[i] + 0.5 * ay[i] * dt;
pred_vz[i] = vz[i] + 0.5 * az[i] * dt;
}
/* 用新位置 + 预测速度重算加速度 */
compute_acceleration(n, x, y, z, pred_vx, pred_vy, pred_vz, m, G, B, bonds, ax, ay, az);
/* 速度后半步:v = v_half + 0.5*a_next*dt
vx 仍为 v_half(未被覆盖)*/
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt * 0.5;
vy[i] += ay[i] * dt * 0.5;
vz[i] += az[i] * dt * 0.5;
}
}
/* ── 驱动力(与 Python apply_driving_force 一致)──────────────── */
static void apply_driving_force(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
double t, int step, double dt,
const DriverData *drivers)
{
if (!drivers || drivers->n_drivers == 0) return;
for (int d = 0; d < drivers->n_drivers; d++) {
int idx = drivers->atom_idx[d];
/* 检查周期限制 */
if (drivers->has_period[d]) {
double max_freq = fmax(fabs(drivers->freq_x[d]),
fmax(fabs(drivers->freq_y[d]), fabs(drivers->freq_z[d])));
int period_steps = 0;
if (max_freq > 1e-12) {
period_steps = (int)(drivers->period_cycles[d] / max_freq / dt);
}
if (step > period_steps) {
/* 冻结 */
if (drivers->freeze_x) {
x[idx] = drivers->freeze_x[d];
y[idx] = drivers->freeze_y[d];
z[idx] = drivers->freeze_z[d];
}
vx[idx] = vy[idx] = vz[idx] = 0.0;
continue;
}
}
double px = drivers->amp_x[d] * cos(2*M_PI*drivers->freq_x[d]*t + drivers->phi_x[d]);
double py = drivers->amp_y[d] * cos(2*M_PI*drivers->freq_y[d]*t + drivers->phi_y[d]);
double pz = drivers->amp_z[d] * cos(2*M_PI*drivers->freq_z[d]*t + drivers->phi_z[d]);
double vpx = -drivers->amp_x[d]*2*M_PI*drivers->freq_x[d]*sin(2*M_PI*drivers->freq_x[d]*t + drivers->phi_x[d]);
double vpy = -drivers->amp_y[d]*2*M_PI*drivers->freq_y[d]*sin(2*M_PI*drivers->freq_y[d]*t + drivers->phi_y[d]);
double vpz = -drivers->amp_z[d]*2*M_PI*drivers->freq_z[d]*sin(2*M_PI*drivers->freq_z[d]*t + drivers->phi_z[d]);
x[idx] = px; y[idx] = py; z[idx] = pz;
vx[idx] = vpx; vy[idx] = vpy; vz[idx] = vpz;
/* 记录冻结位置(周期结束时) */
if (drivers->has_period[d]) {
double max_freq = fmax(fabs(drivers->freq_x[d]),
fmax(fabs(drivers->freq_y[d]), fabs(drivers->freq_z[d])));
int period_steps = 0;
if (max_freq > 1e-12) {
period_steps = (int)(drivers->period_cycles[d] / max_freq / dt);
}
if (step == period_steps) {
drivers->freeze_x[d] = px;
drivers->freeze_y[d] = py;
drivers->freeze_z[d] = pz;
}
}
}
}
/* ── 分发器:调用对应积分方法 + 边界条件 + 自由度约束(与 Python 一致)── */
static void apply_step(
const char *method,
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData *bonds, const int *fixed,
const double *pos_0,
double box_a, double dt)
{
if (strcmp(method, "explicit_euler") == 0) {
explicit_euler_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt);
} else if (strcmp(method, "implicit_euler") == 0) {
implicit_euler_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt);
} else if (strcmp(method, "midpoint") == 0) {
midpoint_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt);
} else if (strcmp(method, "leapfrog") == 0) {
leapfrog_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt);
} else {
fprintf(stderr, "[C-engine] 未知算法: %s\n", method);
exit(1);
}
/* 边界条件(与 Python Limit_in_box 一致) */
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
limit_in_box(&x[i], &vx[i], -box_a, box_a);
limit_in_box(&y[i], &vy[i], -box_a, box_a);
limit_in_box(&z[i], &vz[i], -box_a, box_a);
}
/* 逐自由度固定约束(与 Python apply_fixed_constraints 一致) */
for (int i = 0; i < n; i++) {
if (fixed[i*3+0]) { x[i] = pos_0[i*3+0]; vx[i] = 0.0; }
if (fixed[i*3+1]) { y[i] = pos_0[i*3+1]; vy[i] = 0.0; }
if (fixed[i*3+2]) { z[i] = pos_0[i*3+2]; vz[i] = 0.0; }
}
}
// ========================================================================
// JSON 输出
// ========================================================================
static void write_trajectory_json(const char *path, const Trajectory *traj,
const SimParams *params, const AtomData *atoms,
const BondData *bonds)
{
FILE *f = fopen(path, "w");
if (!f) die("无法写入 trajectory.txt");
fprintf(f, "{\n");
const char *names[] = {"traj_x","traj_y","traj_z","traj_vx","traj_vy","traj_vz"};
double *arrs[] = {traj->x, traj->y, traj->z, traj->vx, traj->vy, traj->vz};
for (int a = 0; a < 6; a++) {
fprintf(f, " \"%s\": [\n", names[a]);
for (int t = 0; t < traj->n_steps; t++) {
fprintf(f, " [");
for (int i = 0; i < traj->n_atoms; i++) {
fprintf(f, "%.15g", arrs[a][t * traj->n_atoms + i]);
if (i < traj->n_atoms - 1) fputc(',', f);
}
fprintf(f, "]");
if (t < traj->n_steps - 1) fputc(',', f);
fputc('\n', f);
}
fprintf(f, " ]");
fputc(',', f);
fputc('\n', f);
}
/* 标量参数 */
fprintf(f, " \"NT\": %d,\n", params->NT);
fprintf(f, " \"DT\": %.15g,\n", params->DT);
fprintf(f, " \"NSTEP\": %d,\n", params->NSTEP);
fprintf(f, " \"method\": \"%s\",\n", params->method);
fprintf(f, " \"warmup_steps\": %d,\n", params->warmup_steps);
fprintf(f, " \"G\": [%.15g, %.15g, %.15g],\n", params->G[0], params->G[1], params->G[2]);
fprintf(f, " \"B\": [%.15g, %.15g, %.15g],\n", params->B[0], params->B[1], params->B[2]);
fprintf(f, " \"atom_ids\": [");
for (int i = 0; i < atoms->n_atoms; i++) {
if (i > 0) fputc(',', f);
fprintf(f, "%d", atoms->atom_ids[i]);
}
fprintf(f, "],\n");
fprintf(f, " \"atom_masses\": [");
for (int i = 0; i < atoms->n_atoms; i++) {
if (i > 0) fputc(',', f);
fprintf(f, "%.15g", atoms->masses[i]);
}
fprintf(f, "],\n");
fprintf(f, " \"bond_pairs\": [");
for (int b = 0; b < bonds->n_bonds; b++) {
if (b > 0) fputc(',', f);
fprintf(f, "[%d, %d]", bonds->pairs[b*2], bonds->pairs[b*2+1]);
}
fprintf(f, "],\n");
fprintf(f, " \"bond_stiffness\": [");
for (int b = 0; b < bonds->n_bonds; b++) {
if (b > 0) fputc(',', f);
fprintf(f, "%.15g", bonds->stiffness[b]);
}
fprintf(f, "],\n");
fprintf(f, " \"bond_rest_lengths\": [");
for (int b = 0; b < bonds->n_bonds; b++) {
if (b > 0) fputc(',', f);
fprintf(f, "%.15g", bonds->rest_lengths[b]);
}
fprintf(f, "],\n");
fprintf(f, " \"driving_force\": %d\n", params->driving_force);
fprintf(f, "}\n");
fclose(f);
}
// ========================================================================
// 主函数
// ========================================================================
int main(int argc, char **argv) {
if (argc < 4) {
fprintf(stderr, "用法: %s <input_dir> <output_dir> <param_json>\n", argv[0]);
return 1;
}
const char *input_dir = argv[1];
const char *output_dir = argv[2];
const char *param_path = argv[3];
clock_t t0 = clock();
SimParams params = read_params(param_path);
AtomData atoms = read_coord(input_dir);
BondData bonds = read_bonds(input_dir, &atoms);
DriverData drivers;
drivers.n_drivers = 0;
if (params.driving_force) {
drivers = read_driver(input_dir, &atoms);
}
printf("[C-engine] 原子数=%d, 键数=%d, 驱动=%d, NT=%d, DT=%.6g, method=%s\n",
atoms.n_atoms, bonds.n_bonds, drivers.n_drivers, params.NT, params.DT, params.method);
int n = atoms.n_atoms;
double *x = (double*)xmalloc(n * sizeof(double));
double *y = (double*)xmalloc(n * sizeof(double));
double *z = (double*)xmalloc(n * sizeof(double));
double *vx = (double*)xmalloc(n * sizeof(double));
double *vy = (double*)xmalloc(n * sizeof(double));
double *vz = (double*)xmalloc(n * sizeof(double));
for (int i = 0; i < n; i++) {
x[i] = atoms.pos_0[i*3+0];
y[i] = atoms.pos_0[i*3+1];
z[i] = atoms.pos_0[i*3+2];
vx[i] = atoms.vel_0[i*3+0];
vy[i] = atoms.vel_0[i*3+1];
vz[i] = atoms.vel_0[i*3+2];
}
/* 分配轨迹缓冲区:改用 record_steps */
int record_steps = params.NT - params.warmup_steps;
Trajectory traj;
traj.n_steps = record_steps;
traj.n_atoms = n;
traj.x = (double*)xmalloc(record_steps * n * sizeof(double) * 6);
traj.y = traj.x + record_steps * n;
traj.z = traj.y + record_steps * n;
traj.vx = traj.z + record_steps * n;
traj.vy = traj.vx + record_steps * n;
traj.vz = traj.vy + record_steps * n;
/* 预热 */
for (int s = 0; s < params.warmup_steps; s++) {
double tw = (s + 1) * params.DT;
if (params.driving_force) apply_driving_force(n, x, y, z, vx, vy, vz, tw, s, params.DT, &drivers);
apply_step(params.method, n, x, y, z, vx, vy, vz,
atoms.masses, params.G, params.B, &bonds, atoms.fixed,
atoms.pos_0,
params.box_a, params.DT);
}
/* 记录 */
for (int s = 0; s < record_steps; s++) {
double t = (s + params.warmup_steps) * params.DT;
if (params.driving_force) apply_driving_force(n, x, y, z, vx, vy, vz, t, s, params.DT, &drivers);
for (int i = 0; i < n; i++) {
traj.x[ s * n + i] = x[i];
traj.y[ s * n + i] = y[i];
traj.z[ s * n + i] = z[i];
traj.vx[s * n + i] = vx[i];
traj.vy[s * n + i] = vy[i];
traj.vz[s * n + i] = vz[i];
}
apply_step(params.method, n, x, y, z, vx, vy, vz,
atoms.masses, params.G, params.B, &bonds, atoms.fixed,
atoms.pos_0,
params.box_a, params.DT);
}
char out_path[512];
snprintf(out_path, sizeof(out_path), "%s/trajectory.txt", output_dir);
write_trajectory_json(out_path, &traj, &params, &atoms, &bonds);
clock_t t1 = clock();
double elapsed = (double)(t1 - t0) / CLOCKS_PER_SEC;
printf("[C-engine] 计算完成: %d 步, %.3f s\n", record_steps, elapsed);
/* 清理 */
free(traj.x);
free(x); free(y); free(z);
free(vx); free(vy); free(vz);
free(atoms.atom_ids);
free(atoms.masses); free(atoms.radii);
free(atoms.pos_0); free(atoms.vel_0);
free(atoms.fixed);
if (bonds.pairs) { free(bonds.pairs); free(bonds.stiffness); free(bonds.rest_lengths); }
return 0;
}
-863
View File
@@ -1,863 +0,0 @@
/**
* engines/cpp/main.cpp
* --------------------
* C++ 动力学模拟引擎。
* 与 Python 版 (compute.py) 算法保持一致。
*
* 输入: param.json, <input_dir>/coord.txt, connection.txt, bond.txt
* 输出: <output_dir>/trajectory.txt (JSON, 与 Python 版兼容)
*
* 编译:
* g++ -O3 -march=native -std=c++17 -o build/dynamics_cpp main.cpp
*
* 用法:
* ./build/dynamics_cpp <input_dir> <output_dir> <param_json>
*/
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
// ========================================================================
// 配置参数(从 param.json 读取)
// ========================================================================
struct SimParams {
double box_a = 10.0;
int NT = 10000;
double DT = 0.001;
int NSTEP = 100;
int warmup_steps = 0;
std::string method = "leapfrog";
double G[3] = {0, 0, -9.8};
double B[3] = {0, 0, 0};
int gravity_field = 1;
int gravity_interaction = 0;
int elastic_force = 1;
int damping_force = 0;
double gravity_strength = 1.0;
int driving_force = 0;
};
// ========================================================================
// 原子数据
// ========================================================================
struct AtomData {
std::vector<int> ids;
std::vector<double> masses;
std::vector<double> radii;
std::vector<double> pos_0; // (n_atoms * 3)
std::vector<double> vel_0; // (n_atoms * 3)
std::vector<int> fixed; // (n_atoms * 3), 0/1 flags
};
// ========================================================================
// 成键数据
// ========================================================================
struct BondData {
std::vector<int> pairs; // (n_bonds * 2)
std::vector<double> stiffness;
std::vector<double> rest_lengths;
};
// ========================================================================
// 驱动力数据
// ========================================================================
struct DriverData {
int n_drivers = 0;
std::vector<int> atom_idx; // internal atom indices
std::vector<double> amp_x, amp_y, amp_z;
std::vector<double> freq_x, freq_y, freq_z;
std::vector<double> phi_x, phi_y, phi_z; // radians
std::vector<int> has_period; // 0=all, 1=limited
std::vector<double> period_cycles;
std::vector<double> freeze_x, freeze_y, freeze_z;
};
// ========================================================================
// 辅助函数
// ========================================================================
static void die(const std::string &msg) {
std::cerr << "[C++-engine] 错误: " << msg << std::endl;
exit(1);
}
/* 读整个文件为字符串 */
static std::string read_file(const std::string &path) {
std::ifstream f(path, std::ios::binary);
if (!f) die("无法打开 " + path);
std::ostringstream ss;
ss << f.rdbuf();
return ss.str();
}
/* 从 JSON 中查找 key,返回冒号后的数值 */
static double json_read_double(const std::string &json, const std::string &key) {
auto pos = json.find("\"" + key + "\"");
if (pos == std::string::npos) return 0.0;
pos = json.find(':', pos);
if (pos == std::string::npos) return 0.0;
while (pos < json.size() && (json[pos] == ':' || json[pos] == ' ' || json[pos] == '\t' || json[pos] == '\n')) pos++;
return std::stod(json.substr(pos));
}
static int json_read_int(const std::string &json, const std::string &key) {
return static_cast<int>(json_read_double(json, key));
}
/* 从 JSON 中读取字符串值 */
static std::string json_read_string(const std::string &json, const std::string &key) {
auto pos = json.find("\"" + key + "\"");
if (pos == std::string::npos) return "";
pos = json.find(':', pos);
if (pos == std::string::npos) return "";
while (pos < json.size() && (json[pos] == ':' || json[pos] == ' ' || json[pos] == '\t' || json[pos] == '\n')) pos++;
if (pos >= json.size()) return "";
// 找到引号
if (json[pos] != '"') return "";
pos++;
std::string result;
while (pos < json.size() && json[pos] != '"') {
result += json[pos];
pos++;
}
return result;
}
/* 读取 JSON 数组 (如 "G": [0, 0, -9.8]) 到 double[3] */
static void json_read_double3(const std::string &json, const std::string &key, double out[3]) {
auto pos = json.find("\"" + key + "\"");
if (pos == std::string::npos) { out[0] = out[1] = out[2] = 0; return; }
pos = json.find('[', pos);
if (pos == std::string::npos) { out[0] = out[1] = out[2] = 0; return; }
pos++;
for (int i = 0; i < 3; i++) {
while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || json[pos] == '\n' || json[pos] == ',')) pos++;
char *end;
out[i] = std::strtod(json.c_str() + pos, &end);
pos = end - json.c_str();
}
}
/* 解析 param.json */
static SimParams read_params(const std::string &path) {
std::string buf = read_file(path);
SimParams p;
p.box_a = json_read_double(buf, "box_a");
p.NT = json_read_int(buf, "NT");
p.DT = json_read_double(buf, "DT");
p.NSTEP = json_read_int(buf, "NSTEP");
p.warmup_steps = json_read_int(buf, "warmup_steps");
std::string m = json_read_string(buf, "method");
if (!m.empty()) p.method = m;
json_read_double3(buf, "G", p.G);
json_read_double3(buf, "B", p.B);
p.gravity_field = json_read_int(buf, "gravity_field");
p.gravity_interaction = json_read_int(buf, "gravity_interaction");
p.elastic_force = json_read_int(buf, "elastic_force");
p.damping_force = json_read_int(buf, "damping_force");
p.gravity_strength = json_read_double(buf, "gravity_strength");
p.driving_force = json_read_int(buf, "driving_force");
return p;
}
/* 读取 coord.txt */
static AtomData read_coord(const std::string &input_dir) {
std::string path = input_dir + "/coord.txt";
std::ifstream f(path);
if (!f) die("无法打开 " + path);
std::string header;
std::getline(f, header); // 跳过表头
AtomData a;
int id, fx, fy, fz;
double mass, rad, px, py, pz, vx, vy, vz;
std::string line;
while (std::getline(f, line)) {
if (line.empty() || line[0] == '#') continue;
int n_parsed = std::sscanf(line.c_str(), "%d %lf %lf %lf %lf %lf %lf %lf %lf %d %d %d",
&id, &mass, &rad, &px, &py, &pz, &vx, &vy, &vz, &fx, &fy, &fz);
if (n_parsed == 9) {
fx = fy = fz = 0;
} else if (n_parsed != 12) {
continue;
}
a.ids.push_back(id);
a.masses.push_back(mass);
a.radii.push_back(rad);
a.pos_0.push_back(px); a.pos_0.push_back(py); a.pos_0.push_back(pz);
a.vel_0.push_back(vx); a.vel_0.push_back(vy); a.vel_0.push_back(vz);
a.fixed.push_back(fx); a.fixed.push_back(fy); a.fixed.push_back(fz);
}
if (a.ids.empty()) die("coord.txt 中没有原子数据");
return a;
}
/* 读取 connection.txt 和 bond.txt */
static BondData read_bonds(const std::string &input_dir) {
BondData b;
std::string conn_path = input_dir + "/connection.txt";
std::ifstream f(conn_path);
if (!f) return b; // 无成键
std::string header;
std::getline(f, header); // 跳过表头
// 先读取 bond.txt 获得键参数映射
std::string bond_path = input_dir + "/bond.txt";
std::ifstream fb(bond_path);
int a1, a2;
std::string bond_name;
std::vector<std::tuple<int, int, std::string>> conn_lines;
while (f >> a1 >> a2 >> bond_name) {
conn_lines.emplace_back(a1 - 1, a2 - 1, bond_name);
}
for (auto &[i, j, name] : conn_lines) {
double k = 1.0, r0 = 2.0;
if (fb) {
fb.clear();
fb.seekg(0);
std::string bn, header;
double bk, br;
std::getline(fb, header); // 跳过表头行
while (fb >> bn >> bk >> br) {
if (bn == name) {
k = bk;
r0 = br;
break;
}
}
}
b.pairs.push_back(i);
b.pairs.push_back(j);
b.stiffness.push_back(k);
b.rest_lengths.push_back(r0);
}
return b;
}
/* 读取 driver.txt */
static DriverData read_driver(const std::string &input_dir, const AtomData &atoms) {
DriverData d;
std::string path = input_dir + "/driver.txt";
std::ifstream f(path);
if (!f) { std::cerr << "[C++-engine] 警告: 无法打开 " << path << std::endl; return d; }
std::string header;
std::getline(f, header); // skip header
int n;
double ax, ay, az, fx, fy, fz, px, py, pz;
std::string period_str;
while (f >> n >> ax >> ay >> az >> fx >> fy >> fz >> px >> py >> pz >> period_str) {
// Find atom index by id
int idx = -1;
for (size_t i = 0; i < atoms.ids.size(); i++) {
if (atoms.ids[i] == n) { idx = i; break; }
}
if (idx < 0) {
std::cerr << "[C++-engine] 警告: driver.txt 原子 " << n << " 不在 coord.txt 中" << std::endl;
continue;
}
d.atom_idx.push_back(idx);
d.amp_x.push_back(ax); d.amp_y.push_back(ay); d.amp_z.push_back(az);
d.freq_x.push_back(fx); d.freq_y.push_back(fy); d.freq_z.push_back(fz);
// Convert degrees to radians
const double DEG2RAD = M_PI / 180.0;
d.phi_x.push_back(px * DEG2RAD);
d.phi_y.push_back(py * DEG2RAD);
d.phi_z.push_back(pz * DEG2RAD);
if (period_str == "all") {
d.has_period.push_back(0);
d.period_cycles.push_back(-1.0);
} else {
d.has_period.push_back(1);
d.period_cycles.push_back(std::stod(period_str));
}
d.freeze_x.push_back(0.0);
d.freeze_y.push_back(0.0);
d.freeze_z.push_back(0.0);
d.n_drivers++;
}
if (d.n_drivers > 0)
std::cout << "[C++-engine] 已加载驱动力: " << d.n_drivers << " 条定义" << std::endl;
return d;
}
// ========================================================================
// 物理核心
// ========================================================================
/* 加速度计算(各力独立开关控制)——与 Python compute_acceleration 一致 */
static void compute_acceleration(
int n,
const double *x, const double *y, const double *z,
const double *vx, const double *vy, const double *vz,
const double *m, const double G[3], const double B[3],
const BondData &bonds,
int gravity_field, int gravity_interaction,
int elastic_force, int damping_force,
double gravity_strength,
double *ax, double *ay, double *az)
{
// 清零
std::fill(ax, ax + n, 0.0);
std::fill(ay, ay + n, 0.0);
std::fill(az, az + n, 0.0);
// 均匀重力场
if (gravity_field) {
for (int i = 0; i < n; i++) {
ax[i] += G[0];
ay[i] += G[1];
az[i] += G[2];
}
}
// 阻尼
if (damping_force) {
for (int i = 0; i < n; i++) {
ax[i] -= B[0] * vx[i] / m[i];
ay[i] -= B[1] * vy[i] / m[i];
az[i] -= B[2] * vz[i] / m[i];
}
}
// 弹簧力
if (elastic_force) {
int nb = static_cast<int>(bonds.stiffness.size());
for (int b = 0; b < nb; b++) {
int i = bonds.pairs[b * 2];
int j = bonds.pairs[b * 2 + 1];
double dx = x[j] - x[i];
double dy = y[j] - y[i];
double dz = z[j] - z[i];
double dist = std::sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 1e-12) continue;
double stretch = dist - bonds.rest_lengths[b];
double fmag = bonds.stiffness[b] * stretch;
double ux = dx / dist, uy = dy / dist, uz = dz / dist;
double fx = fmag * ux, fy = fmag * uy, fz = fmag * uz;
ax[i] += fx / m[i]; ay[i] += fy / m[i]; az[i] += fz / m[i];
ax[j] -= fx / m[j]; ay[j] -= fy / m[j]; az[j] -= fz / m[j];
}
}
// 万有引力(所有原子对之间)
if (gravity_interaction) {
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
double dx = x[j] - x[i];
double dy = y[j] - y[i];
double dz = z[j] - z[i];
double r2 = dx * dx + dy * dy + dz * dz;
if (r2 <= 1e-12) continue;
double r = std::sqrt(r2);
double f_mag = gravity_strength * m[i] * m[j] / r2;
double fx_g = f_mag * dx / r;
double fy_g = f_mag * dy / r;
double fz_g = f_mag * dz / r;
ax[i] += fx_g / m[i]; ay[i] += fy_g / m[i]; az[i] += fz_g / m[i];
ax[j] -= fx_g / m[j]; ay[j] -= fy_g / m[j]; az[j] -= fz_g / m[j];
}
}
}
}
/* 边界条件:clamp 位置 + 速度反转 ——与 Python Limit_in_box 一致 */
static void limit_in_box(double &pos, double &vel, double lo, double hi) {
if (pos > hi) { pos = hi; vel = -vel; }
if (pos < lo) { pos = lo; vel = -vel; }
}
// ========================================================================
// 四种积分方法(只做位置/速度更新,不含边界条件)
// 与 Python: Explicit_Euler_Method / Implicit_Euler_Method /
// Midpoint_Method / Leapfrog_Method 保持一致
// ========================================================================
/* ── 显式欧拉法 ──────────── */
static void explicit_euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData &bonds, const int *fixed, double dt,
int gravity_field, int gravity_interaction,
int elastic_force, int damping_force,
double gravity_strength)
{
std::vector<double> ax(n), ay(n), az(n);
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength,
ax.data(), ay.data(), az.data());
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && 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;
}
}
/* ── 隐式欧拉法 ──────────── */
static void implicit_euler_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData &bonds, const int *fixed, double dt,
int gravity_field, int gravity_interaction,
int elastic_force, int damping_force,
double gravity_strength)
{
std::vector<double> ax(n), ay(n), az(n);
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) {
ax[i] = ay[i] = az[i] = 0;
continue;
}
double gamma_x = B[0] / m[i];
double gamma_y = B[1] / m[i];
double gamma_z = B[2] / m[i];
// 隐式更新速度(重力 + 阻尼)
double vxn = (vx[i] + G[0] * dt) / (1.0 + gamma_x * dt);
double vyn = (vy[i] + G[1] * dt) / (1.0 + gamma_y * dt);
double vzn = (vz[i] + G[2] * dt) / (1.0 + gamma_z * dt);
// 用隐式速度 + 当前位置算加速度(包含各力开关)
// 注意:Python 中 compute_acceleration(x, y, z, vx_next, ...) 用新速度+旧位置
double tpx = x[i], tpy = y[i], tpz = z[i];
compute_acceleration(1, &tpx, &tpy, &tpz, &vxn, &vyn, &vzn,
&m[i], G, B, bonds,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength,
&ax[i], &ay[i], &az[i]);
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 midpoint_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData &bonds, const int *fixed, double dt,
int gravity_field, int gravity_interaction,
int elastic_force, int damping_force,
double gravity_strength)
{
std::vector<double> ax(n), ay(n), az(n);
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength,
ax.data(), ay.data(), az.data());
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
double x_mid = x[i] + 0.5 * vx[i] * dt;
double y_mid = y[i] + 0.5 * vy[i] * dt;
double z_mid = z[i] + 0.5 * vz[i] * dt;
double vx_mid = vx[i] + 0.5 * ax[i] * dt;
double vy_mid = vy[i] + 0.5 * ay[i] * dt;
double vz_mid = vz[i] + 0.5 * az[i] * dt;
x[i] += vx_mid * dt;
y[i] += vy_mid * dt;
z[i] += vz_mid * dt;
double ax_mid, ay_mid, az_mid;
compute_acceleration(1, &x_mid, &y_mid, &z_mid, &vx_mid, &vy_mid, &vz_mid,
&m[i], G, B, bonds,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength,
&ax_mid, &ay_mid, &az_mid);
vx[i] += ax_mid * dt;
vy[i] += ay_mid * dt;
vz[i] += az_mid * dt;
}
}
/* ── 蛙跳法(Velocity-Verlet)——与 Python Leapfrog_Method 一致 ── */
static void leapfrog_full_step(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData &bonds, const int *fixed, double dt,
int gravity_field, int gravity_interaction,
int elastic_force, int damping_force,
double gravity_strength)
{
// 第一次加速度
std::vector<double> ax(n), ay(n), az(n);
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength,
ax.data(), ay.data(), az.data());
// 半推速度:v_half = v + 0.5*a*dt (存入 vx, vy, vz)
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt * 0.5;
vy[i] += ay[i] * dt * 0.5;
vz[i] += az[i] * dt * 0.5;
}
// 全推位置(不含边界,边界在外层统一处理)
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
x[i] += vx[i] * dt; // vx 此时是 v_half
y[i] += vy[i] * dt;
z[i] += vz[i] * dt;
}
// 显式预测器:v_pred = v_half + 0.5*a_old*dt,用第一次加速度外推半步
// 包含所有力的贡献(标准 Velocity-Verlet 预测步)
std::vector<double> pred_vx(n), pred_vy(n), pred_vz(n);
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
pred_vx[i] = vx[i] + 0.5 * ax[i] * dt;
pred_vy[i] = vy[i] + 0.5 * ay[i] * dt;
pred_vz[i] = vz[i] + 0.5 * az[i] * dt;
}
// 用新位置 + 预测速度重算加速度
compute_acceleration(n, x, y, z, pred_vx.data(), pred_vy.data(), pred_vz.data(),
m, G, B, bonds,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength,
ax.data(), ay.data(), az.data());
// 速度后半步:v = v_half + 0.5*a_next*dt
// vx 仍为 v_half(未被覆盖),直接加上 0.5*a_next*dt
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt * 0.5;
vy[i] += ay[i] * dt * 0.5;
vz[i] += az[i] * dt * 0.5;
}
}
/* ── 分发器:调用对应积分方法 + 边界条件(与 Python apply_motion_update 一致)── */
static void apply_step(
const std::string &method,
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
const double *m, const double G[3], const double B[3],
const BondData &bonds, const int *fixed,
const double *pos_0,
double box_a, double dt,
int gravity_field, int gravity_interaction,
int elastic_force, int damping_force,
double gravity_strength)
{
// 积分
if (method == "explicit_euler") {
explicit_euler_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength);
} else if (method == "implicit_euler") {
implicit_euler_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength);
} else if (method == "midpoint") {
midpoint_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength);
} else if (method == "leapfrog") {
leapfrog_full_step(n, x, y, z, vx, vy, vz, m, G, B, bonds, fixed, dt,
gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength);
} else {
die("未知算法: " + method);
}
// 边界条件(与 Python Limit_in_box 一致)
for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
limit_in_box(x[i], vx[i], -box_a, box_a);
limit_in_box(y[i], vy[i], -box_a, box_a);
limit_in_box(z[i], vz[i], -box_a, box_a);
}
// 逐自由度固定约束(与 Python apply_fixed_constraints 一致)
for (int i = 0; i < n; i++) {
if (fixed[i*3+0]) { x[i] = pos_0[i*3]; vx[i] = 0.0; }
if (fixed[i*3+1]) { y[i] = pos_0[i*3+1]; vy[i] = 0.0; }
if (fixed[i*3+2]) { z[i] = pos_0[i*3+2]; vz[i] = 0.0; }
}
}
// ========================================================================
// 驱动力应用
// ========================================================================
static void apply_driving_force(
int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz,
double t, int step, double dt,
DriverData &drivers)
{
if (drivers.n_drivers == 0) return;
for (int d = 0; d < drivers.n_drivers; d++) {
int idx = drivers.atom_idx[d];
// Check period limits
if (drivers.has_period[d]) {
double max_freq = std::max({std::fabs(drivers.freq_x[d]),
std::fabs(drivers.freq_y[d]),
std::fabs(drivers.freq_z[d])});
int period_steps = 0;
if (max_freq > 1e-12) {
period_steps = static_cast<int>(drivers.period_cycles[d] / max_freq / dt);
}
if (step > period_steps) {
// Frozen: keep last position, zero velocity
x[idx] = drivers.freeze_x[d];
y[idx] = drivers.freeze_y[d];
z[idx] = drivers.freeze_z[d];
vx[idx] = vy[idx] = vz[idx] = 0.0;
continue;
}
}
const double TWO_PI = 2.0 * M_PI;
double px = drivers.amp_x[d] * std::cos(TWO_PI * drivers.freq_x[d] * t + drivers.phi_x[d]);
double py = drivers.amp_y[d] * std::cos(TWO_PI * drivers.freq_y[d] * t + drivers.phi_y[d]);
double pz = drivers.amp_z[d] * std::cos(TWO_PI * drivers.freq_z[d] * t + drivers.phi_z[d]);
double vpx = -drivers.amp_x[d] * TWO_PI * drivers.freq_x[d] * std::sin(TWO_PI * drivers.freq_x[d] * t + drivers.phi_x[d]);
double vpy = -drivers.amp_y[d] * TWO_PI * drivers.freq_y[d] * std::sin(TWO_PI * drivers.freq_y[d] * t + drivers.phi_y[d]);
double vpz = -drivers.amp_z[d] * TWO_PI * drivers.freq_z[d] * std::sin(TWO_PI * drivers.freq_z[d] * t + drivers.phi_z[d]);
x[idx] = px; y[idx] = py; z[idx] = pz;
vx[idx] = vpx; vy[idx] = vpy; vz[idx] = vpz;
// Record freeze position at the last driving step
if (drivers.has_period[d]) {
double max_freq = std::max({std::fabs(drivers.freq_x[d]),
std::fabs(drivers.freq_y[d]),
std::fabs(drivers.freq_z[d])});
int period_steps = 0;
if (max_freq > 1e-12) {
period_steps = static_cast<int>(drivers.period_cycles[d] / max_freq / dt);
}
if (step == period_steps) {
drivers.freeze_x[d] = px;
drivers.freeze_y[d] = py;
drivers.freeze_z[d] = pz;
}
}
}
}
// ========================================================================
// JSON 输出
// ========================================================================
static void write_trajectory_json(
const std::string &path,
const std::vector<double> &x, const std::vector<double> &y,
const std::vector<double> &z, const std::vector<double> &vx,
const std::vector<double> &vy, const std::vector<double> &vz,
int n_steps, int n_atoms,
const SimParams &params, const AtomData &atoms, const BondData &bonds)
{
std::ofstream f(path);
if (!f) die("无法写入 " + path);
f << std::setprecision(15);
f << "{\n";
// 轨迹数组
const std::string names[] = {"traj_x","traj_y","traj_z","traj_vx","traj_vy","traj_vz"};
const std::vector<double> *arrs[] = {&x, &y, &z, &vx, &vy, &vz};
for (int a = 0; a < 6; a++) {
f << " \"" << names[a] << "\": [\n";
const auto &data = *arrs[a];
for (int t = 0; t < n_steps; t++) {
f << " [";
for (int i = 0; i < n_atoms; i++) {
f << data[t * n_atoms + i];
if (i < n_atoms - 1) f << ',';
}
f << ']';
if (t < n_steps - 1) f << ',';
f << '\n';
}
f << " ],\n";
}
// 标量参数
f << " \"NT\": " << params.NT << ",\n";
f << " \"DT\": " << params.DT << ",\n";
f << " \"NSTEP\": " << params.NSTEP << ",\n";
f << " \"method\": \"" << params.method << "\",\n";
f << " \"warmup_steps\": " << params.warmup_steps << ",\n";
f << " \"G\": [" << params.G[0] << ", " << params.G[1] << ", " << params.G[2] << "],\n";
f << " \"B\": [" << params.B[0] << ", " << params.B[1] << ", " << params.B[2] << "],\n";
// 原子信息
f << " \"atom_ids\": [";
for (size_t i = 0; i < atoms.ids.size(); i++) {
if (i > 0) f << ',';
f << atoms.ids[i];
}
f << "],\n";
f << " \"atom_masses\": [";
for (size_t i = 0; i < atoms.masses.size(); i++) {
if (i > 0) f << ',';
f << atoms.masses[i];
}
f << "],\n";
// 成键
f << " \"bond_pairs\": [";
for (size_t b = 0; b < bonds.stiffness.size(); b++) {
if (b > 0) f << ',';
f << "[" << bonds.pairs[b * 2] << ", " << bonds.pairs[b * 2 + 1] << "]";
}
f << "],\n";
f << " \"bond_stiffness\": [";
for (size_t b = 0; b < bonds.stiffness.size(); b++) {
if (b > 0) f << ',';
f << bonds.stiffness[b];
}
f << "],\n";
f << " \"bond_rest_lengths\": [";
for (size_t b = 0; b < bonds.rest_lengths.size(); b++) {
if (b > 0) f << ',';
f << bonds.rest_lengths[b];
}
f << "],\n";
f << " \"driving_force\": " << params.driving_force << "\n";
f << "}\n";
}
// ========================================================================
// 主函数
// ========================================================================
int main(int argc, char **argv) {
if (argc < 4) {
std::cerr << "用法: " << argv[0] << " <input_dir> <output_dir> <param_json>" << std::endl;
return 1;
}
std::string input_dir = argv[1];
std::string output_dir = argv[2];
std::string param_path = argv[3];
auto t0 = std::chrono::high_resolution_clock::now();
// 读取参数和输入
SimParams params = read_params(param_path);
AtomData atoms = read_coord(input_dir);
BondData bonds = read_bonds(input_dir);
DriverData drivers;
if (params.driving_force) {
drivers = read_driver(input_dir, atoms);
}
std::cout << "[C++-engine] 原子数=" << atoms.ids.size()
<< ", 键数=" << bonds.stiffness.size()
<< ", NT=" << params.NT << ", DT=" << params.DT
<< ", method=" << params.method << std::endl;
int n = static_cast<int>(atoms.ids.size());
// 初始化位置/速度
std::vector<double> x(n), y(n), z(n), vx(n), vy(n), vz(n);
for (int i = 0; i < n; i++) {
x[i] = atoms.pos_0[i * 3];
y[i] = atoms.pos_0[i * 3 + 1];
z[i] = atoms.pos_0[i * 3 + 2];
vx[i] = atoms.vel_0[i * 3];
vy[i] = atoms.vel_0[i * 3 + 1];
vz[i] = atoms.vel_0[i * 3 + 2];
}
// 分配轨迹缓冲区
int record_steps = params.NT - params.warmup_steps;
std::vector<double> traj_x(record_steps * n);
std::vector<double> traj_y(record_steps * n);
std::vector<double> traj_z(record_steps * n);
std::vector<double> traj_vx(record_steps * n);
std::vector<double> traj_vy(record_steps * n);
std::vector<double> traj_vz(record_steps * n);
// 预热
for (int s = 0; s < params.warmup_steps; s++) {
double tw = (s + 1) * params.DT;
if (params.driving_force)
apply_driving_force(n, x.data(), y.data(), z.data(), vx.data(), vy.data(), vz.data(), tw, s, params.DT, drivers);
apply_step(params.method, n, x.data(), y.data(), z.data(),
vx.data(), vy.data(), vz.data(),
atoms.masses.data(), params.G, params.B,
bonds, atoms.fixed.data(),
atoms.pos_0.data(),
params.box_a, params.DT,
params.gravity_field, params.gravity_interaction,
params.elastic_force, params.damping_force, params.gravity_strength);
}
// 记录
for (int s = 0; s < record_steps; s++) {
double t = (s + params.warmup_steps) * params.DT;
if (params.driving_force)
apply_driving_force(n, x.data(), y.data(), z.data(), vx.data(), vy.data(), vz.data(), t, s, params.DT, drivers);
// 保存当前帧
for (int i = 0; i < n; i++) {
traj_x[s * n + i] = x[i];
traj_y[s * n + i] = y[i];
traj_z[s * n + i] = z[i];
traj_vx[s * n + i] = vx[i];
traj_vy[s * n + i] = vy[i];
traj_vz[s * n + i] = vz[i];
}
apply_step(params.method, n, x.data(), y.data(), z.data(),
vx.data(), vy.data(), vz.data(),
atoms.masses.data(), params.G, params.B,
bonds, atoms.fixed.data(),
atoms.pos_0.data(),
params.box_a, params.DT,
params.gravity_field, params.gravity_interaction,
params.elastic_force, params.damping_force, params.gravity_strength);
}
// 输出轨迹
std::string out_path = output_dir + "/trajectory.txt";
write_trajectory_json(out_path, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz,
record_steps, n, params, atoms, bonds);
auto t1 = std::chrono::high_resolution_clock::now();
double elapsed = std::chrono::duration<double>(t1 - t0).count();
std::cout << "[C++-engine] 计算完成: " << record_steps << " 步, " << elapsed << " s" << std::endl;
return 0;
}
+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 示例案例 # Dynamics 示例案例
本目录包含 6 个从简单到复杂的物理模拟案例,均基于 `../dynamics.py` 框架运行。 本目录包含 10 个从简单到复杂的物理模拟案例,均基于 `../dynamics.py` 框架运行。
--- ---
## 案例一览 ## 案例一览
| 案例 | 标题 | 简介 | 原子数 | 力类型 | | 案例 | 标题 | 简介 | 原子数 | 引擎 | 力类型 |
|---|---|---|---|---| |------|------|------|--------|------|--------|
| [case01](./case01/) | **双粒子弹簧系统** | 两个原子由弹簧连接,在重力场中运动 | 2 | 重力 + 弹簧 | | [case01](./case01/) | **双粒子弹簧系统** | 两个原子由弹簧连接,在重力场中运动 | 2 | Python | 重力 + 弹簧 |
| [case02](./case02/) | **行星运动** | 地球绕太阳椭圆公转(万有引力) | 2 | 万有引力 | | [case02](./case02/) | **行星运动** | 地球绕太阳椭圆公转(万有引力) | 2 | Python | 万有引力 |
| [case03](./case03/) | **日地月系统(失** | 地球绕太阳、月球绕地球,参数不当导致失稳 | 3 | 万有引力 | | [case03](./case03/) | **日地月系统(失** | 地球绕太阳、月球绕地球,参数不当导致失稳 | 3 | Python | 万有引力 |
| [case04](./case04/) | **日地月系统(成功** | 地球绕太阳、月球绕地球,稳定轨道 | 3 | 万有引力 | | [case04](./case04/) | **日地月系统(稳定** | 地球绕太阳、月球绕地球,稳定轨道 | 3 | Python | 万有引力 |
| [case05](./case05/) | **一维原子链纵波** | 驱动原子 1 沿 x 轴振动,产生纵波传播 | 60 | 弹簧 + 驱动力 | | [case05](./case05/) | **一维原子链纵波** | 驱动原子 1 沿 x 轴振动,产生纵波传播 | 60 | Python | 弹簧 + 驱动力 |
| [case06](./case06/) | **一维原子链横波** | 驱动原子 1 沿 z 轴振动,产生横波传播 | 120 | 弹簧 + 驱动力 | | [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(蛙跳法) - **算法**leapfrog(蛙跳法)
- **物理**:重力 m·g + 弹簧胡克力 - **物理**:重力 m·g + 弹簧胡克力
- **渲染**Sphere 模式(精细网格球体)
### case02 — 行星运动 ### case02 — 行星运动
模拟地球绕太阳的椭圆轨道运动(一个固定大质量中心体 + 一个绕行小质量体)。采用万有引力相互作用。 模拟地球绕太阳的椭圆轨道运动(一个固定大质量中心体 + 一个绕行小质量体)。采用万有引力相互作用。
- **力开关**:万有引力开(含强度参数) - **力开关**:万有引力开(含强度参数 `gravity_strength: 100.0`
- **算法**leapfrog(蛙跳法) - **算法**leapfrog(蛙跳法)
- **物理**:牛顿万有引力 F = G·m₁·m₂/r² - **物理**:牛顿万有引力 F = G·m₁·m₂/r²
- **渲染**Sphere 模式
### case03 — 日地月系统(失败案例) ### case03 — 日地月系统(失败案例)
@@ -60,14 +66,53 @@
- **波速**:快(x 方向弹簧力为线性) - **波速**:快(x 方向弹簧力为线性)
- **渲染**Marker 模式(GPU 实例化,60 原子) - **渲染**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(蛙跳法) - **算法**leapfrog(蛙跳法)
- **引擎**C(高性能)
- **参数**T_total=1000, NSTEP=500
- **波速**:慢(z 方向弹簧力呈几何非线性,类似 FPU 系统) - **波速**:慢(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 cd examples/case05
# 完整运行(模拟 + 采样 + 3D 动画) # 完整运行(模拟 + 3D 动画)
python run_dynamics.py python run_dynamics.py
# 仅运行模拟,跳过 3D 动画 # 仅运行模拟,跳过 3D 动画
@@ -89,6 +134,18 @@ python ../../draw.py output/
每个案例的 `input/input.txt` 中可配置所有物理参数、力开关、算法、渲染方式等。 每个案例的 `input/input.txt` 中可配置所有物理参数、力开关、算法、渲染方式等。
## 案例选择指南
| 你想做什么 | 推荐案例 |
|-----------|---------|
| 快速上手、理解基本框架 | case01 |
| 天体力学 / 万有引力 | case02 / case04 |
| 波动物理(纵波) | case05 |
| 波动物理(横波、非线性、阻尼) | case06 |
| 双端驱动/波干涉 | case07 |
| 对比不同引擎性能 | case08 (C) / case09 (Fortran) |
| 能量分析 | case10 |
## 框架结构 ## 框架结构
``` ```
@@ -101,6 +158,6 @@ dynamics/
├── examples/ # 案例(本目录) ├── examples/ # 案例(本目录)
│ ├── case01/ │ ├── case01/
│ ├── ... │ ├── ...
│ └── case06/ │ └── case10/
└── output/ # 默认输出目录 └── 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=跳过 # 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧 # 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/trajectory.txt 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: 1 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_plot_wave: 0 # 绘制波形能量动画 step_plot_wave: 0 # 绘制波形能量动画
plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1 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>
+3 -3
View File
@@ -1,4 +1,4 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.28 0 0 0 0 0 0 1 1 1 1 27000 1.0 0 0 0 0 0 0 1 1 1
2 1 0.28 4 0 0 0 0 4 0 0 0 2 81 0.2 10 0 0 0 0 520 0 0 0
3 0.1 0.18 5 0 0 0 0 6 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 引擎 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 NSTEP: 2
# ── 时间步长 ────────────────────────────────── # ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s) DT: 0.0001 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 # 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起) 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 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 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 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 4 1 0.1 3 0 0 0 0 0 0 1 1
+1 -1
View File
@@ -1,2 +1,2 @@
bond_name k rest_length bond_name k rest_length
k1 1.0 1.0 k1 10.0 1.0
+119 -119
View File
@@ -1,121 +1,121 @@
n mass radius x y z vx vy vz fix_x fix_y fix_z n mass radius x y z vx vy vz fix_x fix_y fix_z
1 1 0.1 0 0 0 0 0 0 1 1 0 1 1 0.1 0 0 0 0 0 0 0 1 0
2 1 0.1 1 0 0 0 0 0 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 0 40 1 0.1 39 0 0 0 0 0 0 1 0
41 1 0.1 40 0 0 0 0 0 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 1 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 120 1 0.1 119 0 0 0 0 0 1 1 1
+1 -1
View File
@@ -1,2 +1,2 @@
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period 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.1 0 0 90 all 1 0 0 2.0 0 0 0.05 0 0 90 all
+21 -9
View File
@@ -5,21 +5,24 @@
# ── 流程控制 ────────────────────────────────── # ── 流程控制 ──────────────────────────────────
# 每步用 0/1 单独开关,1=执行,0=跳过 # 每步用 0/1 单独开关,1=执行,0=跳过
# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧 # 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧
step_simulate: 1 # 运行物理模拟 → output/trajectory.txt step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧)
step_sample: 1 # 抽帧 → output/display.txt step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行
step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png
step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy) step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy)
step_plot_wave: 0 # 绘制波形能量动画 step_plot_wave: 0 # 绘制波形能量动画
force_calc: 0 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出 force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出
plot_wave_save_gif: 1 # 输出波形 GIF(需 step_plot_wave=1 plot_wave_save_gif: 1 # 输出波形 GIF(需 step_plot_wave=1
plot_wave_save_mp4: 1 # 输出波形 MP4(需 step_plot_wave=1 plot_wave_save_mp4: 1 # 输出波形 MP4(需 step_plot_wave=1
# ── 文件保存 ──────────────────────────────────
save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧)
# ── 计算引擎 ────────────────────────────────── # ── 计算引擎 ──────────────────────────────────
# 可选: python, c, cpp, fortran, java # 可选: 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]³ 内
# ── 初始构型 ────────────────────────────────── # ── 初始构型 ──────────────────────────────────
# 坐标文件格式: # 坐标文件格式:
@@ -36,7 +39,7 @@ plot_atom: 1
# ── 物理参数 ────────────────────────────────── # ── 物理参数 ──────────────────────────────────
# 三个方向分量分别对应 x, y, z # 三个方向分量分别对应 x, y, z
G: [0.00, 0.00, 0.00] # 重力场分量 (m/s²) 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=开启)────────────────── # ── 力开关(0=关闭, 1=开启)──────────────────
gravity_field: 0 # 均匀重力场 (G) gravity_field: 0 # 均匀重力场 (G)
@@ -63,10 +66,10 @@ warmup_steps: 0 # 默认 0(立即开始记录)
# 总模拟时间(秒),程序自动计算 NT = T_total / DT # 总模拟时间(秒),程序自动计算 NT = T_total / DT
# 如果同时指定了 NT,以 NT 为准 # 如果同时指定了 NT,以 NT 为准
T_total: 4.0 T_total: 1000.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画) # 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 100 NSTEP: 500
# ── 时间步长 ────────────────────────────────── # ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s) DT: 0.001 # 时间步长 (s)
@@ -85,7 +88,7 @@ use_marker: 1
# ── 显示参数 ────────────────────────────────── # ── 显示参数 ──────────────────────────────────
# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 # 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序
alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.5] alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
# 小球颜色 # 小球颜色
# 小球半径从 coord_file 的 radius 列读取 # 小球半径从 coord_file 的 radius 列读取
@@ -97,3 +100,12 @@ ball_color_b: 0.90 # B 分量
box_color_r: 0.80 box_color_r: 0.80
box_color_g: 0.80 box_color_g: 0.80
box_color_b: 0.85 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 运动
+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
+1 -1
View File
@@ -31,7 +31,7 @@ def load_dynamics_module(module_path: Path):
def main(): def main():
parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case01") parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case06")
parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图") parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图")
args = parser.parse_args() args = parser.parse_args()
+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()

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