Files
dynamics/optimization/claude.md
T
admin d930fb558c docs: 综合三方工具分析,输出最终优化方案 workbuddy_v1.md
对比 WorkBuddy/Claude/Codex 三款 AI 工具对同一代码库的
优化建议,以表格形式评价各自优劣(Bug 发现/代码质量/战略
思维/代码示例),最终整合为 6 阶段实施计划(11 人天)。
2026-06-12 15:39:31 +08:00

451 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 现象。*