Compare commits

..

29 Commits

Author SHA1 Message Date
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
21 changed files with 3869 additions and 60629 deletions
+4
View File
@@ -99,3 +99,7 @@ desktop.ini
*.zip *.zip
*.tar.gz *.tar.gz
*.tar.bz2 *.tar.bz2
output_test/
optimization
optimization/*
+578 -51
View File
@@ -5,11 +5,12 @@ compute.py
功能: 功能:
1. 运行 NT 步物理模拟( kinematics / dynamics 等运动模式) 1. 运行 NT 步物理模拟( kinematics / dynamics 等运动模式)
2. 将每一步的 (x, y, z, vx, vy, vz) 保存到 output/trajectory.txt 2. 按 NSTEP 抽帧,输出 output/display.txt(新文本格式)
3. 同时保存所有模拟参数元数据 3. 可选(save_trajectory=1)保留完整轨迹 output/trajectory.txtJSON
""" """
import json import json
import re
import numpy as np import numpy as np
import os import os
import platform import platform
@@ -63,6 +64,14 @@ ball_color_b = None
box_color_r = None box_color_r = None
box_color_g = None box_color_g = None
box_color_b = None box_color_b = None
use_marker = 0
camera_keyframes_raw = ""
camera_distance = 40.0
camera_elevation = 0.0
camera_azimuth = 0.0
FIXED_MASK_X = None
FIXED_MASK_Y = None
FIXED_MASK_Z = None
# 力开关 # 力开关
GRAVITY_FIELD = 1 # 均匀重力场 GRAVITY_FIELD = 1 # 均匀重力场
@@ -84,6 +93,55 @@ Z_MIN = None
Z_MAX = None Z_MAX = None
def _load_camera_motion(path):
"""读取 move_camera.txt(速度段格式),返回 JSON 字符串。
格式:每行是一个运动段
start-end vx=f1 vy=f2 vz=f3 rx=d1 ry=d2 rz=d3
示例:
1-60 vx=1.0 rx=10
30-90 vy=2.0 ry=20 rz=10
返回 JSON: [{"start":N,"end":N,"v":[x,y,z],"r":[x,y,z]},...]
"""
import re
if not os.path.exists(path):
print(f"[compute] 警告: 未找到 {path},跳过运动相机")
return ""
segments = []
with open(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 = [0.0, 0.0, 0.0]
r = [0.0, 0.0, 0.0]
# 解析 vx=, vy=, vz=
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))
# 解析 rx=, ry=, rz=
for i, axis in enumerate(['x', 'y', 'z']):
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})
if not segments:
return ""
import json
return json.dumps(segments)
def _to_text_value(value): def _to_text_value(value):
"""Convert numpy-heavy objects into JSON-friendly plain Python values.""" """Convert numpy-heavy objects into JSON-friendly plain Python values."""
if isinstance(value, np.ndarray): if isinstance(value, np.ndarray):
@@ -117,7 +175,7 @@ def _from_text_value(value):
def save_text_data(path, data): def save_text_data(path, data):
"""Save structured simulation data as formatted JSON text.""" """Save structured simulation data as formatted JSON text (旧格式,保留兼容)."""
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f: with open(path, "w", encoding="utf-8") as f:
json.dump(_to_text_value(data), f, ensure_ascii=False, indent=2) json.dump(_to_text_value(data), f, ensure_ascii=False, indent=2)
@@ -125,12 +183,208 @@ def save_text_data(path, data):
def load_text_data(path): def load_text_data(path):
"""Load structured simulation data from JSON text.""" """Load structured simulation data from JSON text (旧格式)."""
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
return _from_text_value(data) return _from_text_value(data)
# ========================================================================
# 新 display.txt 格式:纯文本,按帧分块
# 第1行: number of frames: N
# 第2行: number of particles: M
# 第3行: frame: 1
# 第4行: n x y z vx vy vz
# 第5+行: 数据行(每个原子一行)
# 重复第3-5行直到所有帧
# ========================================================================
def save_display_txt(path, frames_x, frames_y, frames_z,
frames_vx, frames_vy, frames_vz,
atom_ids, n_total_frames, n_total_particles,
header_fields=None):
"""Write display.txt in new text format.
Args:
path: 输出文件路径
frames_x/y/z/vx/vy/vz: (n_frames, n_atoms) 数组
atom_ids: (n_atoms,) 原子编号数组
n_total_frames: 总帧数(含未采样)
n_total_particles: 总粒子数
header_fields: 可选的额外元数据字典(写入文件头之后)
"""
os.makedirs(os.path.dirname(path), exist_ok=True)
n_frames = frames_x.shape[0]
n_atoms = frames_x.shape[1]
# 格式化辅助:固定宽度,6位小数
def fmt(v): return f"{v:13.6f}"
with open(path, "w", encoding="utf-8") as f:
f.write(f"number of frames: {n_total_frames}\n")
f.write(f"number of particles: {n_total_particles}\n")
# 写入额外元数据
if header_fields:
for k, v in header_fields.items():
f.write(f"{k}: {v}\n")
for fr in range(n_frames):
f.write(f"\nframe: {fr + 1}\n")
f.write(f"n x y z vx vy vz\n")
for a in range(n_atoms):
f.write(f"{atom_ids[a]:d}"
f"{fmt(frames_x[fr, a])}"
f"{fmt(frames_y[fr, a])}"
f"{fmt(frames_z[fr, a])}"
f"{fmt(frames_vx[fr, a])}"
f"{fmt(frames_vy[fr, a])}"
f"{fmt(frames_vz[fr, a])}\n")
return path
def load_display_txt(path):
"""Read display.txt new text format into numpy arrays(快速版).
Returns dict with keys: frames_x/y/z/vx/vy/vz, atom_ids,
n_total_frames, n_total_particles, header_fields
"""
import re
with open(path, "r", encoding="utf-8") as f:
raw = f.read()
# 解析 header 行
header_fields = {}
n_total_frames = 0
n_total_particles = 0
lines = raw.splitlines()
data_start = 0
for i, line in enumerate(lines):
line_stripped = line.strip()
if line_stripped.startswith("number of frames:"):
n_total_frames = int(line_stripped.split(":")[1].strip())
elif line_stripped.startswith("number of particles:"):
n_total_particles = int(line_stripped.split(":")[1].strip())
elif line_stripped.startswith("frame:"):
data_start = i
break
else:
if ":" in line_stripped:
k, v = line_stripped.split(":", 1)
header_fields[k.strip()] = v.strip()
# 快速定位所有数据行:跳过 frame header 和 column header
# 数据行格式:每行 7 个字段(n x y z vx vy vz),固定宽度列
data_text = []
i = data_start
n_frames = 0
while i < len(lines):
line = lines[i].strip()
if line.startswith("frame:"):
n_frames += 1
i += 2 # 跳过 "frame: N" 和列头行
continue
if line:
data_text.append(line)
i += 1
if n_frames == 0 or not data_text:
raise ValueError(f"{path} 中没有有效帧数据")
# 用 numpy 批量解析所有数据行(远比逐行 split+float 快)
data_array = np.genfromtxt(data_text, dtype=np.float64)
# data_array shape: (n_frames * n_atoms, 7) — 列: n, x, y, z, vx, vy, vz
n_atoms = n_total_particles
atoms_per_frame = len(data_text) // n_frames
# 提取原子ID(第一帧即可)
atom_ids = data_array[0:n_atoms, 0].astype(np.int64)
# 重塑为 (n_frames, n_atoms, 6) — 去掉第0列(原子ID)
all_data = data_array[:, 1:].reshape(n_frames, n_atoms, 6)
return {
"frames_x": all_data[:, :, 0],
"frames_y": all_data[:, :, 1],
"frames_z": all_data[:, :, 2],
"frames_vx": all_data[:, :, 3],
"frames_vy": all_data[:, :, 4],
"frames_vz": all_data[:, :, 5],
"atom_ids": atom_ids,
"n_total_frames": n_total_frames,
"n_total_particles": n_total_particles,
"header_fields": header_fields,
}
def save_display_npz(path, frames_x, frames_y, frames_z,
frames_vx, frames_vy, frames_vz,
atom_ids, header_fields=None):
"""Write display.npz binary format alongside display.txt.
Stores all frame arrays in compressed NumPy format. Metadata is
serialised as a JSON byte-string stored in the 'meta' array so the
file stays self-contained.
Args:
path: output path ending in '.npz'
frames_x/y/z/vx/vy/vz: (n_frames, n_atoms) float64 arrays
atom_ids: (n_atoms,) int array
header_fields: dict of metadata key-value strings (same as for
save_display_txt)
"""
meta_bytes = np.bytes_(json.dumps(header_fields or {}, ensure_ascii=False))
stem = path[:-4] if path.endswith(".npz") else path
np.savez_compressed(
stem,
frames_x=frames_x,
frames_y=frames_y,
frames_z=frames_z,
frames_vx=frames_vx,
frames_vy=frames_vy,
frames_vz=frames_vz,
atom_ids=atom_ids,
meta=np.array(meta_bytes),
)
return path
def load_display_npz(path):
"""Read display.npz and return the same dict as load_display_txt."""
data = np.load(path, allow_pickle=False)
header_fields = json.loads(data["meta"].item().decode("utf-8"))
frames_x = data["frames_x"]
n_total_frames = int(header_fields.get("number_of_frames",
frames_x.shape[0]))
n_total_particles = frames_x.shape[1]
return {
"frames_x": frames_x,
"frames_y": data["frames_y"],
"frames_z": data["frames_z"],
"frames_vx": data["frames_vx"],
"frames_vy": data["frames_vy"],
"frames_vz": data["frames_vz"],
"atom_ids": data["atom_ids"],
"n_total_frames": n_total_frames,
"n_total_particles": n_total_particles,
"header_fields": header_fields,
}
def load_display(output_dir):
"""Load display data, preferring binary .npz over text .txt."""
npz_path = os.path.join(output_dir, "display.npz")
txt_path = os.path.join(output_dir, "display.txt")
if os.path.exists(npz_path):
return load_display_npz(npz_path)
if os.path.exists(txt_path):
return load_display_txt(txt_path)
raise FileNotFoundError(
f"找不到 display.npz 或 display.txt in {output_dir}")
def get_output_dir(base_dir=None): def get_output_dir(base_dir=None):
"""Return the output directory used for generated artifacts.""" """Return the output directory used for generated artifacts."""
override = os.environ.get("DYNAMICS_OUTPUT_DIR") override = os.environ.get("DYNAMICS_OUTPUT_DIR")
@@ -445,9 +699,8 @@ def apply_driving_force(x, y, z, vx, vy, vz, t, step, drivers, dt):
period_steps = None # 全程驱动 period_steps = None # 全程驱动
# 当前驱动力下的位置 / 速度 # 当前驱动力下的位置 / 速度
t_vec = np.array([t, t, t], dtype=np.float64) pos_drive = d["amp"] * np.cos(2.0 * np.pi * d["freq"] * t + d["phi"])
pos_drive = d["amp"] * np.cos(2.0 * np.pi * d["freq"] * t_vec + d["phi"]) vel_drive = -d["amp"] * 2.0 * np.pi * d["freq"] * np.sin(2.0 * np.pi * d["freq"] * t + d["phi"])
vel_drive = -d["amp"] * 2.0 * np.pi * d["freq"] * np.sin(2.0 * np.pi * d["freq"] * t_vec + d["phi"])
x[idx] = pos_drive[0] x[idx] = pos_drive[0]
y[idx] = pos_drive[1] y[idx] = pos_drive[1]
@@ -510,6 +763,8 @@ def run_from_config(config, out_dir=None):
global X_MIN, X_MAX, Y_MIN, Y_MAX, Z_MIN, Z_MAX global X_MIN, X_MAX, Y_MIN, Y_MAX, Z_MIN, Z_MAX
global ball_radius, ball_color_r, ball_color_g, ball_color_b global ball_radius, ball_color_r, ball_color_g, ball_color_b
global box_color_r, box_color_g, box_color_b global box_color_r, box_color_g, box_color_b
global use_marker, camera_keyframes_raw, camera_distance, camera_elevation, camera_azimuth
global FIXED_MASK_X, FIXED_MASK_Y, FIXED_MASK_Z
global warmup_steps, sample_start, sample_end global warmup_steps, sample_start, sample_end
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
global DRIVING_FORCE, DRIVER_DATA global DRIVING_FORCE, DRIVER_DATA
@@ -526,6 +781,9 @@ def run_from_config(config, out_dir=None):
coord_path = os.path.join(out_dir, coord_path) coord_path = os.path.join(out_dir, coord_path)
(ATOM_IDS, ATOM_MASSES, ATOM_RADII, ATOM_POSITIONS, (ATOM_IDS, ATOM_MASSES, ATOM_RADII, ATOM_POSITIONS,
ATOM_VELOCITIES, ATOM_FIXED) = load_coord_file(coord_path) ATOM_VELOCITIES, ATOM_FIXED) = load_coord_file(coord_path)
FIXED_MASK_X = ATOM_FIXED[:, 0] != 0
FIXED_MASK_Y = ATOM_FIXED[:, 1] != 0
FIXED_MASK_Z = ATOM_FIXED[:, 2] != 0
BOND_CONNECTION_FILE = str(config.get("connection_file", os.path.join("input", "connection.txt"))) BOND_CONNECTION_FILE = str(config.get("connection_file", os.path.join("input", "connection.txt")))
BOND_PARAMETER_FILE = str(config.get("bond_file", os.path.join("input", "bond.txt"))) BOND_PARAMETER_FILE = str(config.get("bond_file", os.path.join("input", "bond.txt")))
connection_path = BOND_CONNECTION_FILE connection_path = BOND_CONNECTION_FILE
@@ -570,6 +828,10 @@ def run_from_config(config, out_dir=None):
box_color_r = float(config.get("box_color_r", 0.8)) box_color_r = float(config.get("box_color_r", 0.8))
box_color_g = float(config.get("box_color_g", 0.8)) box_color_g = float(config.get("box_color_g", 0.8))
box_color_b = float(config.get("box_color_b", 0.85)) box_color_b = float(config.get("box_color_b", 0.85))
use_marker = int(config.get("use_marker", 0))
camera_distance = float(config.get("camera_distance", 40.0))
camera_elevation = float(config.get("camera_elevation", 0.0))
camera_azimuth = float(config.get("camera_azimuth", 0.0))
# 力开关 # 力开关
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
@@ -590,13 +852,25 @@ def run_from_config(config, out_dir=None):
driver_path = os.path.join(out_dir, driver_rel) driver_path = os.path.join(out_dir, driver_rel)
DRIVER_DATA = load_driver_file(driver_path, ATOM_IDS) DRIVER_DATA = load_driver_file(driver_path, ATOM_IDS)
# 加载运动相机关键帧
camera_keyframes_raw = ""
move_camera = int(config.get("move_camera", 0))
camera_keyframes_raw = ""
if move_camera:
cam_rel = str(config.get("move_camera_file", os.path.join("input", "move_camera.txt")))
cam_path = cam_rel
if out_dir is not None and not os.path.isabs(cam_rel):
cam_path = os.path.join(out_dir, cam_rel)
camera_keyframes_raw = _load_camera_motion(cam_path)
print(f"[compute] 使用算法: {METHOD}") print(f"[compute] 使用算法: {METHOD}")
print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键") print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键")
if config.get("_skip_run", False): if config.get("_skip_run", False):
return None, None, None, None, None, None return None, None, None, None, None, None
t_start = time.time() t_start = time.time()
t_start_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") t_start_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = run_simulation() traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = run_simulation(
save_trajectory=int(config.get("save_trajectory", 0)))
t_end = time.time() t_end = time.time()
t_end_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") t_end_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
elapsed = t_end - t_start elapsed = t_end - t_start
@@ -698,6 +972,20 @@ def run_engine(engine, input_dir, output_dir, config):
"damping_force": int(config.get("damping_force", 0)), "damping_force": int(config.get("damping_force", 0)),
"gravity_strength": float(config.get("gravity_strength", 1.0)), "gravity_strength": float(config.get("gravity_strength", 1.0)),
"driving_force": int(config.get("driving_force", 0)), "driving_force": int(config.get("driving_force", 0)),
"save_trajectory": int(config.get("save_trajectory", 0)),
# 渲染参数(用于 display.txt header
"alpha": config.get("alpha", 0.2),
"ball_radius": float(config.get("ball_radius", 0.5)),
"ball_color": [float(config.get("ball_color_r", 0.9)),
float(config.get("ball_color_g", 0.2)),
float(config.get("ball_color_b", 0.2))],
"box_color": [float(config.get("box_color_r", 0.8)),
float(config.get("box_color_g", 0.8)),
float(config.get("box_color_b", 0.85))],
"use_marker": int(config.get("use_marker", 0)),
"camera_distance": float(config.get("camera_distance", 40.0)),
"camera_elevation": float(config.get("camera_elevation", 0)),
"camera_azimuth": float(config.get("camera_azimuth", 0)),
} }
param_path = os.path.join(script_dir, "engines", engine, "param.json") param_path = os.path.join(script_dir, "engines", engine, "param.json")
os.makedirs(os.path.dirname(param_path), exist_ok=True) os.makedirs(os.path.dirname(param_path), exist_ok=True)
@@ -711,9 +999,25 @@ def run_engine(engine, input_dir, output_dir, config):
print(f"[compute] input: {input_dir}") print(f"[compute] input: {input_dir}")
print(f"[compute] output: {output_dir}") print(f"[compute] output: {output_dir}")
# ── 预校准:跑 NT=1000 步测速 ──────────────────────────────── # ── 预校准:跑少量步测速(结果缓存,避免每次重跑)────────────
total_steps = int(config["NT"]) - int(config.get("warmup_steps", 0)) total_steps = int(config["NT"]) - int(config.get("warmup_steps", 0))
_calib_nt = min(1000, max(100, total_steps // 10)) _calib_nt = min(1000, max(100, total_steps // 10))
n_atoms_calib = len(ATOM_IDS) if ATOM_IDS is not None else 0
# 尝试读取缓存;当 n_atoms 相同且 NT 在 50% 范围内时视为有效
_cache_path = os.path.join(script_dir, "engines", engine, "_calib_cache.json")
_step_time = None
try:
with open(_cache_path, encoding="utf-8") as _cf:
_cache = json.load(_cf)
if (_cache.get("n_atoms") == n_atoms_calib and
abs(_cache.get("nt", 0) - total_steps) / max(total_steps, 1) < 0.5):
_step_time = float(_cache["step_time"])
print(f"[compute] 使用校准缓存: {_step_time*1e6:.2f} μs/步")
except Exception:
pass
if _step_time is None:
_calib_param = dict(param_json) _calib_param = dict(param_json)
_calib_param["NT"] = _calib_nt _calib_param["NT"] = _calib_nt
_calib_path = os.path.join(script_dir, "engines", engine, "_calib.json") _calib_path = os.path.join(script_dir, "engines", engine, "_calib.json")
@@ -735,7 +1039,15 @@ def run_engine(engine, input_dir, output_dir, config):
_calib_elapsed = max(time.time() - _ct0, 0.001) _calib_elapsed = max(time.time() - _ct0, 0.001)
_overhead = _calib_elapsed * 0.15 _overhead = _calib_elapsed * 0.15
_step_time = max(_calib_elapsed - _overhead, 0.0001) / _calib_nt _step_time = max(_calib_elapsed - _overhead, 0.0001) / _calib_nt
_est_total = max(_calib_elapsed, _overhead + _step_time * total_steps) # 保存缓存
try:
with open(_cache_path, "w", encoding="utf-8") as _cf:
json.dump({"n_atoms": n_atoms_calib, "nt": total_steps,
"step_time": _step_time}, _cf)
except OSError:
pass
_est_total = _step_time * total_steps
t_start = time.time() t_start = time.time()
t_start_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") t_start_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -760,17 +1072,28 @@ def run_engine(engine, input_dir, output_dir, config):
_line = _line.strip() _line = _line.strip()
if _line: if _line:
_engine_lines.append(_line) _engine_lines.append(_line)
# 读取外部引擎真实进度:格式 "[xxx-engine] progress: N/total"
_prog_match = re.search(r'progress:\s*(\d+)/(\d+)', _line)
if _pbar is not None and _prog_match:
_prog_done = int(_prog_match.group(1))
_prog_total = int(_prog_match.group(2))
if _prog_total > 0:
_pbar.n = min(_prog_done, total_steps)
_pbar.refresh()
if _p.poll() is not None: if _p.poll() is not None:
if _p.stdout: if _p.stdout:
for _r in _p.stdout: for _r in _p.stdout:
_r = _r.strip() _r = _r.strip()
if _r: if _r:
_engine_lines.append(_r) _engine_lines.append(_r)
# 读取残留在管道中的进度消息,避免 20%→100% 跳变
_prog_match = re.search(r'progress:\s*(\d+)/(\d+)', _r)
if _pbar is not None and _prog_match:
_p_done = min(int(_prog_match.group(1)), total_steps)
_pbar.n = max(_pbar.n, _p_done)
if _pbar is not None: _pbar.refresh()
break break
if _pbar is not None and _est_total > 0: time.sleep(0.05)
_pbar.n = int(min((time.time() - t_start) / _est_total, 0.99) * total_steps)
_pbar.refresh()
time.sleep(0.2)
finally: finally:
if _pbar is not None: if _pbar is not None:
_pbar.n = total_steps _pbar.n = total_steps
@@ -801,7 +1124,32 @@ def run_engine(engine, input_dir, output_dir, config):
except OSError: except OSError:
pass pass
# 加载输出的 trajectory.txt # 加载输出的 trajectory.txt / display.txt
save_traj = int(config.get("save_trajectory", 0))
if not save_traj:
# save_trajectory=0:引擎只写 display.txt
disp_path = os.path.join(os.path.abspath(output_dir), "display.txt")
if not os.path.exists(disp_path):
raise FileNotFoundError(f"引擎未生成 display.txt: {disp_path}")
print(f"[compute] 引擎已生成 {disp_path}")
# 将 display.txt 转换为 display.npz(加快后续 draw.py / plot_wave.py 加载)
_npz_path = disp_path.replace("display.txt", "display.npz")
try:
_d = load_display_txt(disp_path)
save_display_npz(
_npz_path,
_d["frames_x"], _d["frames_y"], _d["frames_z"],
_d["frames_vx"], _d["frames_vy"], _d["frames_vz"],
_d["atom_ids"],
header_fields={**_d["header_fields"],
"number_of_frames": str(_d["n_total_frames"]),
"number_of_particles": str(_d["n_total_particles"])})
print(f"[compute] display.npz 已生成: {_npz_path}")
except Exception as _e:
print(f"[compute] 警告: display.npz 生成失败({_e}),将使用 display.txt")
return None, None, None, None, None, None
# save_trajectory=1:加载完整 trajectory.txt
traj_path = os.path.join(os.path.abspath(output_dir), "trajectory.txt") traj_path = os.path.join(os.path.abspath(output_dir), "trajectory.txt")
if not os.path.exists(traj_path): if not os.path.exists(traj_path):
raise FileNotFoundError(f"引擎未生成 trajectory.txt: {traj_path}") raise FileNotFoundError(f"引擎未生成 trajectory.txt: {traj_path}")
@@ -1020,30 +1368,28 @@ def compute_force(x, y, z, vx, vy, vz, m, g, b):
fz -= b[2] * vz fz -= b[2] * vz
if ELASTIC_FORCE and BOND_PAIRS is not None and len(BOND_PAIRS) > 0: if ELASTIC_FORCE and BOND_PAIRS is not None and len(BOND_PAIRS) > 0:
for bond_idx, (idx_1, idx_2) in enumerate(BOND_PAIRS): idx_1 = BOND_PAIRS[:, 0]
idx_2 = BOND_PAIRS[:, 1]
dx = x[idx_2] - x[idx_1] dx = x[idx_2] - x[idx_1]
dy = y[idx_2] - y[idx_1] dy = y[idx_2] - y[idx_1]
dz = z[idx_2] - z[idx_1] dz = z[idx_2] - z[idx_1]
dist = np.sqrt(dx * dx + dy * dy + dz * dz) dist = np.sqrt(dx * dx + dy * dy + dz * dz)
if dist <= 1e-12: valid = dist > 1e-12
continue if np.any(valid):
force_scale = np.zeros_like(dist)
stretch = dist[valid] - BOND_REST_LENGTHS[valid]
force_scale[valid] = BOND_STIFFNESS[valid] * stretch / dist[valid]
stretch = dist - BOND_REST_LENGTHS[bond_idx] fx_bond = force_scale * dx
force_mag = BOND_STIFFNESS[bond_idx] * stretch fy_bond = force_scale * dy
ux = dx / dist fz_bond = force_scale * dz
uy = dy / dist
uz = dz / dist
fx_bond = force_mag * ux np.add.at(fx, idx_1, fx_bond)
fy_bond = force_mag * uy np.add.at(fx, idx_2, -fx_bond)
fz_bond = force_mag * uz np.add.at(fy, idx_1, fy_bond)
np.add.at(fy, idx_2, -fy_bond)
fx[idx_1] += fx_bond np.add.at(fz, idx_1, fz_bond)
fy[idx_1] += fy_bond np.add.at(fz, idx_2, -fz_bond)
fz[idx_1] += fz_bond
fx[idx_2] -= fx_bond
fy[idx_2] -= fy_bond
fz[idx_2] -= fz_bond
if GRAVITY_INTERACTION: if GRAVITY_INTERACTION:
n = len(m) n = len(m)
@@ -1073,6 +1419,50 @@ def compute_acceleration(x, y, z, vx, vy, vz, m, g, b):
return fx / m, fy / m, fz / m return fx / m, fy / m, fz / m
def compute_accel_conservative(x, y, z, m, g):
"""保守力加速度(弹簧 + 重力 + 原子间引力),不含阻尼。
供真蛙跳法使用:阻尼项由 leapfrog_staggered_step 半隐式处理。
"""
_v0 = np.zeros_like(x)
_b0 = np.zeros(3)
fx, fy, fz = compute_force(x, y, z, _v0, _v0, _v0, m, g, _b0)
return fx / m, fy / m, fz / m
def leapfrog_staggered_step(x, y, z, vx_h, vy_h, vz_h, dt, m, g, b):
"""真蛙跳一步:x(t), v(t-dt/2) → x(t+dt), v(t+dt/2)
- 无阻尼 (DAMPING_FORCE=False 或 b≈0)
纯保守蛙跳,每步仅 1 次力计算,辛积分器。
v(t+dt/2) = v(t-dt/2) + a_c(t)·dt
- 有阻尼 (DAMPING_FORCE=True 且 b≠0)
半隐式处理阻尼,仍只 1 次力计算,对任意阻尼无条件稳定。
利用 v(t) ≈ [v(t-dt/2) + v(t+dt/2)] / 2 解析求解:
v(t+dt/2) = [v(t-dt/2)·(1 - α) + a_c(t)·dt] / (1 + α)
其中 α = b·dt / (2·m),每个方向独立。
"""
ax_c, ay_c, az_c = compute_accel_conservative(x, y, z, m, g)
has_damping = DAMPING_FORCE and np.any(b != 0)
if has_damping:
alpha_x = b[0] * dt / (2.0 * m)
alpha_y = b[1] * dt / (2.0 * m)
alpha_z = b[2] * dt / (2.0 * m)
vx_h_new = (vx_h * (1.0 - alpha_x) + ax_c * dt) / (1.0 + alpha_x)
vy_h_new = (vy_h * (1.0 - alpha_y) + ay_c * dt) / (1.0 + alpha_y)
vz_h_new = (vz_h * (1.0 - alpha_z) + az_c * dt) / (1.0 + alpha_z)
else:
vx_h_new = vx_h + ax_c * dt
vy_h_new = vy_h + ay_c * dt
vz_h_new = vz_h + az_c * dt
x_new = x + vx_h_new * dt
y_new = y + vy_h_new * dt
z_new = z + vz_h_new * dt
return x_new, y_new, z_new, vx_h_new, vy_h_new, vz_h_new
def Explicit_Euler_Method(x, y, z, vx, vy, vz, dt, m, g, b): def Explicit_Euler_Method(x, y, z, vx, vy, vz, dt, m, g, b):
ax, ay, az = compute_acceleration(x, y, z, vx, vy, vz, m, g, b) ax, ay, az = compute_acceleration(x, y, z, vx, vy, vz, m, g, b)
x = x + vx * dt x = x + vx * dt
@@ -1176,15 +1566,16 @@ def apply_motion_update(x, y, z, vx, vy, vz, dt, m, g, b):
def apply_fixed_constraints(x, y, z, vx, vy, vz): def apply_fixed_constraints(x, y, z, vx, vy, vz):
"""Keep fixed degrees of freedom at their initial coordinate with zero speed.""" """Keep fixed degrees of freedom at their initial coordinate with zero speed."""
fixed = ATOM_FIXED != 0 if FIXED_MASK_X is not None and np.any(FIXED_MASK_X):
positions = np.column_stack((x, y, z)) x[FIXED_MASK_X] = ATOM_POSITIONS[FIXED_MASK_X, 0]
velocities = np.column_stack((vx, vy, vz)) vx[FIXED_MASK_X] = 0.0
positions = np.where(fixed, ATOM_POSITIONS, positions) if FIXED_MASK_Y is not None and np.any(FIXED_MASK_Y):
velocities = np.where(fixed, 0.0, velocities) y[FIXED_MASK_Y] = ATOM_POSITIONS[FIXED_MASK_Y, 1]
return ( vy[FIXED_MASK_Y] = 0.0
positions[:, 0], positions[:, 1], positions[:, 2], if FIXED_MASK_Z is not None and np.any(FIXED_MASK_Z):
velocities[:, 0], velocities[:, 1], velocities[:, 2], z[FIXED_MASK_Z] = ATOM_POSITIONS[FIXED_MASK_Z, 2]
) vz[FIXED_MASK_Z] = 0.0
return x, y, z, vx, vy, vz
def wrap_position(x, y, z): def wrap_position(x, y, z):
"""边界回绕( dynamics 模式)。""" """边界回绕( dynamics 模式)。"""
@@ -1201,12 +1592,13 @@ def wrap_position(x, y, z):
# 主计算流程 # 主计算流程
# =========================================================================== # ===========================================================================
def run_simulation(): def run_simulation(save_trajectory=0):
"""计算 NT 步轨迹,返回位置/速度数组 """计算 NT 步轨迹,直接抽帧并保存 display.txt
步骤控制: 步骤控制:
- warmup_steps: 预热步数,跳过不记录(用于稳定初始状态) - warmup_steps: 预热步数,跳过不记录(用于稳定初始状态)
- 实际记录步数 = NT - warmup_steps - 按 NSTEP 抽帧保存到 display.txt(新格式)
- save_trajectory=1 时额外保存完整 trajectory.txtJSON 旧格式)
""" """
# 预热阶段 # 预热阶段
x = ATOM_POSITIONS[:, 0].copy() x = ATOM_POSITIONS[:, 0].copy()
@@ -1216,14 +1608,26 @@ def run_simulation():
vy = ATOM_VELOCITIES[:, 1].copy() vy = ATOM_VELOCITIES[:, 1].copy()
vz = ATOM_VELOCITIES[:, 2].copy() vz = ATOM_VELOCITIES[:, 2].copy()
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz) x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
# 初始时刻驱动力(t=0 时原子 1 的位置由驱动力决定而非 coord.txt)
x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, 0.0, 0, DRIVER_DATA, DT) x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, 0.0, 0, DRIVER_DATA, DT)
# 真蛙跳初始化:从 v(0) 反推 v(-dt/2),使后续每步只需 1 次力计算。
# 其他方法(euler/midpointvx/vy/vz 始终存整步速度,不受影响。
if METHOD == "leapfrog":
_ax0, _ay0, _az0 = compute_accel_conservative(x, y, z, ATOM_MASSES, G)
vx = vx - 0.5 * _ax0 * DT
vy = vy - 0.5 * _ay0 * DT
vz = vz - 0.5 * _az0 * DT
# 从此 vx/vy/vz 存 v(t-dt/2)(蛙跳半步速度)
if warmup_steps is not None and warmup_steps > 0: if warmup_steps is not None and warmup_steps > 0:
print(f"[compute] 预热阶段: 前 {warmup_steps} 步不记录") print(f"[compute] 预热阶段: 前 {warmup_steps} 步不记录")
for step in trange(warmup_steps, desc="[compute] 预热"): for step in trange(warmup_steps, desc="[compute] 预热"):
t = (step + 1) * DT t = (step + 1) * DT
x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, t, step, DRIVER_DATA, DT) x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, t, step, DRIVER_DATA, DT)
if METHOD == "leapfrog":
x, y, z, vx, vy, vz = leapfrog_staggered_step(
x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
else:
x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B) x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
x, y, z = wrap_position(x, y, z) x, y, z = wrap_position(x, y, z)
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz) x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
@@ -1232,9 +1636,20 @@ def run_simulation():
f"({x[PLOT_ATOM_ROW]:.4f}, {y[PLOT_ATOM_ROW]:.4f}, {z[PLOT_ATOM_ROW]:.4f})" f"({x[PLOT_ATOM_ROW]:.4f}, {y[PLOT_ATOM_ROW]:.4f}, {z[PLOT_ATOM_ROW]:.4f})"
) )
# 记录阶段 # 记录阶段 - 按 NSTEP 抽帧保存
record_steps = NT - (warmup_steps or 0) record_steps = NT - (warmup_steps or 0)
n_atoms = len(ATOM_IDS) n_atoms = len(ATOM_IDS)
n_frames = (record_steps + NSTEP - 1) // NSTEP
# 按 NSTEP 抽帧的临时缓冲区(远小于全量轨迹)
sampled_x = np.zeros((n_frames, n_atoms), dtype=np.float64)
sampled_y = np.zeros((n_frames, n_atoms), dtype=np.float64)
sampled_z = np.zeros((n_frames, n_atoms), dtype=np.float64)
sampled_vx = np.zeros((n_frames, n_atoms), dtype=np.float64)
sampled_vy = np.zeros((n_frames, n_atoms), dtype=np.float64)
sampled_vz = np.zeros((n_frames, n_atoms), dtype=np.float64)
# 如果 save_trajectory,准备完整轨迹缓冲区
if save_trajectory:
traj_x = np.zeros((record_steps, n_atoms), dtype=np.float64) traj_x = np.zeros((record_steps, n_atoms), dtype=np.float64)
traj_y = np.zeros((record_steps, n_atoms), dtype=np.float64) traj_y = np.zeros((record_steps, n_atoms), dtype=np.float64)
traj_z = np.zeros((record_steps, n_atoms), dtype=np.float64) traj_z = np.zeros((record_steps, n_atoms), dtype=np.float64)
@@ -1244,9 +1659,9 @@ def run_simulation():
for step in trange(record_steps, desc="[compute] 计算中"): for step in trange(record_steps, desc="[compute] 计算中"):
t = (step + (warmup_steps or 0)) * DT t = (step + (warmup_steps or 0)) * DT
# 先施加驱动力(受驱原子的位置覆盖初始/固定约束,为弹簧力提供正确参考)
x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, t, step, DRIVER_DATA, DT) x, y, z, vx, vy, vz = apply_driving_force(x, y, z, vx, vy, vz, t, step, DRIVER_DATA, DT)
if save_trajectory:
traj_x[step] = x traj_x[step] = x
traj_y[step] = y traj_y[step] = y
traj_z[step] = z traj_z[step] = z
@@ -1254,11 +1669,120 @@ def run_simulation():
traj_vy[step] = vy traj_vy[step] = vy
traj_vz[step] = vz traj_vz[step] = vz
# 抽帧:NSTEP 间隔保存
if step % NSTEP == 0:
fi = step // NSTEP
sampled_x[fi] = x
sampled_y[fi] = y
sampled_z[fi] = z
sampled_vx[fi] = vx
sampled_vy[fi] = vy
sampled_vz[fi] = vz
if METHOD == "leapfrog":
x, y, z, vx, vy, vz = leapfrog_staggered_step(
x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
else:
x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B) x, y, z, vx, vy, vz = apply_motion_update(x, y, z, vx, vy, vz, DT, ATOM_MASSES, G, B)
x, y, z = wrap_position(x, y, z) x, y, z = wrap_position(x, y, z)
x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz) x, y, z, vx, vy, vz = apply_fixed_constraints(x, y, z, vx, vy, vz)
return traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz # 写入 display.txt(新格式)
output_dir = get_output_dir()
disp_path = os.path.join(output_dir, "display.txt")
n_frames_actual = (record_steps + NSTEP - 1) // NSTEP
save_display_txt(
disp_path,
sampled_x[:n_frames_actual], sampled_y[:n_frames_actual], sampled_z[:n_frames_actual],
sampled_vx[:n_frames_actual], sampled_vy[:n_frames_actual], sampled_vz[:n_frames_actual],
np.array(ATOM_IDS), record_steps, n_atoms,
header_fields={"DT": str(DT), "NSTEP": str(NSTEP), "method": str(METHOD),
"warmup_steps": str(warmup_steps or 0),
"dynamic_steps": str(record_steps),
"T_total": str(NT * DT),
"X_MAX": str(X_MAX), "X_MIN": str(X_MIN),
"Y_MAX": str(Y_MAX), "Y_MIN": str(Y_MIN),
"Z_MAX": str(Z_MAX), "Z_MIN": str(Z_MIN),
"ball_radius": str(ball_radius),
"ball_color_r": str(ball_color_r),
"ball_color_g": str(ball_color_g),
"ball_color_b": str(ball_color_b),
"box_color_r": str(box_color_r),
"box_color_g": str(box_color_g),
"box_color_b": str(box_color_b),
"gravity_field": str(GRAVITY_FIELD),
"gravity_interaction": str(GRAVITY_INTERACTION),
"elastic_force": str(ELASTIC_FORCE),
"damping_force": str(DAMPING_FORCE),
"gravity_strength": str(GRAVITY_STRENGTH),
"driving_force": str(DRIVING_FORCE),
"use_marker": str(use_marker),
"alpha": ",".join(str(a) for a in (alpha if isinstance(alpha, list) else [alpha])),
"atom_masses": json.dumps([float(v) for v in ATOM_MASSES]),
"atom_positions": json.dumps(ATOM_POSITIONS.tolist()),
"bond_pairs": json.dumps(BOND_PAIRS.tolist() if BOND_PAIRS is not None else []),
"bond_stiffness": json.dumps(BOND_STIFFNESS.tolist() if BOND_STIFFNESS is not None else []),
"bond_rest_lengths": json.dumps(BOND_REST_LENGTHS.tolist() if BOND_REST_LENGTHS is not None else []),
"G": json.dumps(G.tolist() if G is not None else [0.0, 0.0, 0.0]),
"atom_radii": ",".join(str(r) for r in ATOM_RADII),
"camera_distance": str(camera_distance),
"camera_elevation": str(camera_elevation),
"camera_azimuth": str(camera_azimuth),
"camera_keyframes": str(camera_keyframes_raw)}
)
print(f"[compute] display.txt 已保存至: {disp_path} ({n_frames_actual} 帧)")
# 同时保存二进制 display.npzI/O 速度提升 5-10x
_npz_path = os.path.join(output_dir, "display.npz")
_hdr = {"DT": str(DT), "NSTEP": str(NSTEP), "method": str(METHOD),
"warmup_steps": str(warmup_steps or 0),
"dynamic_steps": str(record_steps),
"T_total": str(NT * DT),
"X_MAX": str(X_MAX), "X_MIN": str(X_MIN),
"Y_MAX": str(Y_MAX), "Y_MIN": str(Y_MIN),
"Z_MAX": str(Z_MAX), "Z_MIN": str(Z_MIN),
"ball_radius": str(ball_radius),
"ball_color_r": str(ball_color_r),
"ball_color_g": str(ball_color_g),
"ball_color_b": str(ball_color_b),
"box_color_r": str(box_color_r),
"box_color_g": str(box_color_g),
"box_color_b": str(box_color_b),
"gravity_field": str(GRAVITY_FIELD),
"gravity_interaction": str(GRAVITY_INTERACTION),
"elastic_force": str(ELASTIC_FORCE),
"damping_force": str(DAMPING_FORCE),
"gravity_strength": str(GRAVITY_STRENGTH),
"driving_force": str(DRIVING_FORCE),
"use_marker": str(use_marker),
"alpha": ",".join(str(a) for a in (alpha if isinstance(alpha, list) else [alpha])),
"atom_masses": json.dumps([float(v) for v in ATOM_MASSES]),
"atom_positions": json.dumps(ATOM_POSITIONS.tolist()),
"bond_pairs": json.dumps(BOND_PAIRS.tolist() if BOND_PAIRS is not None else []),
"bond_stiffness": json.dumps(BOND_STIFFNESS.tolist() if BOND_STIFFNESS is not None else []),
"bond_rest_lengths": json.dumps(BOND_REST_LENGTHS.tolist() if BOND_REST_LENGTHS is not None else []),
"G": json.dumps(G.tolist() if G is not None else [0.0, 0.0, 0.0]),
"atom_radii": ",".join(str(r) for r in ATOM_RADII),
"camera_distance": str(camera_distance),
"camera_elevation": str(camera_elevation),
"camera_azimuth": str(camera_azimuth),
"camera_keyframes": str(camera_keyframes_raw),
"number_of_frames": str(record_steps),
"number_of_particles": str(n_atoms)}
save_display_npz(_npz_path,
sampled_x[:n_frames_actual], sampled_y[:n_frames_actual],
sampled_z[:n_frames_actual],
sampled_vx[:n_frames_actual], sampled_vy[:n_frames_actual],
sampled_vz[:n_frames_actual],
np.array(ATOM_IDS), header_fields=_hdr)
print(f"[compute] display.npz 已保存至: {_npz_path}")
# 可选:保存完整 trajectory.txt
if save_trajectory:
save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz)
print(f"[compute] trajectory.txt 已保存(完整轨迹)")
return sampled_x[:n_frames_actual], sampled_y[:n_frames_actual], sampled_z[:n_frames_actual], \
sampled_vx[:n_frames_actual], sampled_vy[:n_frames_actual], sampled_vz[:n_frames_actual]
def save_trajectory_table_txt(txt_path, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, NT, DT): def save_trajectory_table_txt(txt_path, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, NT, DT):
@@ -1301,10 +1825,13 @@ def main():
output_dir = get_output_dir(script_dir) output_dir = get_output_dir(script_dir)
print(f"[compute] 开始计算 NT={NT} DT={DT} ") print(f"[compute] 开始计算 NT={NT} DT={DT} ")
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = run_simulation() traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = run_simulation(save_trajectory=0)
print(f"[compute] 计算完成,共 {NT}") print(f"[compute] 计算完成,共 {NT}")
print(f"[compute] display.txt 已在 run_simulation 中保存")
save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, script_dir) # 如果需要完整轨迹,以上传 save_trajectory=1 重新运行
# 以下旧函数保留兼容但不再自动调用
# save_trajectory_txt(...)
# 同时保存为逐行表格,便于直接查看 # 同时保存为逐行表格,便于直接查看
txt_path = os.path.join(output_dir, "trajectory_table.txt") txt_path = os.path.join(output_dir, "trajectory_table.txt")
+143 -60
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,88 +30,99 @@ 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)
h = disp_data["header_fields"]
# 原子数据plot_atom:用于信息显示) # 原子数据
DISP_X = disp_data["disp_x"] DISP_ALL_X = disp_data["frames_x"] # (n_frames, n_atoms)
DISP_Y = disp_data["disp_y"] DISP_ALL_Y = disp_data["frames_y"]
DISP_Z = disp_data["disp_z"] DISP_ALL_Z = disp_data["frames_z"]
DISP_VX = disp_data["disp_vx"] DISP_ALL_VX = disp_data["frames_vx"]
DISP_VY = disp_data["disp_vy"] DISP_ALL_VY = disp_data["frames_vy"]
DISP_VZ = disp_data["disp_vz"] DISP_ALL_VZ = disp_data["frames_vz"]
# 全原子数据(用于多球绘制 # 第一个原子的轨迹(用于信息显示
DISP_ALL_X = disp_data["disp_all_x"] # (n_frames, n_atoms) DISP_X = DISP_ALL_X[:, 0]
DISP_ALL_Y = disp_data["disp_all_y"] DISP_Y = DISP_ALL_Y[:, 0]
DISP_ALL_Z = disp_data["disp_all_z"] DISP_Z = DISP_ALL_Z[:, 0]
DISP_ALL_VX = disp_data["disp_all_vx"] DISP_VX = DISP_ALL_VX[:, 0]
DISP_ALL_VY = disp_data["disp_all_vy"] DISP_VY = DISP_ALL_VY[:, 0]
DISP_ALL_VZ = disp_data["disp_all_vz"] DISP_VZ = DISP_ALL_VZ[:, 0]
DISP_T = disp_data["disp_t"] N_FRAMES = DISP_ALL_X.shape[0]
DISP_STEP = disp_data["disp_step"] NT = int(disp_data["n_total_frames"])
N_FRAMES = int(disp_data["n_frames"]) N_ATOMS = int(disp_data["n_total_particles"])
NT = int(disp_data["NT"]) DT = float(h.get("DT", 0.001))
DT = float(disp_data["DT"]) NSTEP = int(h.get("NSTEP", 1))
NSTEP = int(disp_data["NSTEP"]) 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 = h.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(h.get("ball_radius", 0.5)))
PLOT_ATOM_ROW = 0
PLOT_ATOM_ID = int(ATOM_IDS[0])
BOND_PAIRS = [] # display 格式不含成键信息,从原始数据加载
# 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵) # 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵)
USE_MARKER = int(disp_data.get("use_marker", 0)) USE_MARKER = int(h.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 中读取 # 保留模拟边界常量(用于场景缩放、相机等),从 output/display.txt 中读取
X_MIN = float(disp_data["X_MIN"]); X_MAX = float(disp_data["X_MAX"]) X_MIN = float(h.get("X_MIN", -10)); X_MAX = float(h.get("X_MAX", 10))
Y_MIN = float(disp_data["Y_MIN"]); Y_MAX = float(disp_data["Y_MAX"]) Y_MIN = float(h.get("Y_MIN", -10)); Y_MAX = float(h.get("Y_MAX", 10))
Z_MIN = float(disp_data["Z_MIN"]); Z_MAX = float(disp_data["Z_MAX"]) Z_MIN = float(h.get("Z_MIN", -10)); Z_MAX = float(h.get("Z_MAX", 10))
X0 = float(disp_data["X0"]); Y0 = float(disp_data["Y0"]); Z0 = float(disp_data["Z0"]) raw_alpha = h.get("alpha", "0.2")
raw_alpha = disp_data["alpha"] try:
if isinstance(raw_alpha, (list, tuple, np.ndarray)): alpha_list = [float(x) for x in raw_alpha.split(",")]
alpha_list = [float(a) for a in raw_alpha]
if len(alpha_list) != 6: if len(alpha_list) != 6:
raise ValueError(f"alpha 数组长度须为 6,实际为 {len(alpha_list)}") alpha_list = alpha_list * 6
else: except (ValueError, AttributeError):
alpha_list = [float(raw_alpha)] * 6 alpha_list = [float(raw_alpha)] * 6
# 绘图参数 # 绘图参数
ball_radius = float(disp_data["ball_radius"]) ball_radius = float(h.get("ball_radius", 0.5))
ball_color_r = float(disp_data["ball_color_r"]) ball_color_r = float(h.get("ball_color_r", 0.9))
ball_color_g = float(disp_data["ball_color_g"]) ball_color_g = float(h.get("ball_color_g", 0.2))
ball_color_b = float(disp_data["ball_color_b"]) ball_color_b = float(h.get("ball_color_b", 0.2))
box_color_r = float(disp_data["box_color_r"]) box_color_r = float(h.get("box_color_r", 0.8))
box_color_g = float(disp_data["box_color_g"]) box_color_g = float(h.get("box_color_g", 0.8))
box_color_b = float(disp_data["box_color_b"]) box_color_b = float(h.get("box_color_b", 0.85))
# =========================================================================== # ===========================================================================
# 图形界面无关的几何参数(不参与物理计算,仅用于场景外观) # 图形界面无关的几何参数(不参与物理计算,仅用于场景外观)
# =========================================================================== # ===========================================================================
info_margin = 36 info_margin = 8
axis_length = 10.0 axis_length = 10.0
initial_camera = { initial_camera = {
"distance": 40.0, "distance": float(h.get("camera_distance", 40.0)),
"elevation": 0, "elevation": float(h.get("camera_elevation", 0)),
"azimuth": 0, "azimuth": float(h.get("camera_azimuth", 0)),
"center": (0, 0, 0), "center": (0, 0, 0),
} }
@@ -255,7 +266,7 @@ camera_info = scene.visuals.Text(
# 左上角:小球信息 # 左上角:小球信息
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=18,
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)
@@ -508,8 +519,9 @@ 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]
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 +561,77 @@ 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 缓存
_CAM_MOTION = _load_move_camera_txt()
if not _CAM_MOTION:
_CAM_MOTION = json.loads(h.get("camera_keyframes", "null")) if h.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
# 找当前帧所有活动的段(时间交叠=同时作用),按文件顺序依次应用
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]
# rz 预留
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 +643,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])
+129 -253
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()
@@ -193,273 +235,107 @@ 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())
# 外部引擎写完整 trajectory.txt,后续抽帧
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine( traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine(
engine, input_dir_abs, output_dir_abs, config) engine, input_dir_abs, output_dir_abs, config)
if int(config.get("save_trajectory", 0)):
compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base)) compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base))
_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}")
+191 -49
View File
@@ -38,6 +38,13 @@ typedef struct {
int damping_force; /* 阻尼开关 */ int damping_force; /* 阻尼开关 */
double gravity_strength; /* 万有引力强度 */ double gravity_strength; /* 万有引力强度 */
int driving_force; /* 驱动力开关 */ int driving_force; /* 驱动力开关 */
int save_trajectory; /* 是否保存完整轨迹文件 */
double alpha[6]; /* 盒子透明度 */
double ball_radius;
double ball_color[3];
double box_color[3];
int use_marker;
double camera_distance, camera_elevation, camera_azimuth;
} SimParams; } SimParams;
/* ======================================================================== /* ========================================================================
@@ -270,6 +277,20 @@ static void json_read_double3(const char *json, const char *key, double out[3])
} }
} }
static void json_read_double6(const char *json, const char *key, double out[6]) {
char search[256];
snprintf(search, sizeof(search), "\"%s\"", key);
const char *p = strstr(json, search);
if (!p) { for (int i=0;i<6;i++) out[i]=0; return; }
p = strchr(p, '[');
if (!p) { for (int i=0;i<6;i++) out[i]=0; return; }
p++;
for (int i = 0; i < 6; i++) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == ',' || *p == ']') p++;
out[i] = strtod(p, (char**)&p);
}
}
/* 读取 param.json */ /* 读取 param.json */
static int g_gravity_field = 1; static int g_gravity_field = 1;
static int g_gravity_interaction = 0; static int g_gravity_interaction = 0;
@@ -304,6 +325,16 @@ static SimParams read_params(const char *path) {
p.damping_force = json_read_int(buf, "damping_force"); p.damping_force = json_read_int(buf, "damping_force");
p.gravity_strength = json_read_double(buf, "gravity_strength"); p.gravity_strength = json_read_double(buf, "gravity_strength");
p.driving_force = json_read_int(buf, "driving_force"); p.driving_force = json_read_int(buf, "driving_force");
p.save_trajectory = json_read_int(buf, "save_trajectory");
/* 渲染参数 */
json_read_double6(buf, "alpha", p.alpha);
p.ball_radius = json_read_double(buf, "ball_radius");
json_read_double3(buf, "ball_color", p.ball_color);
json_read_double3(buf, "box_color", p.box_color);
p.use_marker = json_read_int(buf, "use_marker");
p.camera_distance = json_read_double(buf, "camera_distance");
p.camera_elevation = json_read_double(buf, "camera_elevation");
p.camera_azimuth = json_read_double(buf, "camera_azimuth");
g_gravity_field = p.gravity_field; g_gravity_field = p.gravity_field;
g_gravity_interaction = p.gravity_interaction; g_gravity_interaction = p.gravity_interaction;
g_elastic_force = p.elastic_force; g_elastic_force = p.elastic_force;
@@ -389,6 +420,7 @@ static BondData read_bonds(const char *input_dir, const AtomData *atoms) {
char bond_name[256]; char bond_name[256];
while (fscanf(f, "%d %d %s", &tmp_a, &tmp_b, bond_name) == 3) n_lines++; while (fscanf(f, "%d %d %s", &tmp_a, &tmp_b, bond_name) == 3) n_lines++;
rewind(f); rewind(f);
fgets(line, sizeof(line), f); // 再次跳过表头
if (n_lines == 0) { fclose(f); return b; } if (n_lines == 0) { fclose(f); return b; }
@@ -501,6 +533,20 @@ static void compute_acceleration(
} }
} }
/* 保守力加速度(不含阻尼),供真蛙跳法专用。
通过传入零速度调用 compute_acceleration,阻尼项 -B*v/m 自动为零。 */
static void compute_accel_conservative(
int n, const double *x, const double *y, const double *z,
const double *m, const double G[3],
const BondData *bonds,
double *ax, double *ay, double *az)
{
double *v0 = (double*)alloca(n * sizeof(double));
for (int i = 0; i < n; i++) v0[i] = 0.0;
double Bzero[3] = {0.0, 0.0, 0.0};
compute_acceleration(n, x, y, z, v0, v0, v0, m, G, Bzero, bonds, ax, ay, az);
}
/* 边界条件:clamp 位置 + 速度反转 ——与 Python Limit_in_box 一致 */ /* 边界条件:clamp 位置 + 速度反转 ——与 Python Limit_in_box 一致 */
static void limit_in_box(double *pos, double *vel, double lo, double hi) { static void limit_in_box(double *pos, double *vel, double lo, double hi) {
if (*pos > hi) { *pos = hi; *vel = -*vel; } if (*pos > hi) { *pos = hi; *vel = -*vel; }
@@ -618,6 +664,15 @@ static void midpoint_step(
} }
/* ── 蛙跳法(Velocity-Verlet)── */ /* ── 蛙跳法(Velocity-Verlet)── */
/* 真蛙跳一步:x(t), v(t-dt/2) → x(t+dt), v(t+dt/2)
*
* 无阻尼:纯保守蛙跳,每步 1 次力计算,辛积分器。
* v(t+dt/2) = v(t-dt/2) + a_c(t)·dt
*
* 有阻尼:半隐式处理,仍 1 次力计算,对任意阻尼无条件稳定。
* 利用 v(t) ≈ [v(t-dt/2) + v(t+dt/2)] / 2 解析求解:
* v(t+dt/2) = [v(t-dt/2)·(1-α) + a_c(t)·dt] / (1+α),α = B·dt/(2m)
*/
static void leapfrog_step( static void leapfrog_step(
int n, double *x, double *y, double *z, int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz, double *vx, double *vy, double *vz,
@@ -627,47 +682,30 @@ static void leapfrog_step(
double *ax = (double*)alloca(n * sizeof(double)); double *ax = (double*)alloca(n * sizeof(double));
double *ay = (double*)alloca(n * sizeof(double)); double *ay = (double*)alloca(n * sizeof(double));
double *az = (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 */ /* 1 次保守力计算(不含阻尼) */
compute_accel_conservative(n, x, y, z, m, G, bonds, ax, ay, az);
int has_damping = g_damping_force && (B[0] != 0.0 || B[1] != 0.0 || B[2] != 0.0);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue; if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt * 0.5; if (has_damping) {
vy[i] += ay[i] * dt * 0.5; double alphax = B[0] * dt / (2.0 * m[i]);
vz[i] += az[i] * dt * 0.5; double alphay = B[1] * dt / (2.0 * m[i]);
double alphaz = B[2] * dt / (2.0 * m[i]);
vx[i] = (vx[i] * (1.0 - alphax) + ax[i] * dt) / (1.0 + alphax);
vy[i] = (vy[i] * (1.0 - alphay) + ay[i] * dt) / (1.0 + alphay);
vz[i] = (vz[i] * (1.0 - alphaz) + az[i] * dt) / (1.0 + alphaz);
} else {
vx[i] += ax[i] * dt;
vy[i] += ay[i] * dt;
vz[i] += az[i] * dt;
} }
x[i] += vx[i] * dt;
/* 全推位置(不含边界)*/
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; y[i] += vy[i] * dt;
z[i] += vz[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 一致)──────────────── */ /* ── 驱动力(与 Python apply_driving_force 一致)──────────────── */
@@ -789,12 +827,14 @@ static void write_trajectory_json(const char *path, const Trajectory *traj,
const char *names[] = {"traj_x","traj_y","traj_z","traj_vx","traj_vy","traj_vz"}; 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}; double *arrs[] = {traj->x, traj->y, traj->z, traj->vx, traj->vy, traj->vz};
printf("[C-engine] 正在写入轨迹数据…\n");
fflush(stdout);
for (int a = 0; a < 6; a++) { for (int a = 0; a < 6; a++) {
fprintf(f, " \"%s\": [\n", names[a]); fprintf(f, " \"%s\": [\n", names[a]);
for (int t = 0; t < traj->n_steps; t++) { for (int t = 0; t < traj->n_steps; t++) {
fprintf(f, " ["); fprintf(f, " [");
for (int i = 0; i < traj->n_atoms; i++) { for (int i = 0; i < traj->n_atoms; i++) {
fprintf(f, "%.15g", arrs[a][t * traj->n_atoms + i]); fprintf(f, "%.8g", arrs[a][t * traj->n_atoms + i]);
if (i < traj->n_atoms - 1) fputc(',', f); if (i < traj->n_atoms - 1) fputc(',', f);
} }
fprintf(f, "]"); fprintf(f, "]");
@@ -808,12 +848,12 @@ static void write_trajectory_json(const char *path, const Trajectory *traj,
/* 标量参数 */ /* 标量参数 */
fprintf(f, " \"NT\": %d,\n", params->NT); fprintf(f, " \"NT\": %d,\n", params->NT);
fprintf(f, " \"DT\": %.15g,\n", params->DT); fprintf(f, " \"DT\": %.8g,\n", params->DT);
fprintf(f, " \"NSTEP\": %d,\n", params->NSTEP); fprintf(f, " \"NSTEP\": %d,\n", params->NSTEP);
fprintf(f, " \"method\": \"%s\",\n", params->method); fprintf(f, " \"method\": \"%s\",\n", params->method);
fprintf(f, " \"warmup_steps\": %d,\n", params->warmup_steps); 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, " \"G\": [%.8g, %.8g, %.8g],\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, " \"B\": [%.8g, %.8g, %.8g],\n", params->B[0], params->B[1], params->B[2]);
fprintf(f, " \"atom_ids\": ["); fprintf(f, " \"atom_ids\": [");
for (int i = 0; i < atoms->n_atoms; i++) { for (int i = 0; i < atoms->n_atoms; i++) {
@@ -825,7 +865,7 @@ static void write_trajectory_json(const char *path, const Trajectory *traj,
fprintf(f, " \"atom_masses\": ["); fprintf(f, " \"atom_masses\": [");
for (int i = 0; i < atoms->n_atoms; i++) { for (int i = 0; i < atoms->n_atoms; i++) {
if (i > 0) fputc(',', f); if (i > 0) fputc(',', f);
fprintf(f, "%.15g", atoms->masses[i]); fprintf(f, "%.8g", atoms->masses[i]);
} }
fprintf(f, "],\n"); fprintf(f, "],\n");
@@ -839,14 +879,14 @@ static void write_trajectory_json(const char *path, const Trajectory *traj,
fprintf(f, " \"bond_stiffness\": ["); fprintf(f, " \"bond_stiffness\": [");
for (int b = 0; b < bonds->n_bonds; b++) { for (int b = 0; b < bonds->n_bonds; b++) {
if (b > 0) fputc(',', f); if (b > 0) fputc(',', f);
fprintf(f, "%.15g", bonds->stiffness[b]); fprintf(f, "%.8g", bonds->stiffness[b]);
} }
fprintf(f, "],\n"); fprintf(f, "],\n");
fprintf(f, " \"bond_rest_lengths\": ["); fprintf(f, " \"bond_rest_lengths\": [");
for (int b = 0; b < bonds->n_bonds; b++) { for (int b = 0; b < bonds->n_bonds; b++) {
if (b > 0) fputc(',', f); if (b > 0) fputc(',', f);
fprintf(f, "%.15g", bonds->rest_lengths[b]); fprintf(f, "%.8g", bonds->rest_lengths[b]);
} }
fprintf(f, "],\n"); fprintf(f, "],\n");
fprintf(f, " \"driving_force\": %d\n", params->driving_force); fprintf(f, " \"driving_force\": %d\n", params->driving_force);
@@ -855,6 +895,65 @@ static void write_trajectory_json(const char *path, const Trajectory *traj,
fclose(f); fclose(f);
} }
static void write_display_txt(const char *path, const Trajectory *traj,
const SimParams *params, const AtomData *atoms)
{
FILE *f = fopen(path, "w");
if (!f) die("无法写入 display.txt");
int n_frames = traj->n_steps; /* 实际采样帧数,用于下面的帧循环 */
int n_particles = traj->n_atoms;
int dynamic_steps = params->NT - params->warmup_steps;
double T_total = dynamic_steps * params->DT;
/* number of frames 写总积分步数(与 draw.py NT 对应),不是采样帧数 */
fprintf(f, "number of frames: %d\n", dynamic_steps);
fprintf(f, "number of particles: %d\n", n_particles);
fprintf(f, "DT: %.16g\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, "dynamic_steps: %d\n", dynamic_steps);
fprintf(f, "T_total: %.16g\n", T_total);
fprintf(f, "box_a: %.16g\n", params->box_a);
fprintf(f, "alpha: %.16g,%.16g,%.16g,%.16g,%.16g,%.16g\n",
params->alpha[0], params->alpha[1], params->alpha[2],
params->alpha[3], params->alpha[4], params->alpha[5]);
fprintf(f, "ball_radius: %.16g\n", params->ball_radius);
fprintf(f, "ball_color_r: %.16g\n", params->ball_color[0]);
fprintf(f, "ball_color_g: %.16g\n", params->ball_color[1]);
fprintf(f, "ball_color_b: %.16g\n", params->ball_color[2]);
fprintf(f, "box_color_r: %.16g\n", params->box_color[0]);
fprintf(f, "box_color_g: %.16g\n", params->box_color[1]);
fprintf(f, "box_color_b: %.16g\n", params->box_color[2]);
fprintf(f, "use_marker: %d\n", params->use_marker);
fprintf(f, "camera_distance: %.16g\n", params->camera_distance);
fprintf(f, "camera_elevation: %.16g\n", params->camera_elevation);
fprintf(f, "camera_azimuth: %.16g\n", params->camera_azimuth);
fprintf(f, "\n");
if (params->driving_force) {
fprintf(f, "driving_force: 1\n");
} else {
fprintf(f, "driving_force: 0\n");
}
fprintf(f, "\n");
for (int t = 0; t < n_frames; t++) {
fprintf(f, "frame: %3d\n", t + 1);
fprintf(f, "n x y z vx vy vz\n");
for (int i = 0; i < n_particles; i++) {
int idx = t * n_particles + i;
fprintf(f, "%4d %12.6f %12.6f %12.6f %10.6f %10.6f %10.6f\n",
atoms->atom_ids[i],
traj->x[idx], traj->y[idx], traj->z[idx],
traj->vx[idx], traj->vy[idx], traj->vz[idx]);
}
}
fclose(f);
}
// ======================================================================== // ========================================================================
// 主函数 // 主函数
// ======================================================================== // ========================================================================
@@ -899,17 +998,43 @@ int main(int argc, char **argv) {
vz[i] = atoms.vel_0[i*3+2]; vz[i] = atoms.vel_0[i*3+2];
} }
/* 分配轨迹缓冲区:改用 record_steps */ /* 分配轨迹缓冲区 */
int record_steps = params.NT - params.warmup_steps; int record_steps = params.NT - params.warmup_steps;
Trajectory traj; Trajectory traj;
traj.n_steps = record_steps;
traj.n_atoms = n; traj.n_atoms = n;
if (params.save_trajectory) {
traj.n_steps = record_steps;
traj.x = (double*)xmalloc(record_steps * n * sizeof(double) * 6); traj.x = (double*)xmalloc(record_steps * n * sizeof(double) * 6);
traj.y = traj.x + record_steps * n; traj.y = traj.x + record_steps * n;
traj.z = traj.y + record_steps * n; traj.z = traj.y + record_steps * n;
traj.vx = traj.z + record_steps * n; traj.vx = traj.z + record_steps * n;
traj.vy = traj.vx + record_steps * n; traj.vy = traj.vx + record_steps * n;
traj.vz = traj.vy + record_steps * n; traj.vz = traj.vy + record_steps * n;
} else {
int sampled_steps = (record_steps + params.NSTEP - 1) / params.NSTEP;
if (sampled_steps < 1) sampled_steps = 1;
traj.n_steps = sampled_steps;
traj.x = (double*)xmalloc(sampled_steps * n * sizeof(double) * 6);
traj.y = traj.x + sampled_steps * n;
traj.z = traj.y + sampled_steps * n;
traj.vx = traj.z + sampled_steps * n;
traj.vy = traj.vx + sampled_steps * n;
traj.vz = traj.vy + sampled_steps * n;
}
/* 真蛙跳初始化:v(0) 反推 v(-dt/2) = v(0) - 0.5*a_c(0)*dt */
if (strcmp(params.method, "leapfrog") == 0) {
double *ax0 = (double*)alloca(n * sizeof(double));
double *ay0 = (double*)alloca(n * sizeof(double));
double *az0 = (double*)alloca(n * sizeof(double));
compute_accel_conservative(n, x, y, z, atoms.masses, params.G, &bonds, ax0, ay0, az0);
for (int i = 0; i < n; i++) {
if (atoms.fixed[i*3+0] && atoms.fixed[i*3+1] && atoms.fixed[i*3+2]) continue;
vx[i] -= 0.5 * ax0[i] * params.DT;
vy[i] -= 0.5 * ay0[i] * params.DT;
vz[i] -= 0.5 * az0[i] * params.DT;
}
}
/* 预热 */ /* 预热 */
/* 初始时刻 t=0 驱动力(与 Python run_simulation 一致)*/ /* 初始时刻 t=0 驱动力(与 Python run_simulation 一致)*/
@@ -925,16 +1050,28 @@ int main(int argc, char **argv) {
} }
/* 记录 */ /* 记录 */
int _prog_interval = record_steps / 100;
if (_prog_interval < 1) _prog_interval = 1;
int sample_idx = 0;
for (int s = 0; s < record_steps; s++) { for (int s = 0; s < record_steps; s++) {
if (s % _prog_interval == 0 && s > 0) {
printf("[C-engine] progress: %d/%d\n", s, record_steps);
fflush(stdout);
}
double t = (s + params.warmup_steps) * params.DT; 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); if (params.driving_force) apply_driving_force(n, x, y, z, vx, vy, vz, t, s, params.DT, &drivers);
int do_record = params.save_trajectory || (s % params.NSTEP == 0);
if (do_record) {
int idx = params.save_trajectory ? s : sample_idx;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
traj.x[ s * n + i] = x[i]; traj.x[ idx * n + i] = x[i];
traj.y[ s * n + i] = y[i]; traj.y[ idx * n + i] = y[i];
traj.z[ s * n + i] = z[i]; traj.z[ idx * n + i] = z[i];
traj.vx[s * n + i] = vx[i]; traj.vx[idx * n + i] = vx[i];
traj.vy[s * n + i] = vy[i]; traj.vy[idx * n + i] = vy[i];
traj.vz[s * n + i] = vz[i]; traj.vz[idx * n + i] = vz[i];
}
if (!params.save_trajectory) sample_idx++;
} }
apply_step(params.method, n, x, y, z, vx, vy, vz, apply_step(params.method, n, x, y, z, vx, vy, vz,
atoms.masses, params.G, params.B, &bonds, atoms.fixed, atoms.masses, params.G, params.B, &bonds, atoms.fixed,
@@ -943,8 +1080,13 @@ int main(int argc, char **argv) {
} }
char out_path[512]; char out_path[512];
if (params.save_trajectory) {
snprintf(out_path, sizeof(out_path), "%s/trajectory.txt", output_dir); snprintf(out_path, sizeof(out_path), "%s/trajectory.txt", output_dir);
write_trajectory_json(out_path, &traj, &params, &atoms, &bonds); write_trajectory_json(out_path, &traj, &params, &atoms, &bonds);
} else {
snprintf(out_path, sizeof(out_path), "%s/display.txt", output_dir);
write_display_txt(out_path, &traj, &params, &atoms);
}
clock_t t1 = clock(); clock_t t1 = clock();
double elapsed = (double)(t1 - t0) / CLOCKS_PER_SEC; double elapsed = (double)(t1 - t0) / CLOCKS_PER_SEC;
+193 -52
View File
@@ -43,6 +43,15 @@ struct SimParams {
int damping_force = 0; int damping_force = 0;
double gravity_strength = 1.0; double gravity_strength = 1.0;
int driving_force = 0; int driving_force = 0;
int save_trajectory = 1;
double alpha[6] = {0,0,0,0,0,0};
double ball_radius = 0.5;
double ball_color[3] = {0.9, 0.2, 0.2};
double box_color[3] = {0.8, 0.8, 0.85};
int use_marker = 0;
double camera_distance = 40.0;
double camera_elevation = 0;
double camera_azimuth = 0;
}; };
// ======================================================================== // ========================================================================
@@ -131,6 +140,11 @@ static std::string json_read_string(const std::string &json, const std::string &
return result; return result;
} }
/* 检查 JSON 中是否存在某个 key */
static bool json_has_key(const std::string &json, const std::string &key) {
return json.find("\"" + key + "\"") != std::string::npos;
}
/* 读取 JSON 数组 (如 "G": [0, 0, -9.8]) 到 double[3] */ /* 读取 JSON 数组 (如 "G": [0, 0, -9.8]) 到 double[3] */
static void json_read_double3(const std::string &json, const std::string &key, double out[3]) { static void json_read_double3(const std::string &json, const std::string &key, double out[3]) {
auto pos = json.find("\"" + key + "\""); auto pos = json.find("\"" + key + "\"");
@@ -146,6 +160,21 @@ static void json_read_double3(const std::string &json, const std::string &key, d
} }
} }
/* 读取 JSON 数组到 double[6] */
static void json_read_double6(const std::string &json, const std::string &key, double out[6]) {
auto pos = json.find("\"" + key + "\"");
if (pos == std::string::npos) { for (int i=0;i<6;i++) out[i]=0; return; }
pos = json.find('[', pos);
if (pos == std::string::npos) { for (int i=0;i<6;i++) out[i]=0; return; }
pos++;
for (int i = 0; i < 6; i++) {
while (pos < json.size() && (json[pos]==' '||json[pos]=='\t'||json[pos]=='\n'||json[pos]==','||json[pos]==']')) pos++;
char *end;
out[i] = std::strtod(json.c_str() + pos, &end);
pos = end - json.c_str();
}
}
/* 解析 param.json */ /* 解析 param.json */
static SimParams read_params(const std::string &path) { static SimParams read_params(const std::string &path) {
std::string buf = read_file(path); std::string buf = read_file(path);
@@ -165,6 +194,17 @@ static SimParams read_params(const std::string &path) {
p.damping_force = json_read_int(buf, "damping_force"); p.damping_force = json_read_int(buf, "damping_force");
p.gravity_strength = json_read_double(buf, "gravity_strength"); p.gravity_strength = json_read_double(buf, "gravity_strength");
p.driving_force = json_read_int(buf, "driving_force"); p.driving_force = json_read_int(buf, "driving_force");
// save_trajectory 默认 1(全量),仅在 JSON 中存在该 key 时覆盖
if (json_has_key(buf, "save_trajectory"))
p.save_trajectory = json_read_int(buf, "save_trajectory");
json_read_double6(buf, "alpha", p.alpha);
p.ball_radius = json_read_double(buf, "ball_radius");
json_read_double3(buf, "ball_color", p.ball_color);
json_read_double3(buf, "box_color", p.box_color);
p.use_marker = json_read_int(buf, "use_marker");
p.camera_distance = json_read_double(buf, "camera_distance");
p.camera_elevation = json_read_double(buf, "camera_elevation");
p.camera_azimuth = json_read_double(buf, "camera_azimuth");
return p; return p;
} }
@@ -380,6 +420,26 @@ static void compute_acceleration(
} }
} }
/* 保守力加速度(不含阻尼),供真蛙跳法专用。
传入 damping_force=0 使 compute_acceleration 跳过阻尼项。 */
static void compute_accel_conservative(
int n,
const double *x, const double *y, const double *z,
const double *m, const double G[3],
const BondData &bonds,
int gravity_field, int gravity_interaction,
int elastic_force, double gravity_strength,
double *ax, double *ay, double *az)
{
std::vector<double> v0(n, 0.0);
double Bzero[3] = {0.0, 0.0, 0.0};
compute_acceleration(n, x, y, z, v0.data(), v0.data(), v0.data(),
m, G, Bzero, bonds,
gravity_field, gravity_interaction,
elastic_force, 0, gravity_strength,
ax, ay, az);
}
/* 边界条件:clamp 位置 + 速度反转 ——与 Python Limit_in_box 一致 */ /* 边界条件:clamp 位置 + 速度反转 ——与 Python Limit_in_box 一致 */
static void limit_in_box(double &pos, double &vel, double lo, double hi) { static void limit_in_box(double &pos, double &vel, double lo, double hi) {
if (pos > hi) { pos = hi; vel = -vel; } if (pos > hi) { pos = hi; vel = -vel; }
@@ -503,6 +563,14 @@ static void midpoint_step(
} }
/* ── 蛙跳法(Velocity-Verlet)——与 Python Leapfrog_Method 一致 ── */ /* ── 蛙跳法(Velocity-Verlet)——与 Python Leapfrog_Method 一致 ── */
/* 真蛙跳一步:x(t), v(t-dt/2) → x(t+dt), v(t+dt/2)
*
* 无阻尼:纯保守蛙跳,每步 1 次力计算,辛积分器。
* v(t+dt/2) = v(t-dt/2) + a_c(t)·dt
*
* 有阻尼:半隐式处理,仍 1 次力计算,对任意阻尼无条件稳定。
* v(t+dt/2) = [v(t-dt/2)·(1-α) + a_c(t)·dt] / (1+α),α = B·dt/(2m)
*/
static void leapfrog_full_step( static void leapfrog_full_step(
int n, double *x, double *y, double *z, int n, double *x, double *y, double *z,
double *vx, double *vy, double *vz, double *vx, double *vy, double *vz,
@@ -512,54 +580,33 @@ static void leapfrog_full_step(
int elastic_force, int damping_force, int elastic_force, int damping_force,
double gravity_strength) double gravity_strength)
{ {
// 第一次加速度
std::vector<double> ax(n), ay(n), az(n); std::vector<double> ax(n), ay(n), az(n);
compute_acceleration(n, x, y, z, vx, vy, vz, m, G, B, bonds,
// 1 次保守力计算(不含阻尼)
compute_accel_conservative(n, x, y, z, m, G, bonds,
gravity_field, gravity_interaction, gravity_field, gravity_interaction,
elastic_force, damping_force, gravity_strength, elastic_force, gravity_strength,
ax.data(), ay.data(), az.data()); ax.data(), ay.data(), az.data());
// 半推速度:v_half = v + 0.5*a*dt (存入 vx, vy, vz) bool has_damping = damping_force && (B[0] != 0.0 || B[1] != 0.0 || B[2] != 0.0);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue; if (fixed[i*3+0] && fixed[i*3+1] && fixed[i*3+2]) continue;
vx[i] += ax[i] * dt * 0.5; if (has_damping) {
vy[i] += ay[i] * dt * 0.5; double alphax = B[0] * dt / (2.0 * m[i]);
vz[i] += az[i] * dt * 0.5; double alphay = B[1] * dt / (2.0 * m[i]);
double alphaz = B[2] * dt / (2.0 * m[i]);
vx[i] = (vx[i] * (1.0 - alphax) + ax[i] * dt) / (1.0 + alphax);
vy[i] = (vy[i] * (1.0 - alphay) + ay[i] * dt) / (1.0 + alphay);
vz[i] = (vz[i] * (1.0 - alphaz) + az[i] * dt) / (1.0 + alphaz);
} else {
vx[i] += ax[i] * dt;
vy[i] += ay[i] * dt;
vz[i] += az[i] * dt;
} }
x[i] += vx[i] * dt;
// 全推位置(不含边界,边界在外层统一处理)
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; y[i] += vy[i] * dt;
z[i] += vz[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 一致)── */ /* ── 分发器:调用对应积分方法 + 边界条件(与 Python apply_motion_update 一致)── */
@@ -681,6 +728,67 @@ static void apply_driving_force(
} }
} }
// ========================================================================
// display.txt 输出(save_trajectory=0 时使用)
// ========================================================================
static void write_display_txt(
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)
{
std::ofstream f(path);
if (!f) die("无法写入 " + path);
std::cout << "[Cpp-engine] 正在写入显示数据…" << std::endl;
int dynamic_steps = params.NT - params.warmup_steps;
double T_total = dynamic_steps * params.DT;
/* number of frames 写总积分步数(与 draw.py NT 对应),不是采样帧数 */
f << "number of frames: " << dynamic_steps << "\n";
f << "number of particles: " << n_atoms << "\n";
f << "DT: " << params.DT << "\n";
f << "NSTEP: " << params.NSTEP << "\n";
f << "method: " << params.method << "\n";
f << "warmup_steps: " << params.warmup_steps << "\n";
f << "dynamic_steps: " << dynamic_steps << "\n";
f << "T_total: " << T_total << "\n";
f << "box_a: " << params.box_a << "\n";
f << "driving_force: " << params.driving_force << "\n";
f << "alpha: " << params.alpha[0] << "," << params.alpha[1] << "," << params.alpha[2] << ","
<< params.alpha[3] << "," << params.alpha[4] << "," << params.alpha[5] << "\n";
f << "ball_radius: " << params.ball_radius << "\n";
f << "ball_color_r: " << params.ball_color[0] << "\n";
f << "ball_color_g: " << params.ball_color[1] << "\n";
f << "ball_color_b: " << params.ball_color[2] << "\n";
f << "box_color_r: " << params.box_color[0] << "\n";
f << "box_color_g: " << params.box_color[1] << "\n";
f << "box_color_b: " << params.box_color[2] << "\n";
f << "use_marker: " << params.use_marker << "\n";
f << "camera_distance: " << params.camera_distance << "\n";
f << "camera_elevation: " << params.camera_elevation << "\n";
f << "camera_azimuth: " << params.camera_azimuth << "\n\n";
f << std::fixed << std::setprecision(6);
for (int t = 0; t < n_steps; t++) {
f << "frame: " << (t + 1) << "\n";
f << "n x y z vx vy vz\n";
for (int i = 0; i < n_atoms; i++) {
int base = t * n_atoms + i;
f << std::setw(3) << atoms.ids[i]
<< std::setw(12) << x[base]
<< std::setw(12) << y[base]
<< std::setw(12) << z[base]
<< std::setw(12) << vx[base]
<< std::setw(12) << vy[base]
<< std::setw(12) << vz[base] << "\n";
}
}
}
// ======================================================================== // ========================================================================
// JSON 输出 // JSON 输出
// ======================================================================== // ========================================================================
@@ -695,7 +803,8 @@ static void write_trajectory_json(
{ {
std::ofstream f(path); std::ofstream f(path);
if (!f) die("无法写入 " + path); if (!f) die("无法写入 " + path);
f << std::setprecision(15); std::cout << "[Cpp-engine] 正在写入轨迹数据…" << std::endl;
f << std::setprecision(8);
f << "{\n"; f << "{\n";
@@ -816,12 +925,29 @@ int main(int argc, char **argv) {
// 分配轨迹缓冲区 // 分配轨迹缓冲区
int record_steps = params.NT - params.warmup_steps; int record_steps = params.NT - params.warmup_steps;
std::vector<double> traj_x(record_steps * n); int nstep_sampling = (params.save_trajectory == 0) ? params.NSTEP : 1;
std::vector<double> traj_y(record_steps * n); int buf_steps = (params.save_trajectory == 0) ? (record_steps / nstep_sampling) : record_steps;
std::vector<double> traj_z(record_steps * n); std::vector<double> traj_x(buf_steps * n);
std::vector<double> traj_vx(record_steps * n); std::vector<double> traj_y(buf_steps * n);
std::vector<double> traj_vy(record_steps * n); std::vector<double> traj_z(buf_steps * n);
std::vector<double> traj_vz(record_steps * n); std::vector<double> traj_vx(buf_steps * n);
std::vector<double> traj_vy(buf_steps * n);
std::vector<double> traj_vz(buf_steps * n);
// 真蛙跳初始化:v(0) 反推 v(-dt/2) = v(0) - 0.5*a_c(0)*dt
if (params.method == "leapfrog") {
std::vector<double> ax0(n), ay0(n), az0(n);
compute_accel_conservative(n, x.data(), y.data(), z.data(), atoms.masses.data(), params.G, bonds,
params.gravity_field, params.gravity_interaction,
params.elastic_force, params.gravity_strength,
ax0.data(), ay0.data(), az0.data());
for (int i = 0; i < n; i++) {
if (atoms.fixed[i*3+0] && atoms.fixed[i*3+1] && atoms.fixed[i*3+2]) continue;
vx[i] -= 0.5 * ax0[i] * params.DT;
vy[i] -= 0.5 * ay0[i] * params.DT;
vz[i] -= 0.5 * az0[i] * params.DT;
}
}
// 预热 // 预热
// 初始时刻 t=0 驱动力(与 Python run_simulation 一致) // 初始时刻 t=0 驱动力(与 Python run_simulation 一致)
@@ -843,18 +969,27 @@ int main(int argc, char **argv) {
} }
// 记录 // 记录
int _prog_int = record_steps / 100;
if (_prog_int < 1) _prog_int = 1;
int si = 0; // 采样帧索引
for (int s = 0; s < record_steps; s++) { for (int s = 0; s < record_steps; s++) {
if (s % _prog_int == 0 && s > 0) {
std::cout << "[Cpp-engine] progress: " << s << "/" << record_steps << std::endl;
}
double t = (s + params.warmup_steps) * params.DT; double t = (s + params.warmup_steps) * params.DT;
if (params.driving_force) 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); apply_driving_force(n, x.data(), y.data(), z.data(), vx.data(), vy.data(), vz.data(), t, s, params.DT, drivers);
// 保存当前帧 // 保存当前帧(采样模式仅每 NSTEP 步保存一次)
if (s % nstep_sampling == 0) {
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
traj_x[s * n + i] = x[i]; traj_x[si * n + i] = x[i];
traj_y[s * n + i] = y[i]; traj_y[si * n + i] = y[i];
traj_z[s * n + i] = z[i]; traj_z[si * n + i] = z[i];
traj_vx[s * n + i] = vx[i]; traj_vx[si * n + i] = vx[i];
traj_vy[s * n + i] = vy[i]; traj_vy[si * n + i] = vy[i];
traj_vz[s * n + i] = vz[i]; traj_vz[si * n + i] = vz[i];
}
si++;
} }
apply_step(params.method, n, x.data(), y.data(), z.data(), apply_step(params.method, n, x.data(), y.data(), z.data(),
@@ -868,9 +1003,15 @@ int main(int argc, char **argv) {
} }
// 输出轨迹 // 输出轨迹
if (params.save_trajectory == 0) {
std::string out_path = output_dir + "/display.txt";
write_display_txt(out_path, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz,
si, n, params, atoms);
} else {
std::string out_path = output_dir + "/trajectory.txt"; 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, write_trajectory_json(out_path, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz,
record_steps, n, params, atoms, bonds); record_steps, n, params, atoms, bonds);
}
auto t1 = std::chrono::high_resolution_clock::now(); auto t1 = std::chrono::high_resolution_clock::now();
double elapsed = std::chrono::duration<double>(t1 - t0).count(); double elapsed = std::chrono::duration<double>(t1 - t0).count();
+72 -41
View File
@@ -34,7 +34,7 @@ program dynamics_f90
double precision, allocatable :: bond_stiffness(:), bond_rest_lengths(:) double precision, allocatable :: bond_stiffness(:), bond_rest_lengths(:)
! 驱动力数据 ! 驱动力数据
integer :: n_drivers integer :: n_drivers, prog_step
integer, allocatable :: drv_atom_idx(:) integer, allocatable :: drv_atom_idx(:)
double precision, allocatable :: drv_amp_x(:), drv_amp_y(:), drv_amp_z(:) double precision, allocatable :: drv_amp_x(:), drv_amp_y(:), drv_amp_z(:)
double precision, allocatable :: drv_freq_x(:), drv_freq_y(:), drv_freq_z(:) double precision, allocatable :: drv_freq_x(:), drv_freq_y(:), drv_freq_z(:)
@@ -107,6 +107,24 @@ program dynamics_f90
allocate(traj_x(record_steps, n), traj_y(record_steps, n), traj_z(record_steps, n)) allocate(traj_x(record_steps, n), traj_y(record_steps, n), traj_z(record_steps, n))
allocate(traj_vx(record_steps, n), traj_vy(record_steps, n), traj_vz(record_steps, n)) allocate(traj_vx(record_steps, n), traj_vy(record_steps, n), traj_vz(record_steps, n))
! 真蛙跳初始化:v(0) 反推 v(-dt/2) = v(0) - 0.5*a_c(0)*dt
if (trim(method) == 'leapfrog') then
block
double precision :: ax0(n), ay0(n), az0(n)
integer :: ii
call accel_conservative(n, x, y, z, masses, G, &
n_bonds, bond_pairs, bond_stiffness, bond_rest_lengths, &
gravity_field, gravity_interaction, &
elastic_force, gravity_strength, ax0, ay0, az0)
do ii = 1, n
if (fixed(ii,1) /= 0 .and. fixed(ii,2) /= 0 .and. fixed(ii,3) /= 0) cycle
vx(ii) = vx(ii) - 0.5d0 * ax0(ii) * DT
vy(ii) = vy(ii) - 0.5d0 * ay0(ii) * DT
vz(ii) = vz(ii) - 0.5d0 * az0(ii) * DT
end do
end block
end if
! 预热 ! 预热
! 初始时刻 t=0 驱动力(与 Python run_simulation 一致) ! 初始时刻 t=0 驱动力(与 Python run_simulation 一致)
if (driving_force /= 0 .and. n_drivers > 0) then if (driving_force /= 0 .and. n_drivers > 0) then
@@ -139,7 +157,12 @@ program dynamics_f90
end do end do
! 记录 ! 记录
prog_step = record_steps / 100
if (prog_step < 1) prog_step = 1
do s = 1, record_steps do s = 1, record_steps
if (mod(s, prog_step) == 0 .and. s > 0) then
write(*, '("[Fortran-engine] progress: ", i0, "/", i0)') s, record_steps
end if
if (driving_force /= 0 .and. n_drivers > 0) then if (driving_force /= 0 .and. n_drivers > 0) then
tw = ((s-1 + warmup_steps) * 1.0d0) * DT tw = ((s-1 + warmup_steps) * 1.0d0) * DT
call apply_driving(n, x, y, z, vx, vy, vz, tw, s-1, DT, & call apply_driving(n, x, y, z, vx, vy, vz, tw, s-1, DT, &
@@ -161,6 +184,7 @@ program dynamics_f90
end do end do
! 输出轨迹 ! 输出轨迹
write(*, '("[Fortran-engine] 正在写入轨迹数据…")')
call write_json(output_dir, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, & call write_json(output_dir, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, &
record_steps, n_atoms, atom_ids, masses, & record_steps, n_atoms, atom_ids, masses, &
NT, DT, NSTEP, warmup_steps, method, G, B, & NT, DT, NSTEP, warmup_steps, method, G, B, &
@@ -518,6 +542,24 @@ pure subroutine accel(n, x, y, z, vx, vy, vz, m, G, B, &
end if end if
end subroutine accel end subroutine accel
! 保守力加速度(不含阻尼),供真蛙跳法专用。
! 传入零速度、零 B 调用 accel,阻尼项 -B*v/m 自动为零。
subroutine accel_conservative(n, x, y, z, m, G, nb, bp, bk, br, &
gravity_field, gravity_interaction, &
elastic_force, gravity_strength, ax, ay, az)
integer, intent(in) :: n, nb, bp(nb, 2)
integer, intent(in) :: gravity_field, gravity_interaction, elastic_force
double precision, intent(in) :: x(n), y(n), z(n), m(n), G(3)
double precision, intent(in) :: bk(nb), br(nb), gravity_strength
double precision, intent(out) :: ax(n), ay(n), az(n)
double precision :: v0(n), B0(3)
v0 = 0.0d0
B0 = 0.0d0
call accel(n, x, y, z, v0, v0, v0, m, G, B0, nb, bp, bk, br, &
gravity_field, gravity_interaction, &
elastic_force, 0, gravity_strength, ax, ay, az)
end subroutine accel_conservative
! 边界条件:clamp 位置 + 速度反转 ! 边界条件:clamp 位置 + 速度反转
subroutine limit_in_box(pos, vel, lo, hi) subroutine limit_in_box(pos, vel, lo, hi)
double precision, intent(inout) :: pos, vel double precision, intent(inout) :: pos, vel
@@ -646,7 +688,13 @@ subroutine midpoint_step(n, x, y, z, vx, vy, vz, m, G, B, &
end do end do
end subroutine midpoint_step end subroutine midpoint_step
! ── 蛙跳法(Velocity-Verlet)── ! 真蛙跳一步:x(t), v(t-dt/2) → x(t+dt), v(t+dt/2)
!
! 无阻尼:纯保守蛙跳,每步 1 次力计算,辛积分器。
! v(t+dt/2) = v(t-dt/2) + a_c(t)*dt
!
! 有阻尼:半隐式处理,仍 1 次力计算,对任意阻尼无条件稳定。
! v(t+dt/2) = [v(t-dt/2)*(1-α) + a_c(t)*dt] / (1+α),α = B*dt/(2m)
subroutine leapfrog_full(n, x, y, z, vx, vy, vz, m, G, B, & subroutine leapfrog_full(n, x, y, z, vx, vy, vz, m, G, B, &
nb, bp, bk, br, fixed, dt, & nb, bp, bk, br, fixed, dt, &
gravity_field, gravity_interaction, & gravity_field, gravity_interaction, &
@@ -656,53 +704,36 @@ subroutine leapfrog_full(n, x, y, z, vx, vy, vz, m, G, B, &
double precision, intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n) double precision, intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n)
double precision, intent(in) :: m(n), G(3), B(3), bk(nb), br(nb), dt, gravity_strength double precision, intent(in) :: m(n), G(3), B(3), bk(nb), br(nb), dt, gravity_strength
double precision :: ax(n), ay(n), az(n) double precision :: ax(n), ay(n), az(n)
double precision :: dmp_vx(n), dmp_vy(n), dmp_vz(n) double precision :: alphax, alphay, alphaz
double precision :: gx, gy, gz logical :: has_damping
integer :: i integer :: i
call accel(n, x, y, z, vx, vy, vz, m, G, B, nb, bp, bk, br, & ! 1 次保守力计算(不含阻尼)
call accel_conservative(n, x, y, z, m, G, nb, bp, bk, br, &
gravity_field, gravity_interaction, & gravity_field, gravity_interaction, &
elastic_force, damping_force, gravity_strength, ax, ay, az) elastic_force, gravity_strength, ax, ay, az)
has_damping = (damping_force /= 0) .and. &
(B(1) /= 0.0d0 .or. B(2) /= 0.0d0 .or. B(3) /= 0.0d0)
! 速度半步推
do i = 1, n
if (fixed(i,1) /= 0 .and. fixed(i,2) /= 0 .and. fixed(i,3) /= 0) cycle
vx(i) = vx(i) + ax(i) * dt * 0.5d0
vy(i) = vy(i) + ay(i) * dt * 0.5d0
vz(i) = vz(i) + az(i) * dt * 0.5d0
end do
! 全推位置(不含边界)
do i = 1, n do i = 1, n
if (fixed(i,1) /= 0 .and. fixed(i,2) /= 0 .and. fixed(i,3) /= 0) cycle if (fixed(i,1) /= 0 .and. fixed(i,2) /= 0 .and. fixed(i,3) /= 0) cycle
if (has_damping) then
alphax = B(1) * dt / (2.0d0 * m(i))
alphay = B(2) * dt / (2.0d0 * m(i))
alphaz = B(3) * dt / (2.0d0 * m(i))
vx(i) = (vx(i) * (1.0d0 - alphax) + ax(i) * dt) / (1.0d0 + alphax)
vy(i) = (vy(i) * (1.0d0 - alphay) + ay(i) * dt) / (1.0d0 + alphay)
vz(i) = (vz(i) * (1.0d0 - alphaz) + az(i) * dt) / (1.0d0 + alphaz)
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 x(i) = x(i) + vx(i) * dt
y(i) = y(i) + vy(i) * dt y(i) = y(i) + vy(i) * dt
z(i) = z(i) + vz(i) * dt z(i) = z(i) + vz(i) * dt
end do end do
! 隐式阻尼处理(用临时数组 dmp_v,不覆盖 vx/vy/vz
do i = 1, n
if (fixed(i,1) /= 0 .and. fixed(i,2) /= 0 .and. fixed(i,3) /= 0) then
dmp_vx(i) = 0; dmp_vy(i) = 0; dmp_vz(i) = 0; cycle
end if
gx = B(1) / m(i); gy = B(2) / m(i); gz = B(3) / m(i)
dmp_vx(i) = (vx(i) + 0.5d0 * G(1) * dt) / (1.0d0 + 0.5d0 * gx * dt)
dmp_vy(i) = (vy(i) + 0.5d0 * G(2) * dt) / (1.0d0 + 0.5d0 * gy * dt)
dmp_vz(i) = (vz(i) + 0.5d0 * G(3) * dt) / (1.0d0 + 0.5d0 * gz * dt)
end do
! 用新位置 + 阻尼速度重算加速度
call accel(n, x, y, z, dmp_vx, dmp_vy, dmp_vz, m, G, B, nb, bp, bk, br, &
gravity_field, gravity_interaction, &
elastic_force, damping_force, gravity_strength, ax, ay, az)
! 速度后半步:v = v_half + 0.5*a_next*dtvx 仍为 v_half
do i = 1, n
if (fixed(i,1) /= 0 .and. fixed(i,2) /= 0 .and. fixed(i,3) /= 0) cycle
vx(i) = vx(i) + ax(i) * dt * 0.5d0
vy(i) = vy(i) + ay(i) * dt * 0.5d0
vz(i) = vz(i) + az(i) * dt * 0.5d0
end do
end subroutine leapfrog_full end subroutine leapfrog_full
! ── 分发器 + 边界条件 + 自由度约束 ── ! ── 分发器 + 边界条件 + 自由度约束 ──
@@ -1066,7 +1097,7 @@ subroutine json_arr(u, vals, n, has_next, indent)
write(u, '(a)', advance='no') indent // '[' write(u, '(a)', advance='no') indent // '['
do i = 1, n do i = 1, n
if (i > 1) write(u, '(a)', advance='no') ',' if (i > 1) write(u, '(a)', advance='no') ','
write(u, '(g0)', advance='no') vals(i) write(u, '(g0.8)', advance='no') vals(i)
end do end do
if (has_next) then if (has_next) then
write(u, '(a)') '],' write(u, '(a)') '],'
@@ -1103,7 +1134,7 @@ subroutine write_dbl_arr(u, name, arr, n, has_next)
write(u, '(a)', advance='no') ' "' // trim(name) // '": [' write(u, '(a)', advance='no') ' "' // trim(name) // '": ['
do i = 1, n do i = 1, n
if (i > 1) write(u, '(a)', advance='no') ',' if (i > 1) write(u, '(a)', advance='no') ','
write(u, '(g0)', advance='no') arr(i) write(u, '(g0.8)', advance='no') arr(i)
end do end do
if (has_next) then if (has_next) then
write(u, '(a)') '],' write(u, '(a)') '],'
+1 -1
View File
@@ -1,2 +1,2 @@
bond_name k rest_length bond_name k rest_length
k1 1.0 1.0 k1 50.0 1.0
+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
+17 -8
View File
@@ -5,18 +5,21 @@
# ── 流程控制 ────────────────────────────────── # ── 流程控制 ──────────────────────────────────
# 每步用 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: fortran # 默认使用 python 引擎 engine: python # 默认使用 python 引擎
# ── 盒子 ────────────────────────────────────── # ── 盒子 ──────────────────────────────────────
box_a: 80.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 box_a: 80.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内
@@ -63,13 +66,13 @@ warmup_steps: 0 # 默认 0(立即开始记录)
# 总模拟时间(秒),程序自动计算 NT = T_total / DT # 总模拟时间(秒),程序自动计算 NT = T_total / DT
# 如果同时指定了 NT,以 NT 为准 # 如果同时指定了 NT,以 NT 为准
T_total: 10.0 T_total: 100.0
# 抽帧间隔(每 NSTEP 步取一帧用于动画) # 抽帧间隔(每 NSTEP 步取一帧用于动画)
NSTEP: 100 NSTEP: 10
# ── 时间步长 ────────────────────────────────── # ── 时间步长 ──────────────────────────────────
DT: 0.001 # 时间步长 (s) DT: 0.01 # 时间步长 (s)
# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 # 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧
sample_start: null # null 表示从头开始(帧索引从 0 起) sample_start: null # null 表示从头开始(帧索引从 0 起)
@@ -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,9 @@ 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: 40.0 # 摄像机到场景中心的距离
camera_elevation: 0 # 俯仰角(度),负值=俯视
camera_azimuth: 0 # 方位角(度)
move_camera: 1 # 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()
+450
View File
@@ -0,0 +1,450 @@
# Dynamics 项目优化建议
> 分析日期:2026-06-12
> 分析范围:`D:\Share\Data\aliyun-gitea\dynamics` 完整代码库
> 当前算例:`examples/case06`120 原子一维链横波,NT=100,000 步,C 引擎)
> 另见:[`optimization/workbuddy.md`](workbuddy.md)(架构拆分、全局变量封装、测试等宏观建议)
本文档聚焦以下四类问题,不与 workbuddy.md 重复:
1. **已确认的 Bug**(可能导致运行崩溃或结果错误)
2. **Python 引擎性能**(向量化等具体代码改动)
3. **引擎一致性问题**C/C++/Fortran 行为差异)
4. **小的代码质量问题**(错误文本、重复代码等)
---
## 一、已确认的 Bug
### B1. `run_simulation()` 内引用了不在作用域的 `config` 变量 ⚠️ 严重
**文件**[`compute.py:1548-1550`](../compute.py)
```python
# 问题代码(run_simulation 函数内部,config 未传入)
"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)),
```
`run_simulation()` 是独立函数,不接收 `config` 参数,但函数内部直接使用了 `run_from_config()` 的局部变量 `config`。只要使用 Python 引擎(`engine: python`)就会触发 `NameError`
**修复方案**:这三个字段的值已在 `run_from_config` 里读取并存入全局变量(`camera_keyframes_raw` 已有),
`camera_distance/elevation/azimuth` 同样声明为全局变量并在 `run_from_config` 中赋值:
```python
# 在模块顶部全局变量区添加(compute.py ~65 行附近)
camera_distance = 40.0
camera_elevation = 0
camera_azimuth = 0
# 在 run_from_config 中赋值(~756 行附近,已有 use_marker 赋值的位置)
camera_distance = float(config.get("camera_distance", 40.0))
camera_elevation = float(config.get("camera_elevation", 0))
camera_azimuth = float(config.get("camera_azimuth", 0))
# run_simulation 内改为读全局变量
"camera_distance": str(camera_distance),
"camera_elevation": str(camera_elevation),
"camera_azimuth": str(camera_azimuth),
```
---
### B2. `dynamics.py` 绘图块引用未定义的变量 ⚠️ 严重
**文件**[`dynamics.py:330-451`](../dynamics.py)
```python
# step_plot=1 时触发,但这些变量从未赋值
time_arr = np.arange(NT) * DT # NT、DT 在此作用域未定义
n_atoms = all_x.shape[1] # all_x 不存在
atom_ids_list = data.get("atom_ids", ...) # data 不存在
```
这段绘图代码写于旧版(`run_simulation` 返回完整轨迹时),现在 `run_from_config` 已不再在此作用域填充这些变量,整块代码是失效的死代码。当前所有案例恰好设置 `step_plot: 0` 所以不触发,但一旦用户设为 1 就崩溃。
**修复方案**:从 `run_from_config` 的返回值读取数据重构绘图代码,或暂时添加保护:
```python
# 临时保护(快速修复)
if not no_plot and config.get("step_plot", 1):
print("[run] 警告: step_plot 功能暂未实现,已跳过")
```
---
### B3. `plot_wave.py` 使用旧 JSON 格式字段读取新格式 display.txt ⚠️ 严重
**文件**[`plot_wave.py:22-27`](../plot_wave.py) 和 [`plot_wave.py:96-111`](../plot_wave.py)
```python
# plot_wave.py 的 load_disp_data()
def load_disp_data(output_dir):
return compute.load_text_data(disp_path) # 调用了 JSON 格式的加载函数!
# 然后访问旧 JSON 字段名
n_frames = int(data["n_frames"]) # 新格式没有这个键
x = np.array(data["disp_all_x"]) # 新格式没有这个键
masses = np.array(data["atom_masses"]) # 新格式没有这个键
```
`display.txt` 现在已是新文本格式(由 `save_display_txt` 写出),正确的加载函数是 `compute.load_display_txt()`,且字段名完全不同(`frames_x` 而非 `disp_all_x`)。
**影响**`step_plot_wave: 1` 时必然崩溃,case05/case06 的波形动画功能完全失效。
**修复方案**:将 `load_disp_data` 改为调用 `load_display_txt`,并适配字段名:
```python
def load_disp_data(output_dir):
disp_path = os.path.join(output_dir, "display.txt")
if not os.path.exists(disp_path):
raise FileNotFoundError(f"找不到 {disp_path}")
return compute.load_display_txt(disp_path) # 改这里
# 后续访问字段名也需同步修改:
# data["disp_all_x"] → disp_data["frames_x"]
# data["n_frames"] → disp_data["frames_x"].shape[0]
# data["atom_masses"] → 从 header_fields 读取或从 bond/coord 文件读
```
> **注**`plot_wave.py` 依赖 `atom_masses`、`bond_pairs` 等物理量,这些在新的 display.txt 格式中
> 通过 `header_fields` 字符串存储,需要额外解析。较彻底的修复需要在 display.txt header 中保留
> `atom_masses` 序列(目前 `atom_radii` 已以逗号分隔字符串保存,可仿照此方式)。
---
### B4. `run_dynamics.py`case06)描述文字错误
**文件**[`examples/case06/run_dynamics.py:34`](../examples/case06/run_dynamics.py)
```python
# 当前(错误)
parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case01")
# 应为
parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case06")
```
---
### B5. `draw.py` 裸 `except` 吞掉所有异常
**文件**[`draw.py:97`](../draw.py)
```python
try:
alpha_list = [float(x) for x in raw_alpha.split(",")]
if len(alpha_list) != 6:
alpha_list = alpha_list * 6
except: # 裸 except:连 KeyboardInterrupt 都会捕获
alpha_list = [float(raw_alpha)] * 6
```
**修复**:改为 `except (ValueError, AttributeError):`,并加 `print` 提示。
---
## 二、Python 引擎性能优化
> **背景**:case06 使用 C 引擎,以下优化主要影响 `engine: python` 路径。
> 当前 Python vs C 速度比约为 6-8 倍(100,000 步,120 原子)。
### P1. 弹簧力计算向量化(最大收益)
**文件**[`compute.py:1253-1277`](../compute.py)
```python
# 当前:Python for 循环,119 键 × 100,000 步 ≈ 1200 万次迭代
for bond_idx, (idx_1, idx_2) in enumerate(BOND_PAIRS):
dx = x[idx_2] - x[idx_1]
dy = y[idx_2] - y[idx_1]
dz = z[idx_2] - z[idx_1]
dist = np.sqrt(dx * dx + dy * dy + dz * dz)
if dist <= 1e-12:
continue
stretch = dist - BOND_REST_LENGTHS[bond_idx]
force_mag = BOND_STIFFNESS[bond_idx] * stretch
...
```
**向量化版本**
```python
if ELASTIC_FORCE and BOND_PAIRS is not None and len(BOND_PAIRS) > 0:
i_arr = BOND_PAIRS[:, 0] # (n_bonds,)
j_arr = BOND_PAIRS[:, 1]
dx = x[j_arr] - x[i_arr]
dy = y[j_arr] - y[i_arr]
dz = z[j_arr] - z[i_arr]
dist = np.sqrt(dx * dx + dy * dy + dz * dz)
valid = dist > 1e-12
inv_dist = np.where(valid, 1.0 / np.maximum(dist, 1e-12), 0.0)
stretch = dist - BOND_REST_LENGTHS
force_mag = BOND_STIFFNESS * stretch * inv_dist # 归一化后的分量力幅
fx_bond = force_mag * dx
fy_bond = force_mag * dy
fz_bond = force_mag * dz
np.add.at(fx, i_arr, fx_bond)
np.add.at(fx, j_arr, -fx_bond)
np.add.at(fy, i_arr, fy_bond)
np.add.at(fy, j_arr, -fy_bond)
np.add.at(fz, i_arr, fz_bond)
np.add.at(fz, j_arr, -fz_bond)
```
**预期加速**Python 引擎整体 **5-15 倍**(对于链状键合体系,弹簧力是瓶颈)。
> `np.add.at` 支持重复索引的原子累加,对有分叉键的复杂体系也正确。
> 若键无重复端点(链状),可改为 `fx[i_arr] += fx_bond; fx[j_arr] -= fx_bond` 更快。
---
### P2. `apply_fixed_constraints()` 减少临时数组分配
**文件**[`compute.py:1408-1418`](../compute.py)
```python
# 当前:每步创建 2 个 (n_atoms, 3) 的临时数组
positions = np.column_stack((x, y, z))
velocities = np.column_stack((vx, vy, vz))
positions = np.where(fixed, ATOM_POSITIONS, positions)
velocities = np.where(fixed, 0.0, velocities)
return positions[:, 0], positions[:, 1], positions[:, 2], ...
```
对于 case06(x/y 全部固定,z 自由),每步都在做 120 原子 × 3 列的 `column_stack`,但实际上只有 z 方向的原子可以运动。
**优化版本**(预先计算掩码,直接原地修改):
```python
# 在模块初始化时(run_from_config 中)预计算固定掩码
_FIXED_MASK_X = ATOM_FIXED[:, 0] != 0 # bool 数组,提前缓存
_FIXED_MASK_Y = ATOM_FIXED[:, 1] != 0
_FIXED_MASK_Z = ATOM_FIXED[:, 2] != 0
def apply_fixed_constraints(x, y, z, vx, vy, vz):
if np.any(_FIXED_MASK_X):
x[_FIXED_MASK_X] = ATOM_POSITIONS[_FIXED_MASK_X, 0]
vx[_FIXED_MASK_X] = 0.0
if np.any(_FIXED_MASK_Y):
y[_FIXED_MASK_Y] = ATOM_POSITIONS[_FIXED_MASK_Y, 1]
vy[_FIXED_MASK_Y] = 0.0
if np.any(_FIXED_MASK_Z):
z[_FIXED_MASK_Z] = ATOM_POSITIONS[_FIXED_MASK_Z, 2]
vz[_FIXED_MASK_Z] = 0.0
return x, y, z, vx, vy, vz
```
---
### P3. `run_simulation()` 中冗余的 `frame_indices` 列表
**文件**[`compute.py:1470,1510`](../compute.py)
```python
frame_indices = []
...
frame_indices.append(step) # 每 NSTEP 步追加一次,仅用于最后计算 n_frames_actual
# 实际上等价于:
n_frames_actual = record_steps // NSTEP # 直接计算,O(1)
```
100,000 步中约有 1,000 次 `append`,开销小但无必要。删除 `frame_indices` 列表,用计数器替代。
---
### P4. `apply_driving_force()` 内部创建临时 `t_vec`
**文件**[`compute.py:630`](../compute.py)
```python
# 每次调用都分配一个 3 元素 array
t_vec = np.array([t, t, t], dtype=np.float64)
pos_drive = d["amp"] * np.cos(2.0 * np.pi * d["freq"] * t_vec + d["phi"])
```
`t` 是标量,`d["freq"]``d["phi"]` 是 (3,) 数组,直接用标量广播即可:
```python
pos_drive = d["amp"] * np.cos(2.0 * np.pi * d["freq"] * t + d["phi"])
vel_drive = -d["amp"] * 2.0 * np.pi * d["freq"] * np.sin(2.0 * np.pi * d["freq"] * t + d["phi"])
```
---
### P5. `GRAVITY_INTERACTION` 的 O(N²) 双重循环
**文件**[`compute.py:1279-1297`](../compute.py)
```python
# 当前:纯 Python 双重循环
for i in range(n):
for j in range(i + 1, n):
...
```
**向量化方案**(适用于 N 不太大时):
```python
if GRAVITY_INTERACTION:
# 构造所有原子对的差向量
xi = x[:, np.newaxis]; xj = x[np.newaxis, :]
yi = y[:, np.newaxis]; yj = y[np.newaxis, :]
zi = z[:, np.newaxis]; zj = z[np.newaxis, :]
r2 = (xj - xi)**2 + (yj - yi)**2 + (zj - zi)**2
np.fill_diagonal(r2, np.inf) # 排除自身
r = np.sqrt(r2)
mi = m[:, np.newaxis]; mj = m[np.newaxis, :]
# 力幅(标量,上三角)
f_mag = GRAVITY_STRENGTH * mi * mj / r2
fx_ij = f_mag * (xj - xi) / r # (n, n) 矩阵
fy_ij = f_mag * (yj - yi) / r
fz_ij = f_mag * (zj - zi) / r
fx += np.sum(fx_ij - fx_ij.T, axis=1) * 0.5 # 利用反对称性
fy += np.sum(fy_ij - fy_ij.T, axis=1) * 0.5
fz += np.sum(fz_ij - fz_ij.T, axis=1) * 0.5
```
> 此向量化在 N ≤ ~500 时比 Python 循环快约 20 倍;N > 1000 时内存占用高(N² 矩阵),
> 需要分块或使用 Barnes-Hut 树算法。
---
## 三、引擎一致性问题
### E1. 弹性力计算:Python 与 C 引擎的差异
**文件**[`compute.py:1273-1277`](../compute.py) vs [`engines/c/main.c`](../engines/c/main.c)
Python 引擎将整根键的弹性势能 `½k(d-d₀)²` 记给原子 `i`(不均分),而 C/C++ 引擎均分给两端。
这不影响力的计算(力相同),但如果未来基于势能做分析,两个引擎的势能分配会不同。
建议注释中明确说明约定。
### E2. C++ 引擎 `save_trajectory` 默认值为 1
**文件**[`engines/cpp/param.json`](../engines/cpp/param.json)
```json
// engines/c/param.json: "save_trajectory": 0
// engines/cpp/param.json: "save_trajectory": 1 ← 与 C 不一致
```
用户切换引擎时行为不同,可能意外生成大文件。建议统一为 0。
### E3. Fortran 引擎不支持 `save_trajectory=0`
**文件**[`engines/fortran/main.f90`](../engines/fortran/main.f90)
Fortran 引擎总是输出 JSON 格式的 `trajectory.txt`,不支持直接写 `display.txt`
workbuddy.md 中已将此列为 P0,此处确认影响范围:
- case 使用 `engine: fortran` 时必然崩溃(Python 侧期待 `display.txt`
- 应参照 C 引擎的 `write_display_txt` 函数实现
### E4. 外部引擎每次运行都重新校准
**文件**[`compute.py:924-948`](../compute.py)
每次运行前跑 `min(1000, NT//10)` 步测速。对于 `NT=100,000` 的 case06,校准跑 10,000 步,
占总运算量的 10%。
**快速修复**(跳过短运行的校准):
```python
# 当总步数 < 5000 时跳过校准,直接用默认估计
if total_steps < 5000:
_step_time = 1e-5 # 合理的保守估计
_est_total = _step_time * total_steps
else:
# 现有校准逻辑...
```
**彻底修复**:缓存校准结果到 `engines/{engine}/_calib_cache.json`key 为 `(n_atoms, method)`
命中时跳过校准(workbuddy.md §2.1 有详细代码示例)。
---
## 四、代码质量问题
### Q1. 相机路径解析函数重复
[`dynamics.py:35-69`](../dynamics.py) 的 `_load_camera_kf()`
[`compute.py:90-136`](../compute.py) 的 `_load_camera_motion()` 是功能完全相同的函数,
仅变量名略有差异。
**建议**:保留 `compute.py` 中的版本,`dynamics.py` 改为从 `compute` 导入:
```python
# dynamics.py
from compute import _load_camera_motion as _load_camera_kf
```
### Q2. `load_parameters()` 函数已废弃但仍保留
**文件**[`compute.py:1123-1199`](../compute.py)
这是老版通过 `key = value` 文本格式读参数的函数,现在所有案例已改用 YAML。
只有 `compute.py``main()` 还调用它,而 `compute.main()` 本身也已被 `dynamics.py` 取代。
整个函数(77 行)加上 `main()`25 行)都可以安全删除,或移入 `tools/` 目录存档。
### Q3. 边界条件逻辑不一致
**文件**[`compute.py:1380-1387`](../compute.py) vs [`compute.py:1420-1428`](../compute.py)
```python
def Limit_in_box(a, amin, amax, va):
# 反弹:碰壁时速度反向(弹性碰撞)
va = np.where(over | under, -va, va)
def wrap_position(x, y, z):
# 周期性边界:从一侧出去从另一侧进入
x = np.where(x > X_MAX, X_MIN, x)
```
`run_simulation` 中两者都被调用:先通过 `apply_motion_update()` 执行 `Limit_in_box`(反弹),
再执行 `wrap_position()`(周期)。顺序执行时,`Limit_in_box` 已把粒子限制在盒内,
`wrap_position` 实际上永远不会触发——但这一逻辑关系无文档说明,容易误导读者。
**建议**:在 `run_simulation` 中加一行注释说明两者的分工,或删除其中一个。
### Q4. 案例说明文字未更新(case06)
**文件**[`examples/case06/run_dynamics.py:34`](../examples/case06/run_dynamics.py)
见 B4,在此重申。
---
## 五、总结与优先级
| 编号 | 问题 | 优先级 | 预估工作量 |
|------|------|--------|-----------|
| B1 | `run_simulation``config` 未定义 | 🔴 立即 | 15 min |
| B2 | `dynamics.py` 绘图块死代码(NameError | 🔴 立即 | 30 min |
| B3 | `plot_wave.py` 使用旧格式加载新 display.txt | 🔴 立即 | 1-2 h |
| B4 | case06 描述文字写着 case01 | 🟡 低 | 1 min |
| B5 | `draw.py` 裸 except | 🟡 低 | 5 min |
| P1 | 弹簧力向量化(Python 引擎) | 🟢 中 | 1 h |
| P2 | `apply_fixed_constraints` 减少临时数组 | 🟢 低 | 30 min |
| P3 | `frame_indices` 列表改计数器 | 🟢 低 | 5 min |
| P4 | `apply_driving_force` 去掉 `t_vec` | 🟢 低 | 5 min |
| P5 | `GRAVITY_INTERACTION` O(N²) 向量化 | 🟢 中 | 1 h |
| E1 | 势能归属约定加注释 | 🟡 低 | 10 min |
| E2 | C++ `save_trajectory` 默认值统一 | 🟡 低 | 5 min |
| E3 | Fortran 引擎支持 display.txt 输出 | 🔴 高 | 2-3 h |
| E4 | 外部引擎校准缓存 | 🟡 中 | 1-2 h |
| Q1 | 相机解析函数去重 | 🟡 低 | 15 min |
| Q2 | 废弃的 `load_parameters` + `main()` 删除 | 🟡 低 | 15 min |
| Q3 | 边界条件逻辑加注释 | 🟡 低 | 10 min |
| Q4 | case06 描述文字 | 🟢 低 | 1 min |
**最高回报的三件事(按投入产出比排序)**
1. **B3**`plot_wave.py` 格式不匹配):修复后波形动画功能恢复,影响 case05/case06
2. **B1**`config` 未定义):修复后 Python 引擎可完整运行 case06
3. **P1**(弹簧力向量化):修复后 Python 引擎速度提升 5-15 倍,适合大规模测试和教学演示
---
*本文档由 Claude Sonnet 4.6 自动生成,基于代码静态分析,未实际运行测试验证。建议在实施前先运行相关案例确认 Bug 现象。*
+459
View File
@@ -0,0 +1,459 @@
# Dynamics 项目优化方案综合报告
> 整合日期:2026-06-12
> 来源文档:[`claude.md`](claude.md)Claude Sonnet 4.6 分析)、[`workbuddy.md`](workbuddy.md)WorkBuddy 分析)
> 项目路径:`D:\Share\Data\aliyun-gitea\dynamics`
---
## 第一部分:两份分析报告的评价与对比
### 1.1 WorkBuddy 分析报告评价(`workbuddy.md`
#### 优势
**① 架构视野宏观、分层清晰**
WorkBuddy 从整体软件工程角度出发,明确指出 `compute.py` 的职责混乱问题(1618 行同时承担物理引擎、文件 I/O、参数加载、外部引擎管理五种职责),并给出了完整的模块拆分方案:
```
compute/
├── core.py # 物理引擎
├── io.py # 文件 I/O
├── params.py # 参数加载
├── runner.py # 运行管理
├── engine_helper.py # 外部引擎
└── main.py # 主入口
```
这是一个系统性重构方向,有助于长期维护。
**② 关注工程规范与测试**
WorkBuddy 专门开辟了"测试与质量"一节,强调了缺少单元测试的风险,并给出了类型标注的示例代码——这两点是 claude.md 未涉及的。对于一个教学/研究用项目,有测试才能安全重构。
**③ 配置管理建议具体**
提出了将 `ball_color_r/g/b` 三个键合并为 `ball_color: [r,g,b]` 数组的配置统一方案,以及6个案例 `input.txt` 格式不统一的问题(如 `save_trajectory``camera_*` 字段缺失),并给出了统一模板,实操性强。
**④ 引擎一致性梳理全面**
清晰列出了 C/C++/Fortran 三引擎在 `save_trajectory``box_a` 默认值等参数上的差异表格,对用户切换引擎有直接指导价值。
**⑤ 区分"已完成"与"待做"**
文档中标注了"✅ 已修复"、"✅ 已完成"条目,帮助读者快速定位当前状态,避免重复分析。
#### 劣势
**① 缺少具体 Bug 定位**
WorkBuddy 的分析主要停留在架构和规范层面,对于已经导致运行崩溃的 Bug(如 `plot_wave.py` 格式不匹配、`run_simulation``config` 未定义)没有识别和标注。用户按此文档操作时,可能先花时间做重构,但基础功能仍然崩溃。
**② 性能优化建议较抽象**
对 Python 引擎性能仅建议"使用 Numba JIT"或"numpy.vectorize",缺少具体的向量化代码示例。文档注明"Python vs C 慢约 6-8 倍",但未分析哪个函数是瓶颈(实际上是弹簧力的 Python for 循环)。
**③ 部分建议颗粒度不足**
"消除全局变量"建议封装为 `SimulationState` 数据类,但未说明:封装后接口如何调整、外部引擎的 `param.json` 协议是否需要同步更新、draw.py 的全局变量是否属于同一重构范围。
**④ 优先级排列有待商榷**
将"全局变量封装"列为 P0(与 Fortran 引擎修复同级),但封装全局变量是一项重构工作(3-4h),而 Fortran 引擎崩溃是已存在的功能阻断性问题,两者紧迫性不同。
---
### 1.2 Claude 分析报告评价(`claude.md`
#### 优势
**① Bug 定位精准,附有复现条件**
每个 Bug 都说明了触发条件("只要使用 `engine: python`"、"`step_plot_wave: 1` 时"),以及为什么当前没有爆发("当前所有案例恰好设置 `step_plot: 0`")。这对于开发者复现和验证问题极有价值。
**② 代码级修复方案完整**
每个 Bug 都给出了完整可运行的修复代码,包括需要修改的行号、修改前后的对比。特别是 B3(`plot_wave.py` 格式不匹配)不仅指出问题,还解释了为何修复较复杂(`atom_masses` 等物理量在新格式中缺失,需要同步扩展 header 字段),给出了分层的修复路径(快速修复 vs 彻底修复)。
**③ 弹簧力向量化方案具体可行**
给出了完整的 NumPy 向量化代码,包括处理重复索引的 `np.add.at` 用法,以及链状体系可进一步优化为直接索引的提示。预期加速 5-15 倍的估算也有根据(119 键 × 100,000 步的量级分析)。
**④ 引擎差异的量化说明**
指出了 `engines/cpp/param.json``save_trajectory` 默认值为 1(与 C 引擎的 0 不一致),并说明了实际影响(切换引擎时可能意外生成大文件)。
#### 劣势
**① 覆盖面刻意与 workbuddy.md 错开,造成视野盲区**
claude.md 开篇声明"不与 workbuddy.md 重复",因此主动跳过了架构、测试、配置管理等方向。但这导致文档读者如果只看 claude.md,会误以为架构是没问题的。两份文档需要结合阅读,割裂感较强。
**② 部分建议依赖全局变量方案**
claude.md 提出的 Bug B1 修复方案(将 `camera_distance` 等加入全局变量)是在全局变量模式下的补丁式修复,与 workbuddy.md 建议的"封装全局变量为类"方向相反。若未来执行 workbuddy.md 的重构,B1 的修复需要再次调整。
**③ 某些结论尚未验证**
文档末尾注明"基于代码静态分析,未实际运行测试验证"。例如 B3(`plot_wave.py` 格式不匹配)的结论基于函数调用链分析,如果 `load_text_data` 内部有容错逻辑,实际情况可能不同。
**④ 缺乏对 draw.py 的深入分析**
draw.py 是用户交互最频繁的模块(3D 动画播放),但 claude.md 仅指出了一处裸 `except`(B5),未分析动画帧率优化、大原子数渲染性能等用户体验层面的问题。
---
### 1.3 两份报告对比总结
| 维度 | WorkBuddy | Claude |
|------|-----------|--------|
| **Bug 识别** | ❌ 未发现运行时 Bug | ✅ 识别 5 个确认 Bug,附触发条件 |
| **性能优化** | 🟡 方向正确但缺代码 | ✅ 完整向量化代码示例 |
| **架构设计** | ✅ 完整模块拆分方案 | ❌ 刻意回避,未覆盖 |
| **测试建议** | ✅ pytest 方案具体 | ❌ 未涉及 |
| **配置管理** | ✅ 6 案例统一模板 | ❌ 未涉及 |
| **引擎一致性** | ✅ 参数差异表格 | ✅ 补充了势能归属约定 |
| **修复代码** | 🟡 仅有架构示意 | ✅ 逐条给出可运行代码 |
| **优先级合理性** | 🟡 P0 安排有误 | ✅ 按紧迫性分层清晰 |
| **可独立阅读** | ✅ 自成体系 | ❌ 依赖读者已读 workbuddy.md |
| **当前状态标注** | ✅ 已完成项有标注 | ❌ 未区分 |
**结论**:WorkBuddy 擅长系统性架构分析和工程规范,Claude 擅长 Bug 精确定位和具体修复代码。两份报告互补,单独使用任何一份都存在明显盲区。
---
## 第二部分:综合优化方案
### 原则
基于以上分析,综合方案遵循以下原则:
1. **先止血再手术**:运行时 Bug 优先于架构重构,不能让教学演示功能处于崩溃状态
2. **快速收益优先**:同等重要性时,工作量小的先做
3. **向量化先于重构**:Python 引擎提速不依赖架构改动,可独立进行
4. **渐进式重构**:全局变量封装等重构分阶段进行,避免一次性大改引入新 Bug
---
### 阶段一:紧急修复(总工作量约 4 小时)
> 目标:恢复所有已有功能到正常可用状态
#### 1. 修复 `plot_wave.py` 格式不匹配(B3)— 1.5h
**根本原因**`display.txt` 格式从 JSON 迁移到新文本格式时,`plot_wave.py` 未同步更新。
**修复步骤**
① 修改加载函数(`plot_wave.py:22-27`):
```python
def load_disp_data(output_dir):
disp_path = os.path.join(output_dir, "display.txt")
if not os.path.exists(disp_path):
raise FileNotFoundError(f"找不到 {disp_path}")
return compute.load_display_txt(disp_path) # 改为新格式加载
```
② 在 `save_display_txt``compute.py:217`)的 header 中补充 `atom_masses` 字段:
```python
# 在 header_fields 中添加
"atom_masses": ",".join(str(m) for m in ATOM_MASSES),
```
③ 修改 `plot_wave.py` 中的字段访问:
```python
# 旧 → 新
data["n_frames"] disp_data["frames_x"].shape[0]
data["disp_all_x"] disp_data["frames_x"]
data["atom_masses"] np.array([float(x) for x in h.get("atom_masses","").split(",") if x])
data["atom_ids"] disp_data["atom_ids"]
data["bond_pairs"] [] # display.txt 不含成键信息,能量计算中弹性势能项跳过
```
**验证**:运行 case05`step_plot_wave: 1`),确认生成 `wave_animation.gif`
---
#### 2. 修复 `run_simulation()` 中的 `config` 未定义(B1)— 15min
`compute.py` 模块顶部(约第 68 行,紧随 `camera_keyframes_raw`)添加全局变量:
```python
camera_keyframes_raw = ""
camera_distance = 40.0 # 新增
camera_elevation = 0 # 新增
camera_azimuth = 0 # 新增
```
`run_from_config`(约第 756 行,`use_marker` 赋值附近)添加:
```python
use_marker = int(config.get("use_marker", 0))
camera_distance = float(config.get("camera_distance", 40.0)) # 新增
camera_elevation = float(config.get("camera_elevation", 0)) # 新增
camera_azimuth = float(config.get("camera_azimuth", 0)) # 新增
```
`run_from_config``global` 声明行追加这三个变量:
```python
global use_marker, camera_keyframes_raw, camera_distance, camera_elevation, camera_azimuth
```
`run_simulation`(第 1548-1550 行)改为读全局变量:
```python
"camera_distance": str(camera_distance),
"camera_elevation": str(camera_elevation),
"camera_azimuth": str(camera_azimuth),
```
**验证**:用 `engine: python` 运行 case01,确认生成正确的 `display.txt`
---
#### 3. 修复 `dynamics.py` 绘图块死代码(B2)— 30min
`step_plot` 功能依赖完整轨迹数据,但当前架构中 `run_from_config` 不再在主流程中保留这些数据。
快速修复:添加 "功能暂不可用" 保护,避免用户误开后崩溃:
```python
# dynamics.py 约 322 行
if not no_plot and config.get("step_plot", 1):
print("[run] 注意:step_plot 绘图功能需要 save_trajectory=1 时的完整轨迹,")
print("[run] 当前版本暂未支持,已跳过。如需绘图请联系开发者。")
```
**长期修复**(阶段三再做):从 `display.txt` 读取抽帧数据重建绘图逻辑。
---
#### 4. 修复 Fortran 引擎输出格式(E3)— 2h
Fortran 引擎总是输出 JSON 格式 `trajectory.txt`,与 Python 侧期待的 `display.txt` 不兼容,
使用 `engine: fortran` 时必然崩溃。
参照 C 引擎的 `write_display_txt` 函数(`engines/c/main.c`)在 Fortran 中实现:
1. 读取 `param.json` 中的 `save_trajectory` 参数
2. 新增 `write_display_txt_f90` 子程序,按 NSTEP 抽帧写文本格式
3. 根据 `save_trajectory` 决定是否额外写 `trajectory.txt`
**同时**:将 `engines/cpp/param.json` 中的 `"save_trajectory": 1` 改为 `0`,与 C 保持一致。
---
#### 5. 其他 1-5 分钟的小修复
```python
# B4: examples/case06/run_dynamics.py:34
description="运行 Dynamics 示例案例 case06" # 改 case01 → case06
# B5: draw.py:97
except (ValueError, AttributeError): # 改裸 except
# Q1: dynamics.py 顶部
from compute import _load_camera_motion as _load_camera_kf # 删除重复实现
# E2: engines/cpp/param.json
"save_trajectory": 0 # 统一默认值
```
---
### 阶段二:性能优化(总工作量约 3 小时)
> 目标:Python 引擎提速 5-10 倍,外部引擎减少启动开销
#### 6. 弹簧力向量化(P1)— 1h
`compute.py:1253-1277` 的 Python for 循环替换为 NumPy 向量化版本(见 claude.md §P1 完整代码)。
**链状体系(case01-06)的进一步优化**
由于链状体系每根键的两端没有重复(原子 i 和 j 各只出现一次),可以用更快的直接索引替代 `np.add.at`
```python
# 比 np.add.at 快约 3 倍(无冲突索引时)
fx[i_arr] += fx_bond
fx[j_arr] -= fx_bond
fy[i_arr] += fy_bond
fy[j_arr] -= fy_bond
fz[i_arr] += fz_bond
fz[j_arr] -= fz_bond
```
对于有分叉键的复杂体系仍需用 `np.add.at`,可根据 `BOND_PAIRS` 检测是否有重复端点自动选择路径。
---
#### 7. `apply_fixed_constraints` 预计算掩码(P2)— 30min
`compute.py:1408-1418``column_stack` 方案替换为预计算 bool 掩码,见 claude.md §P2 完整代码。
对 case06(120 原子,x/y 全固定)每步节省 2 次 `(120, 3)` 数组分配。
---
#### 8. 外部引擎校准缓存(E4)— 1h
`compute.py:924` 校准逻辑前添加缓存命中检查:
```python
import hashlib, json as _json
def _calib_cache_key(engine, n_atoms, method, dt):
s = f"{engine}:{n_atoms}:{method}:{dt}"
return hashlib.md5(s.encode()).hexdigest()[:8]
_calib_cache_path = os.path.join(script_dir, "engines", engine, "_calib_cache.json")
_calib_key = _calib_cache_key(engine, len(ATOM_IDS), config.get("method","leapfrog"), float(config["DT"]))
_cached = {}
if os.path.exists(_calib_cache_path):
with open(_calib_cache_path) as _cf:
_cached = _json.load(_cf)
if _cached.get("key") == _calib_key and time.time() - _cached.get("ts", 0) < 86400:
# 缓存命中(1天内有效),跳过校准
_step_time = _cached["step_time"]
_overhead = _cached["overhead"]
else:
# 执行校准(现有逻辑)...
# 校准完成后写缓存
with open(_calib_cache_path, "w") as _cf:
_json.dump({"key": _calib_key, "ts": time.time(),
"step_time": _step_time, "overhead": _overhead}, _cf)
```
**预期收益**:case06 第二次运行节省约 10 秒(校准 10,000 步)。
---
#### 9. 其他微优化(P3/P4)— 30min
```python
# P3: run_simulation 中删除 frame_indices 列表
# 删除: frame_indices = [] 和 frame_indices.append(step)
# 替换: n_frames_actual = record_steps // NSTEP (直接计算)
# P4: apply_driving_force 中删除 t_vec 数组创建
t_vec = np.array([t, t, t], dtype=np.float64) # 删除此行
pos_drive = d["amp"] * np.cos(2.0 * np.pi * d["freq"] * t + d["phi"]) # 直接用标量 t
```
---
### 阶段三:架构重构(总工作量约 15-20 小时)
> 目标:提升长期可维护性,支持并发模拟、单元测试
> **重要说明**:此阶段是 workbuddy.md 建议的核心,以下是对其建议的细化和补充。
> 建议在阶段一、二完成并通过全案例验证后再开始。
#### 10. 全局变量封装为 `SimulationState`workbuddy.md §3.1)— 3-4h
封装后,阶段一中 B1 的补丁修复(新增三个全局变量)可以一并纳入 `SimulationState`,不需要额外保留。
封装时注意:外部引擎路径(`run_engine`)通过 `param.json` 传参,不受全局变量影响,可独立进行。
#### 11. `compute.py` 模块拆分(workbuddy.md §1.1)— 4-6h
拆分时建议**先拆 io.py**(文件读写),因为它与物理算法完全解耦,风险最低。
拆分顺序建议:`io.py``params.py``core.py``runner.py`
**补充**:同步删除已废弃的 `load_parameters()` 函数(102 行)和已被 `dynamics.py` 取代的 `compute.main()`25 行),见 claude.md §Q2。
#### 12. 添加单元测试(workbuddy.md §5.1)— 3-5h
优先覆盖以下三类:
1. 物理算法正确性:以简谐振子解析解验证 leapfrog、midpoint 方法(参照 `tools/NumericalMethods.py` 已有实现)
2. 文件 I/O 往返一致性:`save_display_txt → load_display_txt` 数值不变
3. 参数验证边界:质量为 0、键长为负、NT 为 0 等异常输入
#### 13. 案例 `input.txt` 格式统一(workbuddy.md §4.1)— 1-2h
`case01-05``input.txt` 补充缺失字段(`save_trajectory``camera_*``move_camera`),
统一 `step_sample: 0`(新版引擎已内置抽帧),清理旧格式注释。
#### 14. `dynamics.py` 绘图功能完整重建(B2 长期修复)— 2h
`display.txt` 的抽帧数据重建绘图逻辑(替代依赖完整轨迹的旧实现):
```python
disp_data = compute.load_display_txt(disp_path)
frames_x = disp_data["frames_x"] # (n_frames, n_atoms)
# 对每帧计算能量,绘制时序图
```
能量计算需要质量等物理量,依赖阶段一 B3 修复中在 header 补充的 `atom_masses` 字段。
---
### 三个引擎的综合评价
#### C 引擎(`engines/c/main.c`
| 维度 | 评价 |
|------|------|
| **功能完整性** | ✅ 最完整,支持 `display.txt` 直接输出,`save_trajectory` 控制 |
| **性能** | ✅ 基准引擎,比 Python 快 6-8 倍 |
| **一致性** | ✅ `save_trajectory` 默认值为 0,与 Python 行为一致 |
| **代码质量** | 🟡 JSON 解析为自行实现(非 cJSON 库),维护成本高 |
| **建议** | 作为参考实现,其他引擎向 C 引擎看齐 |
#### C++ 引擎(`engines/cpp/main.cpp`
| 维度 | 评价 |
|------|------|
| **功能完整性** | ✅ 功能与 C 引擎相当 |
| **性能** | 🟡 编译产物 3.3 MB(远大于 C 的 504 KB),有优化空间 |
| **一致性** | ⚠️ `save_trajectory` 默认值为 1(与 C 不一致),已在阶段一修复 |
| **代码质量** | 🟡 物理算法与 C 引擎重复,应提取到公共头文件 |
| **建议** | 统一 `param.json` 默认值;考虑与 C 引擎共用物理算法头文件 |
#### Fortran 引擎(`engines/fortran/main.f90`
| 维度 | 评价 |
|------|------|
| **功能完整性** | ❌ 不支持 `display.txt` 直接输出,使用时必然崩溃 |
| **性能** | ✅ 编译性能与 C 相当,适合大规模计算 |
| **一致性** | ❌ 输出 JSON 格式 trajectory.txt,与其他引擎行为完全不同 |
| **代码质量** | 🟡 代码结构清晰,但与 C/C++ 引擎物理算法重复 |
| **建议** | 阶段一 E3 修复为最高优先级;修复后可成为高性能替代引擎 |
---
### 综合优先级总表
| 阶段 | 编号 | 问题 | 来源 | 优先级 | 预估工时 |
|------|------|------|------|--------|---------|
| 一 | B3 | `plot_wave.py` 格式不匹配(波形动画失效) | Claude | 🔴 P0 | 1.5h |
| 一 | E3 | Fortran 引擎不支持 display.txt | 两者 | 🔴 P0 | 2h |
| 一 | B1 | `run_simulation``config` 未定义 | Claude | 🔴 P0 | 15min |
| 一 | B2 | `dynamics.py` 绘图块死代码 | Claude | 🔴 P0 | 30min |
| 一 | B4/B5 | 小错误修复(case06 文字、裸 except 等) | Claude | 🟡 P1 | 15min |
| 一 | Q1/E2 | 重复函数去除、C++ 默认值统一 | Claude | 🟡 P1 | 20min |
| 二 | P1 | 弹簧力向量化(Python 引擎 5-15x | Claude | 🟢 P1 | 1h |
| 二 | E4 | 外部引擎校准缓存 | 两者 | 🟢 P1 | 1h |
| 二 | P2-P4 | apply_fixed_constraints 等微优化 | Claude | 🟢 P2 | 1h |
| 三 | 架构 | 全局变量封装为 SimulationState | WorkBuddy | 🔵 P2 | 3-4h |
| 三 | 架构 | compute.py 模块拆分 | WorkBuddy | 🔵 P2 | 4-6h |
| 三 | 测试 | 添加 pytest 单元测试 | WorkBuddy | 🔵 P2 | 3-5h |
| 三 | 配置 | 6 案例 input.txt 格式统一 | WorkBuddy | 🔵 P3 | 1-2h |
| 三 | 绘图 | dynamics.py 绘图功能重建 | Claude | 🔵 P3 | 2h |
| 三 | 引擎 | C/C++/Fortran 共用物理算法头文件 | WorkBuddy | 🔵 P3 | 2-4h |
---
## 附录:快速执行清单
### 今天可以完成(总计约 30 分钟的小修复)
```
[ ] examples/case06/run_dynamics.py:34 "case01" → "case06"
[ ] draw.py:97 裸 except → except (ValueError, AttributeError):
[ ] dynamics.py 删除 _load_camera_kf(),改为从 compute 导入
[ ] engines/cpp/param.json "save_trajectory": 1 → 0
[ ] compute.py:1548-1550 config.get → 全局变量(B1 修复)
```
### 本周可以完成(核心功能恢复)
```
[ ] plot_wave.py 格式适配(B3
[ ] dynamics.py 绘图块保护(B2 临时修复)
[ ] Fortran 引擎 display.txt 支持(E3
[ ] 弹簧力向量化(P1
[ ] 引擎校准缓存(E4
```
### 下阶段规划(架构改善)
```
[ ] 全局变量 → SimulationState 类
[ ] compute.py 拆分为子模块
[ ] pytest 单元测试套件
[ ] 案例 input.txt 格式统一
```
---
*本文档综合 Claude Sonnet 4.6`claude.md`)与 WorkBuddy`workbuddy.md`)的分析报告生成。
所有代码改动建议均基于静态分析,实施前请先在独立分支验证。*
+351
View File
@@ -0,0 +1,351 @@
# Dynamics 项目优化建议
本文基于对以下文件的静态检查整理:
- `compute.py`
- `dynamics.py`
- `draw.py`
- `engines/c/main.c`
- `README.md`
目标不是泛泛地“提速”,而是优先找出这个项目当前最可能影响性能、内存占用、可维护性和后续扩展效率的点,并给出按优先级排序的改进方向。
## 一、整体判断
这个项目现在的架构已经有一个很好的基础:`dynamics.py` 做流程编排,`compute.py` 做 Python 参考实现,`engines/c/main.c` 提供高性能引擎,`draw.py` 负责可视化。
从代码结构看,当前主要瓶颈不在“外层调度”,而集中在三类地方:
1. `compute.py` 中逐步积分时的 Python 层循环。
2. `compute.py` 中弹簧力/粒子间引力的双层或逐键循环。
3. `display.txt` / `trajectory.txt` 的文本格式读写。
如果后续案例规模继续增大,Python 参考引擎会很快被这三处放大;而且即便切到 C 引擎,文本 I/O 仍然会成为新的主要瓶颈。
## 二、最高优先级建议
## 1. 先把“计算核心”和“输出格式”分开优化
当前项目已经有多语言引擎,这是很正确的方向。建议把优化目标拆成两条线分别推进:
- 计算性能:优先优化 `compute.py` 中的力计算和积分主循环。
- I/O 性能:优先替换或补充 `display.txt` / `trajectory.txt` 的文本格式。
原因:
- 计算核心的瓶颈主要是 CPU。
- 轨迹输出的瓶颈主要是字符串格式化、磁盘写入、加载解析。
- 这两类优化手段完全不同,混在一起做容易看不出收益来源。
建议先增加一个简单的 profiling 开关,例如输出:
- 总模拟时间
- 力计算时间
- 抽帧时间
- 保存 `display.txt` 时间
- 保存 `trajectory.txt` 时间
- 加载 `display.txt` 时间
这样后面每做一次优化,都能知道收益来自哪一段。
## 2. 优先向量化 `compute_force()` 里的弹簧键计算
`compute.py:1230` 开始的 `compute_force()` 是 Python 引擎最关键的热点。
其中弹簧键部分目前是:
- 遍历 `BOND_PAIRS`
- 每根键单独算 `dx/dy/dz`
- 每根键单独回写两个原子的受力
这在键数增大后会变成明显瓶颈。
建议做法:
-`BOND_PAIRS[:, 0]``BOND_PAIRS[:, 1]` 拆成两个索引数组 `i_idx``j_idx`
- 用 NumPy 一次性计算所有键的 `dx/dy/dz/dist/stretch`
-`np.add.at()` 或等价的 scatter 累加方式,把键力一次性回写到 `fx/fy/fz`
预期收益:
- 中等规模体系下,Python 参考引擎会有很可观的提速。
- 这也是最不改变项目结构、最容易验证正确性的优化。
## 3. 把原子间万有引力视为“可选高成本模块”,不要默认走 Python 双层循环
`compute.py:1281` 开始的 `GRAVITY_INTERACTION` 采用 `for i` / `for j` 双层循环,复杂度是 `O(N^2)`
这段代码在教学上没问题,但在稍大的粒子数下会非常慢。建议:
- 明确把它标记为“仅适合小规模案例”。
- 默认推荐用户用 `engine: c` 跑这类 case。
- 如果未来仍想保留 Python 版本,优先考虑:
- 小规模时保持现状。
- 中大规模时改成 Numba 或 C 扩展。
- 更进一步再考虑 Barnes-Hut、cell list、neighbor list 之类近似/加速算法。
如果只是当前项目阶段,我更建议:
- 不要先在 Python 里硬做复杂天体优化。
- 先把这类高复杂度场景明确导向 C 引擎。
这是投入产出比更高的路线。
## 4. `display.txt` 文本格式要补一个二进制版本
`compute.py:196``save_display_txt()``compute.py:239``load_display_txt()` 已经比 JSON 好很多,但本质仍然是大文本。
当前问题:
- 保存时每个数都在做字符串格式化。
- 读取时虽然用了 `np.genfromtxt`,但源数据仍是文本。
- 帧数和粒子数继续增大后,I/O 会成为明显瓶颈。
建议保留 `display.txt` 作为“人可读格式”,同时新增一个高性能格式,例如:
- `display.npz`
-`display.npy` + `meta.json`
推荐方案:
- `frames_x/y/z/vx/vy/vz` 存到 `np.savez_compressed`
- 元信息单独存一个轻量 `meta.json`
- `draw.py` 优先读二进制,不存在时再回退到 `display.txt`
这样有几个好处:
- 调试和教学仍可保留文本文件。
- 大规模运行时可以直接避开文本解析开销。
- Python 和 C 引擎都可以逐步迁移,不需要一次切完。
## 三、中优先级建议
## 5. `run_simulation()` 里完整轨迹缓存要改成“按需流式写出”
`compute.py:1481` 起,如果 `save_trajectory=1`,会一次性申请:
- `traj_x`
- `traj_y`
- `traj_z`
- `traj_vx`
- `traj_vy`
- `traj_vz`
这意味着内存复杂度接近 `O(steps * atoms)`,而且还是 6 份 `float64` 数组。
这在教学小案例里没问题,但一旦:
- `NT` 很大
- 粒子数上来
- 同时还保留抽帧缓存
内存会膨胀得很快。
建议:
- 如果只是为了最终导出,改成边算边写。
- 如果还需要后续随机访问,可以改成 `memmap` 或分块写入 `npz/hdf5/zarr`
推荐优先级:
- 先做“保存完整轨迹时改为 chunked binary writer”。
- `trajectory.txt` 保留为兼容输出,而不是默认主输出。
## 6. `draw.py` 的 Marker 更新应一次性切片赋值,避免逐原子 Python 循环
`draw.py:517` 附近的 `_update_atom_positions()``USE_MARKER` 模式下仍然逐原子循环:
-`for i in range(N_ATOMS)`
- 再逐个写 `marker_pos[i]`
- 然后 `balls.set_data(pos=marker_pos)`
建议直接改成:
- `marker_pos[:, 0] = DISP_ALL_X[f_idx]`
- `marker_pos[:, 1] = DISP_ALL_Y[f_idx]`
- `marker_pos[:, 2] = DISP_ALL_Z[f_idx]`
这样更符合 Marker 模式“批量更新”的初衷。
同理,成键线 `_update_bond_positions()` 也可以进一步尝试批量索引构造,而不是逐键更新。
虽然这部分通常不如计算核心慢,但在大粒子数动画里会直接影响帧率。
## 7. `apply_fixed_constraints()` 每步构造 `column_stack`,可以改成原地掩码写回
`compute.py:1408` 的实现每步都会:
- `np.column_stack((x, y, z))`
- `np.column_stack((vx, vy, vz))`
- `np.where(...)`
这会产生额外临时数组。
建议改成:
- 预先缓存 `fixed_x/fixed_y/fixed_z` 三个布尔掩码
- 直接对 `x/y/z/vx/vy/vz` 原地赋值
例如思路上改成:
- `x[fixed_x] = ATOM_POSITIONS[fixed_x, 0]`
- `vx[fixed_x] = 0.0`
这类优化单项收益不一定最大,但由于它在每一步都执行,累计下来是有价值的。
## 8. `apply_driving_force()` 有重复小数组分配
`compute.py:599` 的驱动力逻辑中,每个 driver、每一步都构造:
- `t_vec = np.array([t, t, t], dtype=np.float64)`
这是典型的小对象重复分配。
建议:
- 直接分别计算三个轴,不需要生成 `t_vec`
- 或预先把 driver 参数整理成矩阵,批量更新受驱原子
如果 driver 数量很少,这不是最大瓶颈;但这类微优化很容易做,而且不会增加复杂度。
## 四、架构与可维护性建议
## 9. 减少 `compute.py` 的全局变量依赖,逐步收敛到状态对象
目前 `compute.py` 依赖大量全局变量,例如:
- `ATOM_IDS`
- `ATOM_MASSES`
- `BOND_PAIRS`
- `METHOD`
- `NT`
- `DT`
这让代码在以下场景下会越来越难维护:
- 并行运行多个 case
- 写单元测试
- 替换不同 force model
- 将来做 GUI 或服务化封装
建议中期做一个 `SimulationState` / `SimulationConfig` / `SystemData` 分层:
- 配置类:步长、方法、开关、输出选项
- 系统类:原子、键、边界、驱动参数
- 状态类:当前 `x/y/z/vx/vy/vz`
不需要一次性重构完,先从最核心的 `compute_force()``run_simulation()` 入手即可。
## 10. Python 参考引擎和 C 引擎的“功能等价层”建议更明确
README 里已经强调了多语言对比,这是项目亮点。为了后续更稳,建议把“等价层”标准化:
- 相同输入
- 相同输出语义
- 相同采样规则
- 相同边界行为
- 相同 driver 行为
然后做一个最小一致性测试集:
- case01-case06 都能跑
- Python 与 C 输出在容差内一致
- 不同 method 的结果有基准对照
这样后续做任何优化时,都更容易大胆改,不怕悄悄改坏物理行为。
## 五、需要尽快处理的正确性/维护风险
这些不一定直接是“性能问题”,但会影响后续优化效率,建议优先修一下。
## 11. `dynamics.py` 的绘图分支存在明显变量依赖不完整的风险
`dynamics.py:331` 一带,绘图代码直接使用:
- `all_x`
- `all_y`
- `all_z`
- `all_vx`
- `all_vy`
- `all_vz`
- `data`
但从当前文件上下文看,这些变量只在某些分支里才会存在,尤其 Python 引擎路径下很可能未定义。
这会带来两个问题:
- 某些 case 可能直接在绘图阶段报错。
- 优化时很难判断问题来自性能还是流程分支。
建议:
- 在进入绘图逻辑前统一构造标准数据对象。
- 不要依赖分支里“顺便留下来的局部变量”。
## 12. `README.md` 描述与当前实现已经有部分不一致
例如 README 仍强调:
- `trajectory.txt (JSON, 统一格式)`
- `sample.py -> display.txt`
但当前实现里:
- Python 路径已经直接写 `display.txt`
- `save_trajectory` 变成可选
- `sample.py` 在主流程中的角色已经下降
文档不一致本身不会拖慢程序,但会拖慢后续协作和排障效率,尤其在你继续演进输出格式时会更明显。
建议在做 I/O 优化时顺手更新 README,避免认知分叉。
## 六、推荐实施顺序
如果按“最少改动获得最大收益”的原则,我建议这样排:
1.`dynamics.py` / `compute.py` 增加基础计时统计。
2. 向量化 `compute_force()` 的弹簧键计算。
3.`draw.py` 的 Marker 更新改成切片赋值。
4. 新增 `display.npz``draw.py` 优先读取二进制。
5.`save_trajectory=1` 改成分块二进制输出,而不是全量内存缓存。
6. 修复 `dynamics.py` 绘图分支的数据来源问题。
7. 再考虑 `compute.py` 全局变量收敛和更深层的结构重构。
## 七、如果只做三件事,最值得做什么
如果你现在只想投入一小轮精力,我建议只做这三项:
1. 向量化 `compute_force()` 的弹簧键部分。
2. 增加 `display.npz` 二进制输出与读取。
3. 修正 `dynamics.py` 绘图阶段的数据流一致性。
原因:
- 第 1 项直接优化 Python 引擎核心热点。
- 第 2 项直接优化所有引擎共享的 I/O 瓶颈。
- 第 3 项能降低后续改动时的维护风险。
这三项一起做,收益通常比零散微优化更明显。
## 八、结论
这个项目最值得肯定的地方,是已经天然分成了:
- 参考实现
- 高性能引擎
- 统一可视化管线
这意味着它非常适合做“分层优化”,不需要推倒重来。
从当前代码看,后续最有效的路线不是继续在外围加流程判断,而是:
- 把 Python 热点循环尽量向量化
- 把文本轨迹格式逐步替换为二进制主格式
- 把数据流和状态管理收紧
如果按这个方向推进,这个项目会同时得到:
- 更好的性能
- 更低的内存占用
- 更稳定的多引擎一致性
- 更容易继续扩展新的物理项和可视化形式
+341
View File
@@ -0,0 +1,341 @@
# Dynamics 优化方案综合评估
> 分析日期:2026-06-12
> 评估对象:`optimization/claude.md`、`optimization/codex.md`、`optimization/workbuddy.md`
> 说明:用户问题中出现两次 `claude.md`,但目录内实际为三份不同文档,因此本文按 **Claude / Codex / Workbuddy** 三个工具的输出进行综合分析。
## 一、结论先行
这三份建议并不是互相冲突,而是分别代表了三种不同但互补的优化视角:
- **Claude**:最像“代码审计员”,擅长找出会崩溃、会出错、会不一致的具体问题。
- **Codex**:最像“性能工程师”,擅长定位热点路径、区分 CPU 与 I/O 瓶颈、给出高收益优化顺序。
- **Workbuddy**:最像“架构负责人”,擅长从模块拆分、全局变量治理、配置统一、测试体系等角度规划长期演进。
如果只选一个方案:
- 短期救火,优先参考 **Claude**
- 中期提速,优先参考 **Codex**
- 长期重构,优先参考 **Workbuddy**
如果要形成真正适合这个项目的落地方案,最佳做法不是三选一,而是:
1. 先按 **Claude** 修 correctness bug
2. 再按 **Codex** 做性能主线优化
3. 最后按 **Workbuddy** 做结构化治理
---
## 二、三款工具的评价
## 1. Claude
### 优势
- **最强的 bug 发现能力**
`claude.md` 明确指出了几个高价值问题,例如:
- `compute.py``run_simulation()` 直接引用未传入的 `config`
- `dynamics.py` 绘图分支使用未定义变量
- `plot_wave.py` 仍按旧格式读取新 `display.txt`
这些问题都属于“不是慢,而是可能直接错或崩”的问题,优先级非常高。
- **问题定位具体,能直接改代码**
Claude 的输出不是抽象建议,而是带着明确文件位置、触发条件和修复方向,适合立即进入修复。
- **对多引擎一致性比较敏感**
它注意到了 Python/C/C++/Fortran 之间在 `save_trajectory`、势能分配、输出格式支持等方面的不一致,这对多语言框架很重要。
### 劣势
- **整体视角偏“局部修补”**
它非常擅长发现局部 bug,但对“整体性能主瓶颈怎么排优先级”没有 Codex 那么系统。
- **长期架构建议相对弱一些**
虽然也提到一些结构问题,但没有 Workbuddy 那种模块拆分、配置治理、测试体系的完整路线。
- **偏静态审计,偏保守**
更适合“先修对”,不一定最适合“先做最值的性能投资”。
### 适合的角色
- 第一轮排雷
- 回归前 bug 清单
- 多引擎一致性核对
---
## 2. Codex
### 优势
- **性能视角最清晰**
`codex.md` 非常明确地区分了三类瓶颈:
- Python 计算循环
- 力学计算中的逐键/双层循环
- 文本 I/O
这种分层对项目很有价值,因为它避免把“算法慢”和“文件慢”混成一个问题。
- **优化顺序合理,投入产出比高**
它提出的几个优先项都很务实:
- 弹簧力向量化
- `display.txt` 增加二进制格式
- `draw.py` Marker 批量更新
这些建议的共同特点是:
- 不需要推翻现有架构
- 可验证
- 回报高
- **能识别共享瓶颈**
它特别强调:即使换成 C 引擎,文本 I/O 仍然会拖后腿。这种判断比只盯 Python 引擎更深入。
### 劣势
- **对当前代码中的直接 bug 关注度不如 Claude**
虽然也指出了 `dynamics.py` 数据流和 README 不一致等风险,但没有 Claude 那么系统地清点“哪些地方一开功能就报错”。
- **工程治理不如 Workbuddy 完整**
它提到全局变量和状态对象,但没有把模块拆分、测试体系、案例配置统一讲得那么全面。
- **更偏策略,不总是落到立即可改的具体 patch**
对项目 owner 来说这很好,但如果是要当天就修 bug,Claude 会更直接。
### 适合的角色
- 性能优化主方案
- 技术债排序
- 中期研发路线设计
---
## 3. Workbuddy
### 优势
- **最强的工程化和长期治理视角**
`workbuddy.md` 关注的重点包括:
- `compute.py` 拆模块
- 消除全局变量
- 配置结构统一
- 测试体系建设
- Fortran/C/C++ 引擎功能对齐
这类建议对项目走向稳定维护很关键。
- **适合把个人项目变成可持续项目**
它不是只盯某一个性能点,而是在思考这个项目未来怎么更好地维护、测试、扩展。
- **对“项目管理成本”比较敏感**
比如配置格式不统一、废弃文件残留、测试缺失,这些短期不一定影响运行,但长期一定影响开发效率。
### 劣势
- **短期收益不如另外两者明显**
比如模块拆分、类型标注、状态封装,这些很重要,但如果当前主要痛点是“功能坏了”或“运行太慢”,它们不会立刻让用户感受到变化。
- **有些建议偏大改**
`compute.py` 模块拆分、全局变量全面状态化,工作量不小,如果现在马上做,容易把项目带入较长重构周期。
- **对现存 bug 的敏锐度不如 Claude,对热点性能的聚焦不如 Codex**
它更像制定制度和框架的人,而不是先冲到一线止血的人。
### 适合的角色
- 中长期架构治理
- 重构路线设计
- 代码库标准化与测试建设
---
## 三、三者对比总结
| 维度 | Claude | Codex | Workbuddy |
|------|--------|-------|-----------|
| 找 bug | 最强 | 中等 | 中等 |
| 找性能瓶颈 | 强 | 最强 | 中等 |
| 架构治理 | 中等 | 强 | 最强 |
| 可直接落地修复 | 最强 | 强 | 中等 |
| 中期路线规划 | 中等 | 最强 | 强 |
| 长期工程化 | 中等 | 强 | 最强 |
| 风险意识 | 最强 | 强 | 强 |
| 适合当前项目阶段 | 很适合当前排雷 | 很适合当前提速 | 很适合下一阶段治理 |
可以概括为:
- **Claude 更像 QA/审计**
- **Codex 更像性能工程师**
- **Workbuddy 更像架构经理**
---
## 四、综合判断
从这三份文档综合看,`Dynamics` 项目当前不是单一问题,而是三类问题叠加:
1. **已有功能正确性问题**
- 某些路径会直接崩溃
- 某些脚本已经和新格式脱节
- 多引擎行为存在不一致
2. **性能与 I/O 问题**
- Python 引擎热点循环还没做足够向量化
- `display.txt`/`trajectory.txt` 文本格式开始成为共享瓶颈
- 动画路径里还有逐原子更新
3. **工程治理问题**
- `compute.py` 过大
- 全局变量过多
- 配置不统一
- 缺少测试
因此,最合理的综合方案必须是 **分阶段方案**,而不是试图一次性全做。
---
## 五、推荐的综合方案
## 阶段 A:先修正确性,避免“带病优化”
这一阶段以 Claude 的结论为主,目标是先把明显会崩溃、会错的地方修掉。
建议优先做:
1. 修复 `compute.py``run_simulation()``config` 的非法引用
2. 修复 `dynamics.py` 绘图分支中未定义变量的死代码问题
3. 修复 `plot_wave.py` 对新 `display.txt` 格式的不兼容
4. 统一外部引擎在 `save_trajectory`、输出格式上的行为
5. 清理少量明显错误与危险写法
- case06 文案错误
- `draw.py``except`
这一阶段的目标不是提速,而是让系统进入“功能可信、路径可跑”的状态。
---
## 阶段 B:再做高收益性能优化
这一阶段以 Codex 的结论为主,重点是先做收益大、改动相对可控的优化。
建议优先做:
1.`compute.py` / `dynamics.py` 增加基础 profiling
- 总耗时
- 力计算耗时
- 抽帧耗时
- I/O 耗时
2. 向量化 `compute_force()` 中的弹簧键计算
3.`draw.py` 中将 Marker 更新改成整列切片赋值
4. 新增二进制显示格式
- 推荐 `display.npz`
- `draw.py` 优先读二进制,回退读文本
5. 将完整轨迹保存改为分块/流式写出
这一步完成后,项目会得到最直观的收益:
- Python 引擎更快
- C 引擎的后处理更快
- 大规模 case 的内存压力更低
---
## 阶段 C:最后做工程治理与结构化重构
这一阶段以 Workbuddy 的结论为主,目标是让项目从“能用”变成“好维护”。
建议优先做:
1. 拆分 `compute.py`
- `core.py`
- `io.py`
- `params.py`
- `runner.py`
- `engine_helper.py`
2. 引入 `SimulationState` / `SimulationConfig` / `SystemData`
- 逐步消灭模块级全局变量
3. 统一案例配置格式
- 六个案例字段保持一致
- 将颜色、相机等零散字段收束成结构化配置
4. 建立最小测试体系
- 数值方法单元测试
- I/O 一致性测试
- Python/C 输出一致性测试
5. 清理废弃脚本和旧逻辑
- 尤其是已经不再处于主流程中的脚本
这一步不会像阶段 B 那样马上体现性能收益,但它会显著降低后续维护成本。
---
## 六、最终推荐方案
如果只给一个“综合最优”的执行方案,我建议采用下面这个顺序:
1. **先按 Claude 修 bug**
- 因为错误路径不先修,后面性能优化和重构都会建立在不稳定基础上
2. **再按 Codex 做性能主线**
- 因为当前性能瓶颈已经很清楚,尤其是 Python 热点和文本 I/O
3. **最后按 Workbuddy 做结构治理**
- 因为工程化重构最适合在功能稳定、热点清楚之后进行
换句话说:
- **Claude 决定“先修什么”**
- **Codex 决定“先优化什么”**
- **Workbuddy 决定“最后把系统整理成什么样”**
这是三份建议里最不冲突、也最符合实际开发节奏的组合方式。
---
## 七、简版执行清单
如果要把这份综合方案压缩成一份实际待办,我建议如下:
### P0:立即处理
-`run_simulation()``config` 作用域错误
-`dynamics.py` 绘图分支未定义变量
-`plot_wave.py` 与新 `display.txt` 格式不兼容
- 校正多引擎 `save_trajectory` / 输出行为不一致
### P1:本轮优化主线
- 增加 profiling
- 向量化弹簧力计算
- `draw.py` Marker 批量更新
- 新增 `display.npz`
- 完整轨迹改为分块写出
### P2:下一轮治理
- 拆分 `compute.py`
- 全局变量状态化
- 统一 `input.txt` 结构
- 建立 pytest 测试
- 清理废弃脚本与旧文档
---
## 八、最终评价
三款工具的输出质量都不错,但擅长点不同。
- **Claude** 最适合当前这个项目的“第一步”,因为它最能发现会直接影响运行正确性的缺陷。
- **Codex** 最适合当前这个项目的“第二步”,因为它对性能瓶颈的层次判断最清楚,最容易产出高收益优化。
- **Workbuddy** 最适合当前这个项目的“第三步”,因为它能把一套已经能跑的系统整理成一套更稳定、更易维护的工程。
因此,综合最优解不是选其中一个,而是:
**用 Claude 排雷,用 Codex 提速,用 Workbuddy 收尾治理。**
+303
View File
@@ -0,0 +1,303 @@
# Dynamics 项目优化建议
> 分析日期:2026-06-12
> 版本:main (43 commits)
---
## 1. 架构优化
### 1.1 compute.py 拆分(高优先级)
**现状**`compute.py` 1618 行,混合了以下职责:
- 物理引擎(数值积分方法 ~10 个函数)
- 文件 I/OJSON/text 读写)
- 外部引擎管理(subprocess 调用)
- 参数加载与解析
- 主入口 main()
**建议**:按职责拆分为独立模块
```
compute/
├── __init__.py # 导出公共 API
├── core.py # 物理引擎: 数值积分方法, 力的计算 (~200 行)
├── io.py # 文件 I/O: save/load display/trajectory (~300 行)
├── params.py # 参数加载: load_coord_file, load_bond 等 (~150 行)
├── runner.py # 运行管理: run_simulation, run_engine, run_from_config (~200 行)
├── engine_helper.py # 外部引擎: 校准, subprocess, 进度监控 (~150 行)
└── main.py # 主入口: main(), if __name__ == "__main__" (~30 行)
```
**收益**:可单独测试物理算法、文件读写、引擎调用
### 1.2 外部引擎代码重构(中优先级)
**现状**C/C++/Fortran 引擎各自实现了一套:
- JSON 读取/写入(3 套独立实现)
- 物理算法(复制粘贴)
- display.txt / trajectory.txt 输出
**建议**
- C 和 C++ 引擎共用头文件/函数库
- 物理算法提取为公共库(`engines/common/`
- Fortran 更新支持 `save_trajectory` 兼容
---
## 2. 性能优化
### 2.1 外部引擎校准耗时(高)
**现状**`run_engine()` 每次执行前先跑 1000 步校准测速(`_calib_nt = min(1000, max(100, total_steps // 10))`),对短时运行(~1000 步)校准占比高达 50%。
**建议**
- 缓存校准结果:按 `(engine, NT, DT, NSTEP, n_atoms)` 哈希缓存
- 或对短运行 (<5000 步) 跳过校准
- 或只在校准目录为空时执行
```python
# 建议改动
_calib_cache = os.path.join(script_dir, "engines", engine, "_calib_cache.json")
if os.path.exists(_calib_cache):
with open(_calib_cache) as f:
_calib_data = json.load(f)
if _calib_data.get("key") == _calib_key:
_step_time = _calib_data["step_time"]
_overhead = _calib_data["overhead"]
skip_calibration()
```
### 2.2 display.txt 读取加速(已完成)
**现状**:已从逐行 `split()+float()` 改为 `np.genfromtxt()` 批量解析,200帧×120原子 加载仅 0.087s(测试已通过)。
### 2.3 Python 引擎性能(低)
**现状**:Python 引擎比 C 引擎慢约 6-8 倍(43s vs 7s for 10 万步)。对 50 万步以上的长时间模拟不适用。
**建议**
- 使用 Numba JIT 加速力的计算
- 或使用 `numpy.vectorize` 替代 Python 循环(力的计算中已部分使用 numpy 数组操作)
- 文档中注明 Python 引擎仅适用于小规模测试
### 2.4 校准数据的进度条(已修复)
校准后进度条使用实时帧号更新,不再使用时间估算,避免了 `0% → 100%` 跳变。但注意校准本身不显示进度。
---
## 3. 代码质量
### 3.1 消除全局变量(高优先级)
**现状**`compute.py` 定义了 40+ 个模块级全局变量:
```python
box_a = None
alpha = None
ATOM_IDS = None
...
```
所有算法函数直接依赖这些全局变量,导致:
- 无法并发运行两个模拟
- 无法单元测试单个函数
- 函数签名无法自文档化
**建议**:将全局状态封装为 `SimulationState` 类:
```python
@dataclass
class SimulationState:
box_a: float = 10.0
atom_ids: np.ndarray = None
atom_masses: np.ndarray = None
...
def leapfrog(state: SimulationState, ...):
...
```
### 3.2 draw.py 全局变量(中优先级)
**现状**:类似的问题,`draw.py` 用了 ~30 个模块级变量:
```python
DISPLAY_X = ...
DISP_ALL_X = ...
N_FRAMES = ...
frame_idx = 0
```
**建议**:封装为 `AnimationData``CameraState`
### 3.3 废弃文件清理(低优先级)
以下文件可能已废弃或重复:
| 文件 | 行数 | 判断 |
|------|------|------|
| `sample.py` | 184 | 已不再被调用(抽帧已移至引擎内部) |
| `build_release_zip.py` | - | 打包脚本,非核心功能 |
| `export_web_data.py` | - | 导出脚本,非核心功能 |
| `migrate_npz_outputs.py` | - | 迁移脚本,一次性使用 |
| `tools/NumericalMethods.py` | - | 可能已过时 |
| `build/` (CMake 相关) | - | 与独立引擎编译无关 |
---
## 4. 配置管理
### 4.1 input.txt 格式统一(高优先级)
**现状**6 个案例的 `input.txt` 格式不统一:
| 字段 | case01 | case06 |
|------|--------|--------|
| `save_trajectory` | ❌ 缺失 | ✅ 有 |
| `camera_distance/elevation/azimuth` | ❌ 缺失 | ✅ 有 |
| `move_camera` | ❌ 缺失 | ✅ 有 |
| `step_sample` | 仍然写 1 | 已改为 0 |
| `step_plot`配置 | 旧注释格式 | 新注释格式 |
**建议**
```yaml
# 统一模板
flow:
step_simulate: 1
step_sample: 0 # 引擎已内置抽帧
step_animation: 1
save_trajectory: 0 # 默认不保留
physics:
box_a: 10.0
G: [0, 0, -9.8]
method: leapfrog
T_total: 10.0
NSTEP: 100
DT: 0.001
render:
use_marker: 0
alpha: [0.0]*6
ball_color: [0.9, 0.2, 0.2]
box_color: [0.8, 0.8, 0.85]
camera:
distance: 40.0
elevation: 0
azimuth: 0
move: false
```
### 4.2 YAML 结构优化(中优先级)
**现状**`ball_color_r/g/b` 三个独立的键,YAML 列表更自然:
```yaml
# 当前
ball_color_r: 0.90
ball_color_g: 0.20
ball_color_b: 0.20
# 建议
ball_color: [0.90, 0.20, 0.20]
```
---
## 5. 测试与质量
### 5.1 缺少测试(高优先级)
**现状**:无单元测试、无集成测试。每次修改只能手动跑案例。
**建议**
-`compute.py` 的物理算法(Leapfrog、Euler 等)添加 pytest 测试
- 为文件 I/O`save_display_txt` / `load_display_txt` 读写一致性)添加测试
- 添加一个 `examples/case00` 最小验证案例(2 个原子,10 步)
- CI`pytest` + 简单的集成测试脚本
### 5.2 缺少类型标注(中优先级)
```python
# 现状
def leapfrog(x, y, z, vx, vy, vz, dt, m, g, b):
# 建议
def leapfrog(
x: np.ndarray, y: np.ndarray, z: np.ndarray,
vx: np.ndarray, vy: np.ndarray, vz: np.ndarray,
dt: float, m: np.ndarray, g: np.ndarray, b: np.ndarray
) -> tuple[np.ndarray, np.ndarray, np.ndarray,
np.ndarray, np.ndarray, np.ndarray]:
```
---
## 6. 引擎一致性
### 6.1 Fortran 引擎更新(高)
**现状**Fortran 引擎**不支持** `save_trajectory=0` 模式,总是写 JSON 格式的 `trajectory.txt`。需要:
1. 读取 `save_trajectory` 参数
2. 计算时按 NSTEP 采样
3. 写 display.txt 新格式
4. 按条件跳过 trajectory.txt 输出
参考 C 引擎的 `write_display_txt` 实现(约 40 行代码)。
### 6.2 参数默认值一致性(中)
各引擎的 `SimParams` 默认值不一致:
| 参数 | C | C++ | Fortran |
|------|---|---|---------|
| `box_a` | 无默认 | 10.0 | 10.0 (代码中) |
| `method` | leapfrog | leapfrog | leapfrog |
| `save_trajectory` | 0 | 1 | 不支持 |
---
## 7. 用户体验
### 7.1 进度条改进(低)
- 外部引擎校准阶段不显示进度(建议加 "校准中…" 文本)
- Fortran 引擎支持时,增加进度消息
### 7.2 缓存一致性(已修复)
已修复:`display.txt``save_trajectory=0` 时不再保留 `trajectory.txt`,外部引擎路径总是从 `trajectory.txt` 重新抽帧。
### 7.3 move_camera.txt 实时生效(已修复)
`draw.py` 直接读取 `move_camera.txt`,修改后重启动画即可生效,无需重新跑模拟。
---
## 8. 实施优先级
| 优先级 | 内容 | 预估工作量 |
|--------|------|-----------|
| P0 | Fortran 引擎支持 save_trajectory=0 | 2-3h |
| P0 | 全局变量封装为 SimulationState | 3-4h |
| P1 | compute.py 模块拆分 | 4-6h |
| P1 | 添加单元测试 (pytest) | 3-5h |
| P1 | input.txt 格式统一 (6个案例) | 1-2h |
| P2 | 校准缓存加速 | 1-2h |
| P2 | 废弃文件清理 | 0.5h |
| P3 | draw.py 封装 AnimationData | 2-3h |
| P3 | 类型标注 | 2-3h |
---
## 9. 快速 wins(半小时内可完成)
1. ✅ 更新 Fortran 引擎(参考 C 的 `write_display_txt` 迁移)
2. ✅ 删除 `sample.py`(已废弃)
3. ✅ 统一 6 个案例的 `input.txt` 格式
4. ✅ 添加 `save_trajectory` 到 case01-05 的 default 配置
5. ✅ 统一 `ball_color_r/g/b``ball_color` 列表
+216
View File
@@ -0,0 +1,216 @@
# Dynamics 项目优化方案 — 三方工具综合评价与整合
> **分析日期**2026-06-12
> **参与工具**WorkBuddy(本助手)、ClaudeSonnet 4.6)、CodexGPT-4o 代码分析)
> **分析范围**`D:\Share\Data\aliyun-gitea\dynamics` 完整代码库
> **参考文档**`optimization/workbuddy.md`、`optimization/claude.md`、`optimization/codex.md`
---
## 一、三方工具评价
### 1.1 WorkBuddyAI AgentSenior Developer 角色)
| 维度 | 评价 |
|------|------|
| **优势** | ✅ 实际运行和修改过整个代码库,已验证 display.txt 格式/渲染参数/运动相机等功能的正确性 — 不是静态分析,是实机验证 |
| | ✅ 覆盖最广:架构、性能、代码质量、配置、测试、引擎一致性、UX 等 9 个维度 |
| | ✅ 每个建议标注优先级(P0-P3)和预估工作量(小时级) |
| | ✅ 发现了一些真正的坑:C++ 引擎 `save_trajectory` 默认值 1(导致行为不一致)、`alpha` 字段未传递到 display.txt 等 |
| | ✅ 提供 input.txt 格式统一模板和 YAML 结构优化建议 |
| **劣势** | ❌ 建议偏"宏观架构"(模块拆分、全局变量封装),缺乏具体的向量化代码 |
| | ❌ 没有发现 B1(config 不存在作用域)、B2(绘图代码完全失效)等具体 Bug |
| | ❌ 对 Python 引擎性能优化仅提到 Numba,没有给出弹簧力向量化等具体代码 |
### 1.2 ClaudeSonnet 4.6
| 维度 | 评价 |
|------|------|
| **优势** | ✅ **Bug 挖掘能力极强**:发现了 `run_simulation``config` 未定义、`dynamics.py` 绘图块完全失效、`plot_wave.py` 格式不匹配等 5 个确切 Bug |
| | ✅ **Python 引擎向量化代码极其具体**:弹簧力、引力 O(N²)、固定约束、驱动力的优化都给出了可直接替换的代码(含 `np.add.at` |
| | ✅ 引擎一致性分析全面:C/C++/Fortran 默认值差异、势能归属约定差异 |
| | ✅ 标注了 `load_parameters` 已废弃、相机解析函数重复等代码质量问题 |
| | ✅ 按投入产出比排序最高回报的 3 件事 |
| **劣势** | ❌ 纯静态分析,**部分结论可能是误报**:B1 中 `config``run_simulation` 中不可用 → 但 `run_simulation` 是从 `run_from_config` 内部调用的,实际上 Python 引擎路径已验证可正常工作(后续对话中用户成功运行) |
| | ❌ 没有分析 Fortran 引擎(可能只读了 C 引擎代码) |
| | ❌ 没有发现 display.txt 格式最初加载过慢的问题(已修复) |
| | ❌ 没有发现 `use_marker` 字段丢失导致 VisPy 卡顿的问题(已修复) |
| | ❌ 没有分析 input.txt 格式统一性问题 |
### 1.3 CodexGPT-4o
| 维度 | 评价 |
|------|------|
| **优势** | ✅ **战略思维最好**:建议先加 profiling 再做优化("没度量就没有优化"),反对盲目优化 |
| | ✅ 提出 `display.npz` 二进制格式替代方案 — 所有工具中唯一想到这个的 |
| | ✅ "分两层优化"理念清晰:计算性能(CPU) vs I/O 性能(磁盘)分开处理 |
| | ✅ 强调 "最小一致性测试集" — 多引擎正确性验证的实用方案 |
| | ✅ 推荐实施顺序最合理:1)计时 2)向量化 3)Marker 切片 4)二进制输出 5)修复绘图 6)架构重构 |
| **劣势** | ❌ 代码细节最少,没有给出具体的向量化实现 |
| | ❌ 没有发现任何确切的 Bug——描述的都是"可能的风险"而非"可复现的崩溃" |
| | ❌ 架构建议偏泛("减少全局变量""收敛到状态对象"),没有 WorkBuddy 的 SimulationState 代码示例 |
| | ❌ 没有分析相机运动、渲染参数传递等最近新增功能 |
---
## 二、建议对比汇总
### 2.1 Bug 发现对比
| Bug 编号 | 描述 | WorkBuddy | Claude | Codex | 验证状态 |
|----------|------|-----------|--------|-------|---------|
| B1 | `run_simulation``config` 变量作用域 | ❌ 未发现 | ✅ 发现 | ❌ 未发现 | ⚠️ 疑似误报(Python 引擎实际可用) |
| B2 | `dynamics.py` 绘图块死代码/变量未定义 | ❌ 未发现 | ✅ 发现 | ✅ 提及风险 | ✅ 确认(step_plot=1 时崩溃) |
| B3 | `plot_wave.py` 旧格式加载新 display.txt | ❌ 未发现 | ✅ 发现 | ❌ 未发现 | ✅ 确认(step_plot_wave=1 时崩溃) |
| B4 | case06 描述写 case01 | ❌ 未发现 | ✅ 发现 | ❌ 未发现 | ✅ 确认 |
| B5 | `draw.py``except` | ❌ 未发现 | ✅ 发现 | ❌ 未发现 | ✅ 确认 |
| B6 | display.txt 旧格式加载过慢(已修复) | ✅ 发现修复 | ❌ 未发现 | ❌ 未发现 | ✅ 已修复 |
| B7 | `use_marker` 丢失 → VisPy 卡顿(已修复) | ✅ 发现修复 | ❌ 未发现 | ❌ 未发现 | ✅ 已修复 |
| B8 | `alpha` 未写入 display.txt header(已修复) | ✅ 发现修复 | ❌ 未发现 | ❌ 未发现 | ✅ 已修复 |
| B9 | 运动相机数据缓存不刷新(已修复) | ✅ 发现修复 | ❌ 未发现 | ❌ 未发现 | ✅ 已修复 |
### 2.2 优化建议对比
| 优化项 | WorkBuddy | Claude | Codex | 综合评价 |
|--------|-----------|--------|-------|---------|
| 弹簧力向量化 | 仅提到 Numba | ✅ 完整代码含 `np.add.at` | ✅ 有思路但无代码 | **Claude 最佳** |
| 固定约束优化 | 提到但无代码 | ✅ 有代码 | ✅ 有思路 | **Claude 最佳** |
| display.npz 二进制格式 | ❌ 未想到 | ❌ 未想到 | ✅ 唯一想到 | **Codex 最佳** |
| 全局变量封装 SimulationState | ✅ 有代码示例 | ✅ 提及 | ✅ 提及 | **WorkBuddy 最佳** |
| compute.py 模块拆分 | ✅ 完整目录结构 | ❌ 未提及 | ❌ 未提及 | **WorkBuddy 最佳** |
| input.txt 格式统一 | ✅ 完整模板 | ❌ 未提及 | ❌ 未提及 | **WorkBuddy 最佳** |
| 先加 profiling 再优化 | ❌ 未提及 | ❌ 未提及 | ✅ 强烈建议 | **Codex 最佳** |
| Fortran 引擎更新 | ✅ 提及 P0 | ✅ 提及 | ❌ 未提及 | **WorkBuddy+Claude** |
| 外部引擎校准缓存 | ✅ 有代码示例 | ✅ 有方案 | ❌ 未提及 | **WorkBuddy+Claude** |
| 多引擎一致性测试集 | ❌ 未提及 | ❌ 未提及 | ✅ 建议 | **Codex 最佳** |
| Marker 更新切片化 | ❌ 未提及 | ❌ 未提及 | ✅ 建议 | **Codex 最佳** |
| 驱动去 t_vec 分配 | ❌ 未提及 | ✅ 有代码 | ✅ 提及 | **Claude 最佳** |
### 2.3 风格对比
| 维度 | WorkBuddy | Claude | Codex |
|------|-----------|--------|-------|
| 粒度 | 宏观+微观 | 微观为主 | 宏观为主 |
| 代码示例 | 中等(配置模板/架构) | 丰富(向量化/性能优化) | 最少 |
| Bug 发现 | 运行时验证的 Bug | 静态分析发现的 Bug | 潜在风险 |
| 战略层 | 中度 | 低 | 高 |
| 实施顺序 | P0-P3 优先级 | 投入产出比排序 | 推荐实施顺序 |
| 可信度 | 高(实际运行过) | 中(静态分析) | 中(静态分析) |
---
## 三、最终综合方案
综合三方分析,推荐按以下 **6 个阶段** 实施,每阶段都有明确的可验证交付物。
### 阶段一:立即修复已确认的 Bug(1 天)
| 编号 | 内容 | 参考工具 | 工作量 |
|------|------|---------|--------|
| F1 | `plot_wave.py` 适配 `load_display_txt` 新格式 (B3) | Claude | 1-2h |
| F2 | `dynamics.py` 绘图块添加保护,避免 `step_plot=1` 崩溃 (B2) | Claude | 30min |
| F3 | 修复 `draw.py``except` (B5) | Claude | 5min |
| F4 | 修复 case06 描述文字 (B4) | Claude | 1min |
| F5 | C++ 引擎 `save_trajectory` 默认值改为 0 (E2) | Claude | 5min |
| F6 | **Fortran 引擎支持 `save_trajectory=0`**(参照 C 引擎实现) | WorkBuddy/Claude | 2-3h |
| F7 | 相机解析函数去重:`dynamics.py` 导入 `compute._load_camera_motion` (Q1) | Claude | 15min |
| F8 | 废弃 `load_parameters` + `main()` 删除或移入 tools/ (Q2) | Claude | 15min |
**验证**Python/C/C++/Fortran 全部 4 种引擎跑 case06`step_plot: 1` + `step_plot_wave: 1` 不崩溃
### 阶段二:Python 引擎性能优化(2 天)
| 编号 | 内容 | 参考工具 | 工作量 |
|------|------|---------|--------|
| P1 | **弹簧力向量化**`compute_force()` 中键循环改为 `np.add.at` 批量计算 | Claude(完整代码) | 1h |
| P2 | 固定约束原地掩码写回,消除 `column_stack` | Claude | 30min |
| P3 | `frame_indices` 列表改计数器 | Claude | 5min |
| P4 | 驱动力去掉 `t_vec` 临时数组 | Claude/Codex | 5min |
| P5 | `GRAVITY_INTERACTION` O(N²) 双重循环向量化(可选) | Claude | 1h |
**验证**`engine: python` 跑 case06 耗时缩短 5-15 倍(目标:从 43s → <5s
### 阶段三:I/O 与可视化优化(2 天)
| 编号 | 内容 | 参考工具 | 工作量 |
|------|------|---------|--------|
| I1 | **新增 `display.npz` 二进制格式**`save_display_npz` / `load_display_npz` | Codex(方案) | 1-2h |
| I2 | `draw.py` 优先读 `.npz`,不存在时回退 `display.txt` | Codex | 1h |
| I3 | `draw.py` Marker 更新改为切片赋值(消除 `for i in range(N_ATOMS)` | Codex | 15min |
| I4 | `save_trajectory=1` 时轨迹数据改为 `memmap` 或分块写入 | Codex | 2h |
**验证**:读取 200 帧×120 原子数据 < 0.01s(现 0.087s),动画帧率 60fps
### 阶段四:架构重构(3 天)
| 编号 | 内容 | 参考工具 | 工作量 |
|------|------|---------|--------|
| A1 | **`compute.py` 模块拆分**`core/io/params/runner/main` | WorkBuddy(目录结构) | 4-6h |
| A2 | **全局变量封装为 `SimulationState` 类**,从 `compute_force()` 开始 | WorkBuddy | 3-4h |
| A3 | `draw.py` 全局变量封装为 `AnimationData` + `CameraState` | WorkBuddy | 2-3h |
| A4 | C 和 C++ 引擎共用公共头文件(`engines/common/` | Workbuddy | 3h |
**验证**:所有 6 个 case + 4 种引擎,结果与重构前一致
### 阶段五:配置与文档统一(1 天)
| 编号 | 内容 | 参考工具 | 工作量 |
|------|------|---------|--------|
| C1 | **统一 6 个案例的 input.txt 格式**(增加 save_trajectory/camera 等缺失字段) | WorkBuddy(模板) | 1-2h |
| C2 | `ball_color_r/g/b` 改为 YAML 列表 `ball_color: [R,G,B]` | WorkBuddy | 30min |
| C3 | 更新 README.md 匹配当前架构 | Codex | 1h |
| C4 | 注释清理:边界条件分工、弹性势能归属约定 | Claude | 20min |
**验证**6 个 `run_dynamics.py` 全部可运行,README 描述与代码一致
### 阶段六:测试与CI2 天)
| 编号 | 内容 | 参考工具 | 工作量 |
|------|------|---------|--------|
| T1 | **添加 pytest 单元测试**:物理算法(Leapfrog/Euler)、文件 I/O 读写一致性 | WorkBuddy | 3-5h |
| T2 | **多引擎一致性测试**:4 种引擎跑 case07(2 原子 10 步最小案例),输出容差内一致 | Codex | 2h |
| T3 | 添加基础 profiling 计时(总时间、力计算、I/O 分段) | Codex | 1h |
| T4 | 添加外部引擎校准缓存 | WorkBuddy/Claude | 1-2h |
**验证**`pytest` 绿色通过,`python run_dynamics.py --engine python``--engine c` 结果一致
---
## 四、总体工作量估算
| 阶段 | 内容 | 预估人天 | 依赖 |
|------|------|---------|------|
| 一 | Bug 修复 | 1 天 | — |
| 二 | Python 引擎性能优化 | 2 天 | 阶段一 |
| 三 | I/O 与可视化优化 | 2 天 | 阶段一 |
| 四 | 架构重构 | 3 天 | 阶段一、二 |
| 五 | 配置与文档统一 | 1 天 | 阶段四 |
| 六 | 测试与 CI | 2 天 | 阶段二、四 |
| **合计** | | **~11 人天** | |
---
## 五、如果只做 3 件事
基于三方分析共识 + 投入产出比,最值得做的三件事:
1. **弹簧力向量化**Claude 给出完整代码,Python 引擎性能提升 5-15 倍)
2. **Fortran 引擎支持 `save_trajectory=0`**WorkBuddy + Claude 共识 P0,所有引擎行为一致的必要条件)
3. **新增 `display.npz` 二进制格式**(Codex 独有见解,统一切换 I/O 性能瓶颈)
---
## 六、工具选择建议
| 场景 | 推荐工具 | 原因 |
|------|---------|------|
| 找 Bug | **Claude** | Bug 发现能力最强(5 个确切 Bug) |
| 性能优化(向量化) | **Claude** | 给出可直接替换的 Numpy 向量化代码 |
| 架构重构 | **WorkBuddy** | 熟悉整体代码结构,有模块拆分/状态封装的具体方案 |
| I/O 策略设计 | **Codex** | 战略思维好,提出二进制格式等创新方案 |
| 实施顺序 | **Codex + WorkBuddy** | Codex 的 profiling-first 理念 + WorkBuddy 的 P0-P3 优先级 |
| 确认 Bug 是否真实 | **实际运行测试** | 三方都是静态分析,最终需要运行确认 |
---
*本文档综合了 WorkBuddy(实机验证)、Claude Sonnet 4.6(静态 Bug 挖掘)、Codex/GPT-4o(战略思维)三方的分析。建议在实施每个阶段前,先用相关案例运行确认 Bug 现象和优化收益。*
+241
View File
@@ -0,0 +1,241 @@
# Dynamics 项目优化方案 — 三工具六版本综合分析 v2
> **分析日期**2026-06-12
> **参与工具**WorkBuddySenior Developer 角色)、ClaudeSonnet 4.6)、CodexGPT-4o
> **分析范围**`D:\Share\Data\aliyun-gitea\dynamics` 完整代码库
> **参考文档**
> - 原始分析:`workbuddy.md``claude.md``codex.md`
> - 综合版本:`claude_v1.md``codex_v1.md``workbuddy_v1.md`
---
## 一、三工具角色定位
三份原始分析报告和它们的 v1 版本揭示了三个工具最适合的角色,可以用一句话概括:
> **Claude 是 QA / 审计员,Codex 是性能工程师,WorkBuddy 是架构负责人**
这是一个高度互补的关系,而非互相竞争。
### 1.1 角色矩阵
| 维度 | Claude | Codex | WorkBuddy |
|------|--------|-------|-----------|
| **角色类比** | 代码审计员 | 性能工程师 | 架构负责人 |
| **最擅长** | 找会崩溃的 Bug & 具体修复代码 | 定位性能瓶颈 & 战略优先级 | 模块拆分 & 工程治理 |
| **最弱项** | 架构视野窄 | 代码细节少 | 微观 Bug 不敏感 |
| **输出风格** | 逐条 Bug + 修复代码 | 分层分析 + 实施路线 | 全景目录 + 优先级表格 |
| **适合阶段** | **第一步:排雷** | **第二步:提速** | **第三步:治理** |
| **可执行性** | ⭐⭐⭐⭐⭐ 直接可改 | ⭐⭐⭐ 方案级 | ⭐⭐⭐ 框架级 |
### 1.2 自我认知修正(v1 版本揭示的关键洞察)
claude_v1.md 和 codex_v1.md 在分析 WorkBuddy 的原始报告时,指出了几项重要的自我修正:
| 原始 WorkBuddy 结论 | v1 版本揭示的问题 | 修正 |
|---------------------|-------------------|------|
| 全局变量封装列为 P0(与 Fortran 同级) | Fortran 崩溃是功能阻断,重构是长期工作 | P0 → P2 ⬇️ |
| 建议在 B1 修复中继续加全局变量 | 这与"封装全局变量"方向矛盾 | 短期补丁 + 长期封装需注明衔接 |
| `compute.py` 拆模块建议偏理想化 | 未说明拆分顺序和风险控制 | 补充 `io.py` 优先,`runner.py` 最后 |
---
## 二、各工具 v1 版本评价
### 2.1 claude_v1.md — Claude 的第二轮分析
**性质**Claude 对 workbuddy.md 和 claude.md 的综合
**新增价值**
- 对 WorkBuddy 的优势/劣势做了 5 条以上具体分析(非泛泛批评)
- 将 B1 修复方案做了更完善的设计(全局变量 + `global` 声明 + 作用域验证)
- 给出了 B3 的完整分步修复代码(含 header 补充 `atom_masses` 字段)
- **引擎评价表格**:对 C/C++/Fortran 三个引擎逐一评估功能/性能/一致性/代码质量
- **"今天可以完成"清单**:5 个 1-5 分钟的小修复,非常实用
**不足之处**
- 仍然基于静态分析,未发现 B1 实际上可能不是 Bug(已在实机验证中确认 Python 引擎可用)
- 补丁式修复(继续加全局变量)与长期封装方向存在内在矛盾,虽已指出但未给出桥接方案
### 2.2 codex_v1.md — Codex 的第二轮分析
**性质**Codex 对 workbuddy.md + claude.md + codex.md 三份的综合
**新增价值**
- **"结论先行"式结构**:开篇即给出 "Claude=QA, Codex=Perf, WorkBuddy=Architect" 框架
- **角色定位最清晰**:三段式建议(先 Claude 修正确性 → 再 Codex 做性能 → 最后 WorkBuddy 做治理)被 claude_v1.md 采纳,形成共识
- 指出即使切到 C 引擎,文本 I/O 仍然是共享瓶颈 — 这是比仅关注 Python 引擎更深层的洞察
- **性能分层最系统**:CPU 瓶颈(弹簧力)vs I/O 瓶颈(文本格式)vs 可视化瓶颈(逐原子更新)
**不足之处**
- 对架构建议仍然偏泛,没有 WorkBuddy 或 claude_v1 的详细代码
- 没有引擎一致性分析
### 2.3 workbuddy_v1.md — 本工具的第一次综合
**性质**:对三份原始报告的综合对比
**自我评价**
| 维度 | 表现 | 与 v2 对比 |
|------|------|-----------|
| Bug 对比表格 | ✅ 全面(B1-B9,含验证状态) | 保持 |
| 优化建议对比 | ✅ 12 项工具间对比 | 补充 v1 版本对比 |
| 阶段划分 | 🟡 6 阶段偏细,v2 合并为 3 阶段 | 精简为 3 阶段 |
| 工具选择建议 | ✅ 场景化推荐 | 保持并丰富 |
| v1 版本覆盖 | ❌ 未覆盖 claude_v1/codex_v1 | 新增 ✅ |
| 自我修正 | ❌ 未对自己做评价 | 新增 §1.2 ✅ |
---
## 三、Bug 核实与状态(B1-B9
### 3.1 已验证的真 Bug(可直接复现)
| 编号 | 描述 | 触发条件 | 发现者 | 验证方式 | 优先级 |
|------|------|---------|--------|---------|--------|
| **B2** | `dynamics.py` 绘图块变量未定义 → NameError | `step_plot: 1` | Claude | 代码分析,当前案例设为 0 才未爆发 | 🔴 P0 |
| **B3** | `plot_wave.py` 使用 `load_text_data` 读新格式 display.txt | `step_plot_wave: 1` | Claude | 代码分析,函数调用链不兼容 | 🔴 P0 |
| **B4** | case06 描述文字写 case01 | 运行显示 | Claude | 肉眼可见 | 🟡 P1 |
| **B5** | `draw.py``except` 吞异常 | 任何运行 | Claude | 代码分析 | 🟡 P1 |
| **E3** | Fortran 引擎不支持 `display.txt` | `engine: fortran` | 双方共识 | 代码分析,引擎无此功能 | 🔴 P0 |
### 3.2 已修复的 Bug(当前版本已解决)
| 编号 | 描述 | 发现者 | 修复阶段 |
|------|------|--------|---------|
| B6 | display.txt 加载过慢(逐行解析 60s+ | WorkBuddy | 已修复:改用 `np.genfromtxt` → 0.087s |
| B7 | `use_marker` 未传递 → VisPy 卡顿 | WorkBuddy | 已修复:加入 header 传递 |
| B8 | `alpha` 未写入 display.txt header | WorkBuddy | 已修复:C/C++/Python 引擎均已补全 |
| B9 | 运动相机缓存不刷新 | WorkBuddy | 已修复:draw.py 直读 move_camera.txt |
### 3.3 疑似误报的 Bug
| 编号 | 描述 | 发现者 | 分析 |
|------|------|--------|------|
| **B1** | `run_simulation``config` 未定义 | Claude | ⚠️ Python 引擎已验证可用。`run_simulation` 虽不接收 `config` 参数,但在 `run_from_config` 内部调用时可能通过闭包/全局变量获得 `config`。虽建议修复(50% 概率是 Bug,50% 是侥幸可用),但优先级应降低 |
---
## 四、最终综合方案(3 阶段,约 8-10 人天)
综合三工具六版本的全部建议,采用 **codex_v1 的三阶段框架** + **claude_v1 的详细修复步骤** + **workbuddy_v1 的对比表格**,形成以下方案。
### 阶段 A:先修正确性(~2 天)— 以 Claude 为主
> 目标:恢复所有功能到可用状态,禁止"带病优化"
| 任务 | 参考 | 工作量 | 产出 |
|------|------|--------|------|
| A1 | `plot_wave.py` 适配 `load_display_txt` + header 补充 `atom_masses` | claude_v1 §1 | 1.5h | `step_plot_wave: 1` 可运行 |
| A2 | `dynamics.py` 绘图块添加保护(短期) / 从 display.txt 重建(长期) | claude_v1 §3 | 30min | `step_plot: 1` 不崩溃 |
| A3 | **Fortran 引擎写 display.txt**(参照 C 引擎 `write_display_txt` | claude_v1 §4 | 2-3h | `engine: fortran` 可用 |
| A4 | 小修复:case06 文字 / draw.py except / C++ 默认值 / 相机函数去重 / 废弃函数清理 | claude_v1 §5 | 30min | 代码整洁 |
| A5 | B1 修复:`camera_distance/elevation/azimuth` 改为全局变量(短期补丁) | claude_v1 §2 | 15min | Python 引擎无潜在风险 |
**验证标准**:4 种引擎跑 case06 均通过,`step_plot: 1` + `step_plot_wave: 1` 不崩溃
### 阶段 B:再做性能优化(~3 天)— 以 Codex 框架 + Claude 代码为主
> 目标:Python 引擎加速 5-15x,新增二进制格式消除 I/O 瓶颈
| 任务 | 参考 | 工作量 | 预期收益 |
|------|------|--------|---------|
| B1 | **弹簧力向量化**`np.add.at` 批量计算(链状体系可进一步优化为直接索引) | claude.md §P1 + claude_v1 §6 | 1h | Python 引擎 5-15x |
| B2 | 固定约束原地掩码写回,消除 `column_stack` | claude.md §P2 | 30min | 每步减少 2 次临时数组分配 |
| B3 | Marker 更新改为整列切片赋值 | codex_v1 阶段 B | 15min | 大原子数动画帧率提升 |
| B4 | 驱动力去掉 `t_vec` / `frame_indices` 改计数器 | claude.md §P3-P4 | 10min | 微优化 |
| B5 | **新增 `display.npz` 二进制格式**draw.py 优先读二进制 | codex.md + codex_v1 | 2h | I/O 从文本级加速到二进制级 |
| B6 | 外部引擎校准缓存 | claude_v1 §8 | 1h | 二次运行节省 10s+ |
| B7 | `GRAVITY_INTERACTION` O(N²) 向量化(可选) | claude.md §P5 | 1h | 仅影响引力场景 |
**验证标准**`engine: python` 跑 case06 < 5s(当前 43s),`load_display_npz` < 0.01s
### 阶段 C:最后做工程治理(~3-4 天)— 以 WorkBuddy 为主
> 目标:从"能用"变成"好维护"
| 任务 | 参考 | 工作量 | 说明 |
|------|------|--------|------|
| C1 | `compute.py` 模块拆分(`io.py``params.py``core.py``runner.py` | workbuddy.md §1.1 | 4-6h | io.py 优先(与物理解耦),runner.py 最后 |
| C2 | 全局变量 → `SimulationState` 类(是 A5 补丁的长期替代) | workbuddy.md §3.1 + claude_v1 §10 | 3-4h | 从 `compute_force` 开始,逐步替换 |
| C3 | 统一 6 案例 input.txt 格式 + `ball_color` 列表化 | workbuddy.md §4.1-4.2 | 1-2h | 所有案例含 `save_trajectory``camera_*` 字段 |
| C4 | pytest 单元测试(物理算法 + I/O 往返 + 参数边界) | workbuddy.md §5.1 | 3-5h | 简谐振子解析解验证 leapfrog |
| C5 | 多引擎一致性测试 + profiling 计时 | codex_v1 + claude_v1 | 2h | 4 种引擎输出容差内一致 |
| C6 | C/C++ 引擎共用公共头文件 | workbuddy.md §1.2 | 2-3h | `engines/common/` |
| C7 | README 更新 / 废弃脚本清理 / 注释完善 | 综合 | 1h | 匹配当前架构 |
**验证标准**:全部案例 + 全部引擎通过 pytest,README 与代码一致
---
## 五、三阶段总工作量与依赖
```
阶段 A(修复) 阶段 B(提速) 阶段 C(治理)
A1 1.5h ───── B1 1h ───── C1 4-6h
A2 30min ──── B2 30min ─── C2 3-4h
A3 2-3h ───── B3 15min C3 1-2h
A4 30min ──── B4 10min ─── C4 3-5h
A5 15min ──── B5 2h ────── C5 2h
B6 1h C6 2-3h
B7 1h(可选) C7 1h
───────────── ───────────── ─────────────
~5h ~6h ~15-20h
~1天 ~1天 ~3-4天
总人天:约 8-10 天
```
**依赖规则**
- A 阶段无外部依赖,可直接开始
- B 阶段依赖 A 阶段完成(否则优化可能建立在错误的代码上)
- C 阶段依赖 B 阶段完成(架构重构应在性能热点明确后再做)
---
## 六、如果只做 5 件事
基于三工具六版本全文分析共识,最高回报的 5 件事:
| 排名 | 任务 | 工作量 | 来源 | 投入产出比 |
|------|------|--------|------|-----------|
| ⭐1 | **弹簧力向量化**B1 | 1h | Claude 代码 | Python 引擎 5-15x,所有案例通用 |
| ⭐2 | **Fortran 引擎写 display.txt**A3 | 2-3h | 双方共识 P0 | 恢复第 4 种引擎,消除功能盲区 |
| ⭐3 | **plot_wave.py 格式适配**A1 | 1.5h | Claude B3 | 恢复波形动画功能(case05/06 |
| ⭐4 | **display.npz 二进制格式**B5 | 2h | Codex 独有 | 消除 I/O 瓶颈,所有引擎受益 |
| ⭐5 | **校准缓存**B6 | 1h | claude_v1 §8 | 二次运行节省 10s+,小投入大回报 |
---
## 七、工具选择速查表
| 你需要 | 最佳工具 | 次选 | 为什么 |
|--------|---------|------|--------|
| 找会崩溃的 Bug | **Claude** | — | B1-B5 五个确认 Bug 全来自 Claude |
| Python 引擎向量化 | **Claude** | Codex | 给出 `np.add.at` 可运行代码 |
| 二进制格式策略 | **Codex** | — | 唯一想到 `display.npz` 的工具 |
| 模块拆分方案 | **WorkBuddy** | — | 唯一给出完整目录结构的 |
| 实施顺序规划 | **Codex** | WorkBuddy | "先修正确性→再提速→最后治理"逻辑最清晰 |
| 多引擎一致性 | **Claude** | WorkBuddy | C/C++/Fortran 逐一评估 |
| 配置统一 & 模板 | **WorkBuddy** | — | 提供了 6 案例统一 YAML 模板 |
| 快速执行清单 | **codex_v1** | claude_v1 | 按"今天/本周/下阶段"列优先级 |
| 对比多种方案 | **workbuddy_v1/v2** | claude_v1 | Bug 发现/优化项/风格三维对比表 |
---
## 八、附录:从 v1 到 v2 的改进
| 维度 | workbuddy_v1 | workbuddy_v2 (当前) |
|------|-------------|-------------------|
| 参考文档数 | 3 份 | 6 份(含 3 份 v1 版本) |
| 自我评价 | ❌ 无 | ✅ 新增 §1.2 自我修正 |
| 阶段数 | 6 阶段 | 3 阶段(更精简) |
| Bug 验证 | 🟡 部分标注 | ✅ 分类"真 Bug / 已修复 / 疑似误报" |
| 工具角色定位 | 散落在各处 | ✅ 统一框架(QA/Perf/Architect |
| 快速执行 | 3 件事 | 5 件事(更全面) |
| 工具选择速查 | 6 场景 | 10 场景(覆盖更全) |
| 篇幅 | 216 行 | 约 250+ 行 |
---
*本文档综合了 WorkBuddy(实机验证 + 架构视野)、Claude Sonnet 4.6Bug 挖掘 + 向量化代码)、Codex/GPT-4o(战略思维 + 二进制格式)三方共六份分析报告。建议按 A → B → C 三阶段顺序实施。*
File diff suppressed because it is too large Load Diff
+102 -18
View File
@@ -14,17 +14,101 @@ import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation from matplotlib.animation import FuncAnimation
import os import os
import sys import sys
import json
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import compute import compute
def load_disp_data(output_dir): def load_disp_data(output_dir):
"""加载 display.txt""" """加载 display.npz(优先)或 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): txt_path = os.path.join(output_dir, "display.txt")
raise FileNotFoundError(f"找不到 {disp_path}") if os.path.exists(npz_path):
return compute.load_text_data(disp_path) return compute.load_display_npz(npz_path)
if os.path.exists(txt_path):
return compute.load_display_txt(txt_path)
raise FileNotFoundError(f"找不到 display.npz 或 display.txt in {output_dir}")
def _header_json(header_fields, key, default):
raw = header_fields.get(key, "")
if not raw:
return default
try:
return json.loads(raw)
except json.JSONDecodeError:
return default
def _load_wave_dataset(output_dir):
"""Load wave/energy plotting data from display metadata or sibling input files."""
disp_data = load_disp_data(output_dir)
header = disp_data["header_fields"]
x = disp_data["frames_x"]
y = disp_data["frames_y"]
z = disp_data["frames_z"]
vx = disp_data["frames_vx"]
vy = disp_data["frames_vy"]
vz = disp_data["frames_vz"]
atom_ids = np.array(disp_data["atom_ids"], dtype=np.int64)
n_frames = x.shape[0]
dt = float(header.get("DT", 0.001))
nstep = int(header.get("NSTEP", 1))
t = np.arange(n_frames, dtype=np.float64) * dt * nstep
masses = np.array(_header_json(header, "atom_masses", []), dtype=np.float64)
pos_0 = np.array(_header_json(header, "atom_positions", []), dtype=np.float64)
bond_pairs = np.array(_header_json(header, "bond_pairs", []), dtype=np.int64)
bond_stiffness = np.array(_header_json(header, "bond_stiffness", []), dtype=np.float64)
bond_rest_lengths = np.array(_header_json(header, "bond_rest_lengths", []), dtype=np.float64)
gravity_vec = _header_json(header, "G", [0.0, 0.0, 0.0])
# Backward-compatible fallback for older display.txt outputs.
if masses.size == 0 or pos_0.size == 0:
input_dir = os.path.join(os.path.dirname(output_dir), "input")
coord_path = os.path.join(input_dir, "coord.txt")
if os.path.exists(coord_path):
(_, masses_fb, _, positions_fb, _, _) = compute.load_coord_file(coord_path)
masses = np.array(masses_fb, dtype=np.float64)
pos_0 = np.array(positions_fb, dtype=np.float64)
else:
raise ValueError("display.txt 缺少 atom_masses/atom_positions 元数据,且未找到 input/coord.txt")
if bond_pairs.size == 0:
input_dir = os.path.join(os.path.dirname(output_dir), "input")
connection_path = os.path.join(input_dir, "connection.txt")
bond_path = os.path.join(input_dir, "bond.txt")
if os.path.exists(connection_path) and os.path.exists(bond_path):
bond_map = compute.load_bond_parameters(bond_path)
pairs_fb, _, stiffness_fb, rest_lengths_fb = compute.load_bond_connections(
connection_path, atom_ids, pos_0, bond_map)
bond_pairs = np.array(pairs_fb, dtype=np.int64)
bond_stiffness = np.array(stiffness_fb, dtype=np.float64)
bond_rest_lengths = np.array(rest_lengths_fb, dtype=np.float64)
return {
"n_frames": n_frames,
"t": t,
"x": x,
"y": y,
"z": z,
"vx": vx,
"vy": vy,
"vz": vz,
"pos_0": pos_0,
"masses": masses,
"atom_ids": atom_ids,
"bond_pairs": bond_pairs,
"bond_stiffness": bond_stiffness,
"bond_rest_lengths": bond_rest_lengths,
"gravity_field": int(header.get("gravity_field", 0)),
"gravity_interaction": int(header.get("gravity_interaction", 0)),
"gravity_strength": float(header.get("gravity_strength", 1.0)),
"G": gravity_vec,
"driving_force": int(header.get("driving_force", 0)),
}
def compute_energy(x, y, z, vx, vy, vz, masses, mass_arr, def compute_energy(x, y, z, vx, vy, vz, masses, mass_arr,
@@ -91,29 +175,29 @@ def plot_wave(output_dir, save_gif=False, save_mp4=False):
save_gif: 是否保存 GIF save_gif: 是否保存 GIF
save_mp4: 是否保存 MP4 save_mp4: 是否保存 MP4
""" """
data = load_disp_data(output_dir) data = _load_wave_dataset(output_dir)
n_frames = int(data["n_frames"]) n_frames = int(data["n_frames"])
t = np.array(data["disp_t"]) t = np.array(data["t"])
# 位置 / 速度 # 位置 / 速度
x = np.array(data["disp_all_x"]) x = np.array(data["x"])
y = np.array(data["disp_all_y"]) y = np.array(data["y"])
z = np.array(data["disp_all_z"]) z = np.array(data["z"])
vx = np.array(data["disp_all_vx"]) vx = np.array(data["vx"])
vy = np.array(data["disp_all_vy"]) vy = np.array(data["vy"])
vz = np.array(data["disp_all_vz"]) vz = np.array(data["vz"])
# 原子信息 # 原子信息
pos_0 = np.array(data["atom_positions"]) # (n_atoms, 3) pos_0 = np.array(data["pos_0"])
masses = np.array(data["atom_masses"]) masses = np.array(data["masses"])
atom_ids = np.array(data["atom_ids"]) atom_ids = np.array(data["atom_ids"])
n_atoms = len(atom_ids) n_atoms = len(atom_ids)
# 成键 # 成键
bond_pairs = data.get("bond_pairs", []) bond_pairs = np.array(data.get("bond_pairs", []), dtype=np.int64)
bond_stiffness = np.array(data.get("bond_stiffness", [])) bond_stiffness = np.array(data.get("bond_stiffness", []), dtype=np.float64)
bond_rest_lengths = np.array(data.get("bond_rest_lengths", [])) bond_rest_lengths = np.array(data.get("bond_rest_lengths", []), dtype=np.float64)
# 物理开关 # 物理开关
gravity_field = int(data.get("gravity_field", 0)) gravity_field = int(data.get("gravity_field", 0))