# 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 现象。*
