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