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

11 KiB
Raw Blame History

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_idxj_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:196save_display_txt()compute.py:239load_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.npzdraw.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 热点循环尽量向量化
  • 把文本轨迹格式逐步替换为二进制主格式
  • 把数据流和状态管理收紧

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

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