# 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 热点循环尽量向量化
- 把文本轨迹格式逐步替换为二进制主格式
- 把数据流和状态管理收紧

如果按这个方向推进，这个项目会同时得到：

- 更好的性能
- 更低的内存占用
- 更稳定的多引擎一致性
- 更容易继续扩展新的物理项和可视化形式
