From 0e636e275d1f7e45f9b846d17f4a678431e55f70 Mon Sep 17 00:00:00 2001 From: Ying-Li Niu <64801511@qq.com> Date: Wed, 17 Jun 2026 15:33:49 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20examples/Readme.md?= =?UTF-8?q?=20=E5=B9=B6=E6=96=B0=E5=A2=9E=20Readme.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 覆盖全部 10 个案例(原 Readme 只到 case06) - 新增案例选择指南表格 - Readme.html 为深色主题独立 HTML 页面 (含卡片布局、标签分类、代码高亮、响应式设计) - 各案例详情对齐最新配置参数 --- compute.py | 231 ++++++++++- dynamics.py | 39 +- engines/__init__.py | 0 engines/c/Makefile | 21 +- engines/c/_calib_cache.json | 1 + engines/c/dynamics_lib.c | 554 ++++++++++++++++++++++++++ engines/cpp/Makefile | 49 +++ engines/cpp/_calib_cache.json | 1 + engines/cpp/dynamics_lib.cpp | 450 +++++++++++++++++++++ engines/engine_dll.py | 424 ++++++++++++++++++++ engines/fortran/Makefile | 47 +++ engines/fortran/_calib_cache.json | 1 + engines/fortran/dynamics_lib.f90 | 483 ++++++++++++++++++++++ engines/fortran/main.f90 | 256 ++++-------- engines/python/__init__.py | 0 engines/python/dynamics_lib.py | 404 +++++++++++++++++++ engines/python/main.py | 283 +++++++++++++ examples/Readme.html | 436 ++++++++++++++++++++ examples/Readme.md | 89 ++++- examples/case06/input/bond.txt | 4 +- examples/case06/input/coord.txt | 240 +++++------ examples/case06/input/input.txt | 23 +- examples/case07/Readme.md | 40 ++ examples/case07/doc/index.html | 477 ++++++++++++++++++++++ examples/case07/input/bond.txt | 2 + examples/case07/input/connection.txt | 120 ++++++ examples/case07/input/coord.txt | 121 ++++++ examples/case07/input/driver.txt | 3 + examples/case07/input/input.txt | 114 ++++++ examples/case07/input/move_camera.txt | 9 + examples/case07/run_dynamics.py | 54 +++ examples/case08/Readme.md | 40 ++ examples/case08/doc/index.html | 477 ++++++++++++++++++++++ examples/case08/input/bond.txt | 2 + examples/case08/input/connection.txt | 2 + examples/case08/input/coord.txt | 3 + examples/case08/input/driver.txt | 2 + examples/case08/input/input.txt | 114 ++++++ examples/case08/input/move_camera.txt | 9 + examples/case08/run_dynamics.py | 54 +++ examples/case09/Readme.md | 40 ++ examples/case09/doc/index.html | 477 ++++++++++++++++++++++ examples/case09/input/bond.txt | 2 + examples/case09/input/connection.txt | 40 ++ examples/case09/input/coord.txt | 41 ++ examples/case09/input/driver.txt | 2 + examples/case09/input/input.txt | 114 ++++++ examples/case09/input/move_camera.txt | 9 + examples/case09/run_dynamics.py | 54 +++ examples/case10/Readme.md | 40 ++ examples/case10/doc/index.html | 477 ++++++++++++++++++++++ examples/case10/input/bond.txt | 2 + examples/case10/input/connection.txt | 40 ++ examples/case10/input/coord.txt | 41 ++ examples/case10/input/driver.txt | 2 + examples/case10/input/input.txt | 114 ++++++ examples/case10/input/move_camera.txt | 9 + examples/case10/run_dynamics.py | 54 +++ plot_wave.py | 483 +++++++++++++++++++--- 59 files changed, 7302 insertions(+), 418 deletions(-) create mode 100644 engines/__init__.py create mode 100644 engines/c/_calib_cache.json create mode 100644 engines/c/dynamics_lib.c create mode 100644 engines/cpp/Makefile create mode 100644 engines/cpp/_calib_cache.json create mode 100644 engines/cpp/dynamics_lib.cpp create mode 100644 engines/engine_dll.py create mode 100644 engines/fortran/Makefile create mode 100644 engines/fortran/_calib_cache.json create mode 100644 engines/fortran/dynamics_lib.f90 create mode 100644 engines/python/__init__.py create mode 100644 engines/python/dynamics_lib.py create mode 100644 engines/python/main.py create mode 100644 examples/Readme.html create mode 100644 examples/case07/Readme.md create mode 100644 examples/case07/doc/index.html create mode 100644 examples/case07/input/bond.txt create mode 100644 examples/case07/input/connection.txt create mode 100644 examples/case07/input/coord.txt create mode 100644 examples/case07/input/driver.txt create mode 100644 examples/case07/input/input.txt create mode 100644 examples/case07/input/move_camera.txt create mode 100644 examples/case07/run_dynamics.py create mode 100644 examples/case08/Readme.md create mode 100644 examples/case08/doc/index.html create mode 100644 examples/case08/input/bond.txt create mode 100644 examples/case08/input/connection.txt create mode 100644 examples/case08/input/coord.txt create mode 100644 examples/case08/input/driver.txt create mode 100644 examples/case08/input/input.txt create mode 100644 examples/case08/input/move_camera.txt create mode 100644 examples/case08/run_dynamics.py create mode 100644 examples/case09/Readme.md create mode 100644 examples/case09/doc/index.html create mode 100644 examples/case09/input/bond.txt create mode 100644 examples/case09/input/connection.txt create mode 100644 examples/case09/input/coord.txt create mode 100644 examples/case09/input/driver.txt create mode 100644 examples/case09/input/input.txt create mode 100644 examples/case09/input/move_camera.txt create mode 100644 examples/case09/run_dynamics.py create mode 100644 examples/case10/Readme.md create mode 100644 examples/case10/doc/index.html create mode 100644 examples/case10/input/bond.txt create mode 100644 examples/case10/input/connection.txt create mode 100644 examples/case10/input/coord.txt create mode 100644 examples/case10/input/driver.txt create mode 100644 examples/case10/input/input.txt create mode 100644 examples/case10/input/move_camera.txt create mode 100644 examples/case10/run_dynamics.py diff --git a/compute.py b/compute.py index ee9e6b9..1aae868 100644 --- a/compute.py +++ b/compute.py @@ -947,6 +947,179 @@ def run_from_config(config, out_dir=None): return traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz +def run_engine_dll(engine, output_dir, config): + """通过 DLL(ctypes)调用计算引擎,不经过文件 I/O,直接返回轨迹数组。 + + Args: + engine: 引擎名称 "c", "cpp", 或 "fortran" + output_dir: 输出目录(用于保存 display.npz) + config: YAML 配置字典 + Returns: + None(结果直接写入 output_dir/display.npz) + Raises: + FileNotFoundError: DLL 尚未编译 + RuntimeError: DLL 运算出错 + """ + import sys as _sys + import datetime as _datetime + _eng_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "engines") + if _eng_dir not in _sys.path: + _sys.path.insert(0, _eng_dir) + from engine_dll import load_dll, run_dynamics_dll, is_dll_available + + if not is_dll_available(engine): + raise FileNotFoundError( + f"DLL 未找到(引擎 {engine})。" + f"请先编译:cd engines/{engine} && make dll") + + lib = load_dll(engine) + + # ── 构造原子数据 ────────────────────────────────────────── + if ATOM_POSITIONS is None: + raise RuntimeError("run_engine_dll: 请先调用 load_parameters() 加载配置") + + pos = np.asarray(ATOM_POSITIONS, dtype=np.float64) # (n, 3) + vel = np.asarray(ATOM_VELOCITIES, dtype=np.float64) + mass = np.asarray(ATOM_MASSES, dtype=np.float64) + fixed= np.asarray(ATOM_FIXED, dtype=np.int32) # (n, 3) + + # ── 键数据 ──────────────────────────────────────────────── + n_bonds = len(BOND_PAIRS) if BOND_PAIRS is not None else 0 + bp = np.asarray(BOND_PAIRS, dtype=np.int32) if n_bonds else np.zeros((0,2), dtype=np.int32) + bk = np.asarray(BOND_STIFFNESS, dtype=np.float64) if n_bonds else np.zeros(0) + br0 = np.asarray(BOND_REST_LENGTHS,dtype=np.float64) if n_bonds else np.zeros(0) + + # ── 驱动数据 ────────────────────────────────────────────── + drv_list = [] + if int(config.get("driving_force", 0)) and DRIVER_DATA: + atom_id_to_local = {int(aid): i for i, aid in enumerate(ATOM_IDS)} + for d in DRIVER_DATA: + aid = int(d.get("atom_id", -1)) + if aid not in atom_id_to_local: + continue + local_idx = atom_id_to_local[aid] + # d["amp"], d["freq"], d["phi"] are numpy arrays; d["phi"] is already in radians + amp = [float(v) for v in d["amp"]] + freq = [float(v) for v in d["freq"]] + phi = [float(v) for v in d["phi"]] # radians + # eq_pos is set by run_from_config; fall back to initial position + eq_pos = d.get("eq_pos") + eq = ([float(v) for v in eq_pos] if eq_pos is not None + else [float(pos[local_idx, 0]), float(pos[local_idx, 1]), float(pos[local_idx, 2])]) + pc = d.get("period_cycles") # None → unlimited, float → finite + nc = float(pc) if pc is not None else 0.0 + hp = 1 if nc > 0 else 0 + drv_list.append({"local_idx": local_idx, "amp": amp, "freq": freq, + "phi": phi, "eq_pos": eq, "n_cycles": nc, "has_period": hp}) + + # ── 进度回调 ────────────────────────────────────────────── + total_steps = int(config["NT"]) - int(config.get("warmup_steps", 0)) + try: + from tqdm import tqdm as _tqdm + _pbar = _tqdm(total=total_steps, desc=f"[compute] DLL {engine}", + unit="步", bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]') + def _cb(step, total): + _pbar.n = step + _pbar.refresh() + except ImportError: + _pbar = None + _cb = None + + _t0 = time.time() + try: + result = run_dynamics_dll( + lib, config, + pos, vel, mass, fixed, + bp, bk, br0, + drv_list, np.asarray(ATOM_IDS), + progress_cb=_cb, + ) + finally: + if _pbar is not None: + _pbar.n = total_steps + _pbar.close() + elapsed = time.time() - _t0 + + n_frames, n_atoms = result["x"].shape + print(f"[compute] DLL 完成: {n_frames} 帧 {n_atoms} 原子 {elapsed:.3f} s") + + # ── 构建 header 并保存 display.npz ──────────────────────── + # 与 run_simulation 写入的 header 保持字段完全一致, + # 确保 draw.py / plot_wave.py 读到所有必要参数。 + G_vec = parse_gravity_vector(config.get("G", [0, 0, 0])) + B_vec = parse_damping_vector(config.get("B", [0, 0, 0])) + record_steps_hdr = int(config["NT"]) - int(config.get("warmup_steps", 0)) + header = { + "DT": str(config["DT"]), + "NSTEP": str(config.get("NSTEP", 1)), + "method": str(config.get("method", "leapfrog")), + "NT": str(config["NT"]), + "warmup_steps": str(config.get("warmup_steps", 0)), + "dynamic_steps": str(record_steps_hdr), + "T_total": str(int(config["NT"]) * float(config["DT"])), + "box_a": str(config.get("box_a", 300.0)), + "gravity_field": str(config.get("gravity_field", 0)), + "gravity_interaction": str(config.get("gravity_interaction", 0)), + "elastic_force": str(config.get("elastic_force", 1)), + "damping_force": str(config.get("damping_force", 0)), + "driving_force": str(config.get("driving_force", 0)), + "gravity_strength": str(config.get("gravity_strength", 1.0)), + "G": json.dumps(G_vec.tolist()), + "B": json.dumps(B_vec.tolist()), + "number_of_frames": str(n_frames), + "number_of_particles": str(n_atoms), + # draw.py 需要的渲染参数 + "use_marker": str(use_marker), + "ball_radius": str(config.get("ball_radius", float(ATOM_RADII[0]) if ATOM_RADII is not None else 0.5)), + "ball_color_r": str(config.get("ball_color_r", 0.9)), + "ball_color_g": str(config.get("ball_color_g", 0.2)), + "ball_color_b": str(config.get("ball_color_b", 0.2)), + "box_color_r": str(config.get("box_color_r", 0.8)), + "box_color_g": str(config.get("box_color_g", 0.8)), + "box_color_b": str(config.get("box_color_b", 0.85)), + "alpha": ",".join(str(a) for a in (alpha if isinstance(alpha, list) else [alpha])), + # draw.py / plot_wave.py 需要的原子、键数据 + "atom_radii": ",".join(str(r) for r in ATOM_RADII), + "atom_masses": json.dumps([float(v) for v in ATOM_MASSES]), + "atom_positions": json.dumps(ATOM_POSITIONS.tolist()), + "bond_pairs": json.dumps(BOND_PAIRS.tolist() if BOND_PAIRS is not None else []), + "bond_stiffness": json.dumps(BOND_STIFFNESS.tolist() if BOND_STIFFNESS is not None else []), + "bond_rest_lengths": json.dumps(BOND_REST_LENGTHS.tolist() if BOND_REST_LENGTHS is not None else []), + # 边界(draw.py 用于场景缩放) + "X_MIN": str(-float(config.get("box_a", 300.0))), + "X_MAX": str( float(config.get("box_a", 300.0))), + "Y_MIN": str(-float(config.get("box_a", 300.0))), + "Y_MAX": str( float(config.get("box_a", 300.0))), + "Z_MIN": str(-float(config.get("box_a", 300.0))), + "Z_MAX": str( float(config.get("box_a", 300.0))), + # 相机参数 + "camera_distance": str(camera_distance), + "camera_elevation": str(camera_elevation), + "camera_azimuth": str(camera_azimuth), + "camera_center_x": str(camera_center_x), + "camera_center_y": str(camera_center_y), + "camera_center_z": str(camera_center_z), + "camera_keyframes": str(camera_keyframes_raw), + } + if display_amp_str: + header["display_amp"] = display_amp_str + if camera_pos_x is not None: + header["camera_pos_x"] = str(camera_pos_x) + header["camera_pos_y"] = str(camera_pos_y) + header["camera_pos_z"] = str(camera_pos_z) + + os.makedirs(output_dir, exist_ok=True) + npz_path = os.path.join(output_dir, "display.npz") + save_display_npz( + npz_path, + result["x"], result["y"], result["z"], + result["vx"], result["vy"], result["vz"], + np.asarray(ATOM_IDS), + header_fields=header, + ) + print(f"[compute] display.npz 已生成: {npz_path}") + + def run_engine(engine, input_dir, output_dir, config): """调用外部计算引擎(C/C++/Fortran),生成 trajectory.txt。 @@ -961,32 +1134,43 @@ def run_engine(engine, input_dir, output_dir, config): script_dir = os.path.dirname(os.path.abspath(__file__)) system = platform.system().lower() engine_map = { - "c": "engines/c/build/dynamics_c", - "cpp": "engines/cpp/build/dynamics_cpp", + "c": "engines/c/build/dynamics_c", + "cpp": "engines/cpp/build/dynamics_cpp", + "c++": "engines/cpp/build/dynamics_cpp", "fortran": "engines/fortran/build/dynamics_f90", + "f90": "engines/fortran/build/dynamics_f90", + "python": None, # 特殊处理:用 sys.executable 调用 main.py } if engine not in engine_map: - raise ValueError(f"不支持的引擎: {engine},可选: {list(engine_map.keys())}") + raise ValueError(f"不支持的引擎: {engine},可选: c, cpp, fortran, python") - engine_rel = engine_map[engine] - engine_path = os.path.join(script_dir, engine_rel) + if engine == "python": + # Python 引擎:用当前解释器运行 engines/python/main.py + py_main = os.path.join(script_dir, "engines", "python", "main.py") + if not os.path.exists(py_main): + raise FileNotFoundError(f"Python 引擎脚本不存在: {py_main}") + found = py_main + engine_path = sys.executable + else: + engine_rel = engine_map[engine] + engine_path = os.path.join(script_dir, engine_rel) - # 自动检测可执行文件后缀和平台专用版本 - candidates = [ - engine_path, # 无后缀 - engine_path + ".exe", # Windows .exe - engine_path + f"_{system}.exe", # 平台专用 (c_linux.exe, c_darwin.exe) - ] - found = None - for p in candidates: - if os.path.exists(p): - found = p - break - if found is None: - raise FileNotFoundError( - f"引擎可执行文件不存在: 尝试了 {candidates}\n" - f"请先编译: cd engines/{engine} && make\n" - f"或安装交叉编译器后: cd engines/{engine} && make {system}") + # 自动检测可执行文件后缀和平台专用版本 + candidates = [ + engine_path, + engine_path + ".exe", + engine_path + f"_{system}.exe", + ] + found = None + for p in candidates: + if os.path.exists(p): + found = p + break + if found is None: + raise FileNotFoundError( + f"引擎可执行文件不存在: 尝试了 {candidates}\n" + f"请先编译: cd engines/{engine} && make\n" + f"或安装交叉编译器后: cd engines/{engine} && make {system}") # 构造 param.json(数值参数) G = parse_gravity_vector(config.get("G", [0, 0, -9.8])) @@ -1089,8 +1273,11 @@ def run_engine(engine, input_dir, output_dir, config): t_start = time.time() t_start_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # Python 引擎:[python, main.py, args];其他引擎:[exe, args] + _cmd = ([engine_path, found] if engine == "python" else [engine_path]) + \ + [os.path.abspath(input_dir), os.path.abspath(output_dir), param_path] _p = subprocess.Popen( - [engine_path, os.path.abspath(input_dir), os.path.abspath(output_dir), param_path], + _cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace') _engine_lines = [] diff --git a/dynamics.py b/dynamics.py index d0f9a06..47a43b4 100644 --- a/dynamics.py +++ b/dynamics.py @@ -226,7 +226,11 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output", # 2. 运行物理模拟 → output/trajectory.txt if config.get("step_simulate", 1): - engine = config.get("engine", "python") + _engine_aliases = {"c++": "cpp", "f90": "fortran", "f": "fortran"} + engine = _engine_aliases.get( + str(config.get("engine", "python")).lower(), + str(config.get("engine", "python")).lower() + ) total_steps = config["NT"] record_steps = total_steps - (config.get("warmup_steps") or 0) print(f"[run] 开始计算 总步数={total_steps} 记录步数={record_steps} DT={config['DT']}") @@ -243,11 +247,24 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output", config.pop("_skip_run", None) input_dir_abs = str(input_dir_path.resolve()) output_dir_abs = str(output_dir_path.resolve()) - # 外部引擎写完整 trajectory.txt,后续抽帧 - traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine( - engine, input_dir_abs, output_dir_abs, config) - if int(config.get("save_trajectory", 0)): - compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base)) + + # ── 优先尝试 DLL 路径(无文件 I/O,直接输出 display.npz)── + _dll_used = False + try: + from engines.engine_dll import is_dll_available + if is_dll_available(engine): + compute.run_engine_dll(engine, output_dir_abs, config) + _dll_used = True + print(f"[run] DLL 路径成功") + except Exception as _dll_err: + print(f"[run] DLL 路径不可用 ({_dll_err}),回退到子进程模式") + + if not _dll_used: + # 回退:子进程模式(读写 display.txt / trajectory.txt) + traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine( + engine, input_dir_abs, output_dir_abs, config) + if int(config.get("save_trajectory", 0)): + compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base)) _elapsed = _time.time() - _t0 print(f"[run] 引擎: {engine} 计算完成: {record_steps} 步 {_elapsed:.3f} s") @@ -345,11 +362,13 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output", if not os.path.exists(draw_script): print(f"[run] 未找到动画脚本: {draw_script}") else: - # 检查 display.txt 是否存在(step_sample=0 时可能没有) - disp_path = os.path.join(output_dir_abs, "display.txt") + # 检查 display.npz 或 display.txt 是否存在 + disp_npz = os.path.join(output_dir_abs, "display.npz") + disp_txt = os.path.join(output_dir_abs, "display.txt") + disp_path = disp_npz if os.path.exists(disp_npz) else disp_txt if not os.path.exists(disp_path): - print(f"[run] 错误: 找不到 {disp_path}") - print(f"[run] 启动动画需要先运行抽帧(step_sample: 1),或手动保留 output/display.txt") + print(f"[run] 错误: 找不到 display.npz 或 display.txt") + print(f"[run] 启动动画需要先运行模拟(step_simulate: 1)") else: try: print("[run] 正在启动 VisPy 3D 动画窗口…") diff --git a/engines/__init__.py b/engines/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/engines/c/Makefile b/engines/c/Makefile index 79cfbe4..03e99ce 100644 --- a/engines/c/Makefile +++ b/engines/c/Makefile @@ -8,6 +8,7 @@ CC = gcc CFLAGS = -O3 -march=native -Wall -Wextra LDFLAGS = -lm SRCS = main.c +LIB_SRC = dynamics_lib.c # 自动检测系统 UNAME_S := $(shell uname -s 2>/dev/null || echo Windows) @@ -15,15 +16,33 @@ UNAME_S := $(shell uname -s 2>/dev/null || echo Windows) # 目标文件名:统一使用 .exe 后缀(方便 Python 跨平台调用) TARGET = build/dynamics_c.exe +# DLL 目标(平台自动选择后缀) +ifeq ($(UNAME_S),Linux) + DLL_TARGET = build/dynamics_c.so + DLL_FLAGS = -shared -fPIC +else ifeq ($(UNAME_S),Darwin) + DLL_TARGET = build/dynamics_c.dylib + DLL_FLAGS = -dynamiclib +else + DLL_TARGET = build/dynamics_c.dll + DLL_FLAGS = -shared +endif + # ── 本地编译 ───────────────────────────────── -.PHONY: all clean linux windows macos +.PHONY: all dll clean linux windows macos all: $(TARGET) +dll: $(DLL_TARGET) + $(TARGET): $(SRCS) | build $(CC) $(CFLAGS) -o $@ $(SRCS) $(LDFLAGS) @echo " === C engine built: $@ ===" +$(DLL_TARGET): $(LIB_SRC) | build + $(CC) $(CFLAGS) $(DLL_FLAGS) -o $@ $(LIB_SRC) $(LDFLAGS) + @echo " === C DLL built: $@ ===" + build: mkdir -p build diff --git a/engines/c/_calib_cache.json b/engines/c/_calib_cache.json new file mode 100644 index 0000000..2a0a2a3 --- /dev/null +++ b/engines/c/_calib_cache.json @@ -0,0 +1 @@ +{"n_atoms": 40, "nt": 200000, "step_time": 2.5352442264556887e-05} \ No newline at end of file diff --git a/engines/c/dynamics_lib.c b/engines/c/dynamics_lib.c new file mode 100644 index 0000000..c83d984 --- /dev/null +++ b/engines/c/dynamics_lib.c @@ -0,0 +1,554 @@ +/** + * engines/c/dynamics_lib.c + * ------------------------- + * 纯计算 DLL:无文件 I/O,所有数据由 Python 以 NumPy 数组传入, + * 结果直接写入 Python 预分配的输出数组。 + * 算法与 main.c 和 compute.py 保持完全一致。 + * + * 编译(Windows DLL): + * gcc -O3 -march=native -shared -o build/dynamics_c.dll dynamics_lib.c -lm + * 编译(Linux .so): + * gcc -O3 -march=native -shared -fPIC -o build/dynamics_c.so dynamics_lib.c -lm + * 编译(macOS .dylib): + * gcc -O3 -march=native -dynamiclib -o build/dynamics_c.dylib dynamics_lib.c -lm + */ + +#ifdef _WIN32 +# define EXPORT __declspec(dllexport) +#else +# define EXPORT __attribute__((visibility("default"))) +#endif + +#include +#include +#include +#include + +/* ── 驱动力结构体 ─────────────────────────────────────────── */ +typedef struct { + int n_drivers; + const int *idx; /* [n_drivers] 0-based local atom index */ + const double *amp; /* [n_drivers*3] (ax,ay,az) interleaved */ + const double *freq; /* [n_drivers*3] */ + const double *phi; /* [n_drivers*3] radians */ + const double *eq; /* [n_drivers*3] equilibrium positions */ + const double *ncycles; /* [n_drivers] 0=unlimited */ + const int *has_period; /* [n_drivers] */ + /* mutable freeze positions (allocated internally) */ + double *freeze; /* [n_drivers*3] */ +} Drivers; + +/* ── 加速度:保守力(弹簧键 + 均匀重力场)────────────────── */ +static void accel_conservative( + int n, const double *x, const double *y, const double *z, + const double *m, + double Gx, double Gy, double Gz, + int gravity_field, int elastic_force, + int n_bonds, const int *bond_pairs, + const double *bond_k, const double *bond_r0, + double *ax, double *ay, double *az) +{ + for (int i = 0; i < n; i++) { + ax[i] = gravity_field ? Gx : 0.0; + ay[i] = gravity_field ? Gy : 0.0; + az[i] = gravity_field ? Gz : 0.0; + } + + if (!elastic_force || n_bonds == 0) return; + + for (int b = 0; b < n_bonds; b++) { + int ii = bond_pairs[b*2]; + int jj = bond_pairs[b*2+1]; + double dx = x[jj] - x[ii]; + double dy = y[jj] - y[ii]; + double dz = z[jj] - z[ii]; + double dist = sqrt(dx*dx + dy*dy + dz*dz); + if (dist < 1e-12) continue; + double k = bond_k[b]; + double r0 = bond_r0[b]; + double fac = k * (dist - r0) / dist; + double fx = fac * dx, fy = fac * dy, fz_b = fac * dz; + ax[ii] += fx / m[ii]; ay[ii] += fy / m[ii]; az[ii] += fz_b / m[ii]; + ax[jj] -= fx / m[jj]; ay[jj] -= fy / m[jj]; az[jj] -= fz_b / m[jj]; + } +} + +/* ── 完整加速度(含阻尼)────────────────────────────────── */ +static void accel_full( + int n, const double *x, const double *y, const double *z, + const double *vx, const double *vy, const double *vz, + const double *m, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bond_pairs, + const double *bond_k, const double *bond_r0, + double *ax, double *ay, double *az) +{ + accel_conservative(n, x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + n_bonds, bond_pairs, bond_k, bond_r0, + ax, ay, az); + if (damping_force) { + for (int i = 0; i < n; i++) { + ax[i] -= Bx * vx[i] / m[i]; + ay[i] -= By * vy[i] / m[i]; + az[i] -= Bz * vz[i] / m[i]; + } + } +} + +/* ── 边界:反弹(与 main.c limit_in_box 一致)────────────── */ +static inline void _limit1(double *p, double *v, double lo, double hi) { + if (*p > hi) { *p = hi; *v = -fabs(*v); } + if (*p < lo) { *p = lo; *v = fabs(*v); } +} + +/* ── 边界:回绕(与 main.c wrap_position 一致)──────────── */ +static inline void _wrap1(double *p, double lo, double hi) { + if (*p > hi) *p = lo; + if (*p < lo) *p = hi; +} + +/* ── 边界 + 固定约束(与 main.c apply_step 末尾一致)──────── */ +static void apply_boundary_and_constraints( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const int *fixed, const double *pos_init, + double box_a) +{ + double lo = -box_a, hi = box_a; + + /* 反弹 */ + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + _limit1(&x[i], &vx[i], lo, hi); + _limit1(&y[i], &vy[i], lo, hi); + _limit1(&z[i], &vz[i], lo, hi); + } + + /* 回绕 */ + for (int i = 0; i < n; i++) { + _wrap1(&x[i], lo, hi); + _wrap1(&y[i], lo, hi); + _wrap1(&z[i], lo, hi); + } + + /* 逐自由度固定约束:与 main.c 和 Python apply_fixed_constraints 一致 */ + for (int i = 0; i < n; i++) { + if (fixed[i*3+0]) { x[i] = pos_init[i*3+0]; vx[i] = 0.0; } + if (fixed[i*3+1]) { y[i] = pos_init[i*3+1]; vy[i] = 0.0; } + if (fixed[i*3+2]) { z[i] = pos_init[i*3+2]; vz[i] = 0.0; } + } +} + +/* ══════════════════════════════════════════════════════════ + * 蛙跳法(与 main.c leapfrog_step 完全一致) + * x(t), v(t-dt/2) → x(t+dt), v(t+dt/2) + * 无阻尼:纯辛积分。有阻尼:半隐式处理 α = B·dt/(2m) + * ══════════════════════════════════════════════════════════ */ +static void leapfrog_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, + double dt) +{ + double *ax = (double*)alloca(n*sizeof(double)*3); + double *ay = ax+n; double *az = ay+n; + + accel_conservative(n, x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + n_bonds, bp, bk, br0, ax, ay, az); + + int has_damp = damping_force && (Bx != 0.0 || By != 0.0 || Bz != 0.0); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + if (has_damp) { + double ax_ = Bx*dt/(2.0*m[i]); + double ay_ = By*dt/(2.0*m[i]); + double az_ = Bz*dt/(2.0*m[i]); + vx[i] = (vx[i]*(1.0-ax_) + ax[i]*dt) / (1.0+ax_); + vy[i] = (vy[i]*(1.0-ay_) + ay[i]*dt) / (1.0+ay_); + vz[i] = (vz[i]*(1.0-az_) + az[i]*dt) / (1.0+az_); + } else { + vx[i] += ax[i]*dt; + vy[i] += ay[i]*dt; + vz[i] += az[i]*dt; + } + x[i] += vx[i]*dt; + y[i] += vy[i]*dt; + z[i] += vz[i]*dt; + } +} + +/* ══════════════════════════════════════════════════════════ + * 显式欧拉法(与 main.c explicit_euler_step 一致) + * ══════════════════════════════════════════════════════════ */ +static void euler_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, + double dt) +{ + double *ax = (double*)alloca(n*sizeof(double)*3); + double *ay = ax+n; double *az = ay+n; + accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, ax, ay, az); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + x[i] += vx[i]*dt; y[i] += vy[i]*dt; z[i] += vz[i]*dt; + vx[i]+= ax[i]*dt; vy[i]+= ay[i]*dt; vz[i]+= az[i]*dt; + } +} + +/* ══════════════════════════════════════════════════════════ + * 隐式欧拉法(与 main.c implicit_euler_step 完全一致) + * + * main.c 逻辑: + * 1. 用 v_next ≈ (v + G·dt)/(1 + γ·dt) 预测(只含重力+阻尼,不含弹簧) + * 2. 用 (x, v_next) 计算完整加速度 a_next + * 3. v += a_next·dt; x += v·dt + * ══════════════════════════════════════════════════════════ */ +static void implicit_euler_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, + double dt) +{ + double *vxn = (double*)alloca(n*sizeof(double)*3); + double *vyn = vxn+n; double *vzn = vyn+n; + + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) { + vxn[i] = vyn[i] = vzn[i] = 0.0; continue; + } + double gx = Bx / m[i], gy = By / m[i], gz = Bz / m[i]; + vxn[i] = (vx[i] + Gx*dt) / (1.0 + gx*dt); + vyn[i] = (vy[i] + Gy*dt) / (1.0 + gy*dt); + vzn[i] = (vz[i] + Gz*dt) / (1.0 + gz*dt); + } + + double *ax = (double*)alloca(n*sizeof(double)*3); + double *ay = ax+n; double *az = ay+n; + accel_full(n, x, y, z, vxn, vyn, vzn, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, ax, ay, az); + + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + vx[i] += ax[i]*dt; + vy[i] += ay[i]*dt; + vz[i] += az[i]*dt; + x[i] += vx[i]*dt; + y[i] += vy[i]*dt; + z[i] += vz[i]*dt; + } +} + +/* ══════════════════════════════════════════════════════════ + * 中点法(与 main.c midpoint_step 完全一致) + * + * main.c 逻辑: + * 1. a = accel(x, v) + * 2. xm = x + 0.5·v·dt; vm = v + 0.5·a·dt + * 3. x = x + vm·dt (位置更新用 vm,即中点速度) + * 4. am = accel(xm, vm) + * 5. v = v + am·dt + * ══════════════════════════════════════════════════════════ */ +static void midpoint_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, + double dt) +{ + /* Allocate in one block for cache locality */ + double *buf = (double*)alloca(n*sizeof(double)*9); + double *ax = buf; + double *ay = ax+n; double *az = ay+n; + double *xm = az+n; double *ym = xm+n; double *zm = ym+n; + double *vxm = zm+n; double *vym = vxm+n; double *vzm = vym+n; + + accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, ax, ay, az); + + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) { + xm[i]=x[i]; ym[i]=y[i]; zm[i]=z[i]; + vxm[i]=vym[i]=vzm[i]=0.0; continue; + } + xm[i] = x[i] + 0.5*vx[i]*dt; + ym[i] = y[i] + 0.5*vy[i]*dt; + zm[i] = z[i] + 0.5*vz[i]*dt; + vxm[i] = vx[i] + 0.5*ax[i]*dt; + vym[i] = vy[i] + 0.5*ay[i]*dt; + vzm[i] = vz[i] + 0.5*az[i]*dt; + /* position updated with midpoint velocity (same as main.c) */ + x[i] = x[i] + vxm[i]*dt; + y[i] = y[i] + vym[i]*dt; + z[i] = z[i] + vzm[i]*dt; + } + + double *axm = (double*)alloca(n*sizeof(double)*3); + double *aym = axm+n; double *azm = aym+n; + accel_full(n, xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, axm, aym, azm); + + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + vx[i] += axm[i]*dt; + vy[i] += aym[i]*dt; + vz[i] += azm[i]*dt; + } +} + +/* ── 驱动力(与 main.c apply_driving_force 一致)────────── */ +static void apply_driving( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + double t, int step, double dt, Drivers *drv) +{ + (void)n; + if (!drv || drv->n_drivers == 0) return; + const double TWO_PI = 2.0 * 3.14159265358979323846; + + for (int d = 0; d < drv->n_drivers; d++) { + int idx = drv->idx[d]; + double fx = drv->freq[d*3+0]; + double fy = drv->freq[d*3+1]; + double fz = drv->freq[d*3+2]; + + if (drv->has_period[d]) { + double mf = fabs(fx) > fabs(fy) ? fabs(fx) : fabs(fy); + if (fabs(fz) > mf) mf = fabs(fz); + int period_steps = 0; + if (mf > 1e-12) + period_steps = (int)(drv->ncycles[d] / mf / dt); + if (step > period_steps) { + x[idx] = drv->freeze[d*3+0]; + y[idx] = drv->freeze[d*3+1]; + z[idx] = drv->freeze[d*3+2]; + vx[idx] = vy[idx] = vz[idx] = 0.0; + continue; + } + + double px = drv->eq[d*3+0] + drv->amp[d*3+0]*cos(TWO_PI*fx*t + drv->phi[d*3+0]); + double py = drv->eq[d*3+1] + drv->amp[d*3+1]*cos(TWO_PI*fy*t + drv->phi[d*3+1]); + double pz = drv->eq[d*3+2] + drv->amp[d*3+2]*cos(TWO_PI*fz*t + drv->phi[d*3+2]); + if (step == period_steps) { + drv->freeze[d*3+0] = px; + drv->freeze[d*3+1] = py; + drv->freeze[d*3+2] = pz; + } + } + + x[idx] = drv->eq[d*3+0] + drv->amp[d*3+0]*cos(TWO_PI*fx*t + drv->phi[d*3+0]); + y[idx] = drv->eq[d*3+1] + drv->amp[d*3+1]*cos(TWO_PI*fy*t + drv->phi[d*3+1]); + z[idx] = drv->eq[d*3+2] + drv->amp[d*3+2]*cos(TWO_PI*fz*t + drv->phi[d*3+2]); + vx[idx] = -drv->amp[d*3+0]*TWO_PI*fx*sin(TWO_PI*fx*t + drv->phi[d*3+0]); + vy[idx] = -drv->amp[d*3+1]*TWO_PI*fy*sin(TWO_PI*fy*t + drv->phi[d*3+1]); + vz[idx] = -drv->amp[d*3+2]*TWO_PI*fz*sin(TWO_PI*fz*t + drv->phi[d*3+2]); + } +} + +/* ══════════════════════════════════════════════════════════ + * 导出函数:run_dynamics + * + * 与 main.c 的计算顺序完全一致: + * 1. leapfrog 初始化 v(-dt/2) + * 2. 初始驱动 t=0 + * 3. 预热循环(不记录) + * 4. 记录循环:drive → record → step → boundary → constraints + * + * 参数说明(所有数组均为 C-contiguous 行优先 float64/int32): + * n_atoms 原子数 + * pos_init 初始位置 [n_atoms*3] x0,y0,z0, x1,y1,z1, ... + * vel_init 初始速度 [n_atoms*3] + * masses 质量 [n_atoms] + * fixed 自由度约束 [n_atoms*3] int32, 1=固定 + * n_bonds 键数 + * bond_pairs 键对 [n_bonds*2] int32, 0-based local index + * bond_k 刚度 [n_bonds] + * bond_r0 平衡键长 [n_bonds] + * box_a 盒子半边长 + * dt 时间步长 + * NT 总步数(含预热) + * NSTEP 抽帧间隔 + * warmup_steps 预热步数 + * method_id 0=euler 1=implicit 2=midpoint 3=leapfrog + * Gx/Gy/Gz 均匀重力场加速度分量 + * Bx/By/Bz 阻尼系数分量 + * gravity_field / elastic_force / damping_force 力开关 + * gravity_strength 原子间引力强度(暂未实现,留接口) + * n_drivers 驱动原子数 + * drv_idx 驱动原子局部索引 [n_drivers] int32 + * drv_amp 振幅 [n_drivers*3] + * drv_freq 频率 [n_drivers*3] + * drv_phi 初相(弧度)[n_drivers*3] + * drv_eq 平衡位置 [n_drivers*3] + * drv_ncycles 周期数 [n_drivers] 0=不限 + * drv_has_period [n_drivers] int32 + * n_frames 输出帧数(Python 预计算:(NT-warmup)/NSTEP 向上取整) + * out_x/y/z/vx/vy/vz 输出数组 [n_frames*n_atoms] 由 Python 预分配 + * progress_cb 进度回调(可为 NULL) + * + * 返回:0=成功,负数=错误 + * ══════════════════════════════════════════════════════════ */ +EXPORT int run_dynamics( + int n_atoms, + const double *pos_init, + const double *vel_init, + const double *masses, + const int *fixed, + int n_bonds, + const int *bond_pairs, + const double *bond_k, + const double *bond_r0, + double box_a, double dt, + int NT, int NSTEP, int warmup_steps, int method_id, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + double gravity_strength, + int n_drivers, + const int *drv_idx, + const double *drv_amp, + const double *drv_freq, + const double *drv_phi, + const double *drv_eq, + const double *drv_ncycles, + const int *drv_has_period, + int n_frames, + double *out_x, double *out_y, double *out_z, + double *out_vx, double *out_vy, double *out_vz, + void (*progress_cb)(int step, int total)) +{ + (void)gravity_strength; /* 原子间引力暂未实现 */ + + int n = n_atoms; + + /* ── 工作数组 ── */ + double *x = (double*)malloc(n*sizeof(double)); + double *y = (double*)malloc(n*sizeof(double)); + double *z = (double*)malloc(n*sizeof(double)); + double *vx = (double*)malloc(n*sizeof(double)); + double *vy = (double*)malloc(n*sizeof(double)); + double *vz = (double*)malloc(n*sizeof(double)); + if (!x||!y||!z||!vx||!vy||!vz) return -1; + + for (int i = 0; i < n; i++) { + x[i]=pos_init[i*3+0]; y[i]=pos_init[i*3+1]; z[i]=pos_init[i*3+2]; + vx[i]=vel_init[i*3+0]; vy[i]=vel_init[i*3+1]; vz[i]=vel_init[i*3+2]; + } + + /* ── 驱动结构 ── */ + Drivers drv; + drv.n_drivers = n_drivers; + drv.idx = drv_idx; + drv.amp = drv_amp; + drv.freq = drv_freq; + drv.phi = drv_phi; + drv.eq = drv_eq; + drv.ncycles = drv_ncycles; + drv.has_period = drv_has_period; + drv.freeze = NULL; + if (n_drivers > 0) { + drv.freeze = (double*)calloc(n_drivers*3, sizeof(double)); + if (!drv.freeze) { free(x);free(y);free(z);free(vx);free(vy);free(vz); return -2; } + } + + /* ── 内联步进宏 ── */ +#define DO_STEP() do { \ + switch (method_id) { \ + case 0: euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + case 1: implicit_euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + case 2: midpoint_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + default: leapfrog_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + } \ + apply_boundary_and_constraints(n,x,y,z,vx,vy,vz,fixed,pos_init,box_a); \ +} while(0) + + /* ── 蛙跳法:初始化 v(-dt/2) = v(0) - 0.5·a_c(0)·dt ── */ + if (method_id == 3) { + double *ax0 = (double*)alloca(n*sizeof(double)*3); + double *ay0 = ax0+n; double *az0 = ay0+n; + accel_conservative(n, x, y, z, masses, Gx, Gy, Gz, + gravity_field, elastic_force, + n_bonds, bond_pairs, bond_k, bond_r0, + ax0, ay0, az0); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + vx[i] -= 0.5*ax0[i]*dt; + vy[i] -= 0.5*ay0[i]*dt; + vz[i] -= 0.5*az0[i]*dt; + } + } + + /* ── 初始驱动 t=0(与 main.c 一致:leapfrog init 之后施加)── */ + if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, 0.0, 0, dt, &drv); + + /* ── 预热(不记录)── */ + for (int s = 0; s < warmup_steps; s++) { + double tw = (s + 1) * dt; + if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, tw, s, dt, &drv); + DO_STEP(); + } + + /* ── 记录循环 ── */ + int record_steps = NT - warmup_steps; + int prog_interval = record_steps / 100; + if (prog_interval < 1) prog_interval = 1; + int frame_idx = 0; + + for (int s = 0; s < record_steps; s++) { + if (progress_cb && s % prog_interval == 0 && s > 0) + progress_cb(s, record_steps); + + double t = (s + warmup_steps) * dt; + if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, t, s, dt, &drv); + + /* 抽帧记录(drive 之后,step 之前,与 main.c 一致)*/ + if (s % NSTEP == 0 && frame_idx < n_frames) { + int base = frame_idx * n; + for (int i = 0; i < n; i++) { + out_x [base+i] = x[i]; out_y [base+i] = y[i]; out_z [base+i] = z[i]; + out_vx[base+i] = vx[i]; out_vy[base+i] = vy[i]; out_vz[base+i] = vz[i]; + } + frame_idx++; + } + DO_STEP(); + } + +#undef DO_STEP + + free(x); free(y); free(z); + free(vx); free(vy); free(vz); + if (drv.freeze) free(drv.freeze); + return 0; +} diff --git a/engines/cpp/Makefile b/engines/cpp/Makefile new file mode 100644 index 0000000..93cefc7 --- /dev/null +++ b/engines/cpp/Makefile @@ -0,0 +1,49 @@ +# engines/cpp/Makefile + +CXX = g++ +SRCS = main.cpp +LIB_SRC = dynamics_lib.cpp + +UNAME_S := $(shell uname -s 2>/dev/null || echo Windows) + +CXXFLAGS = -O3 -march=native -std=c++17 -Wall -Wextra -D_USE_MATH_DEFINES + +# Windows 下静态链接运行时,避免 libstdc++-6.dll / libgcc_s_seh-1.dll 版本冲突 +ifeq ($(UNAME_S),Windows) + STATIC_FLAGS = -static-libgcc -static-libstdc++ +else + STATIC_FLAGS = +endif + +TARGET = build/dynamics_cpp.exe + +ifeq ($(UNAME_S),Linux) + DLL_TARGET = build/dynamics_cpp.so + DLL_FLAGS = -shared -fPIC +else ifeq ($(UNAME_S),Darwin) + DLL_TARGET = build/dynamics_cpp.dylib + DLL_FLAGS = -dynamiclib +else + DLL_TARGET = build/dynamics_cpp.dll + DLL_FLAGS = -shared +endif + +.PHONY: all dll clean + +all: $(TARGET) + +dll: $(DLL_TARGET) + +$(TARGET): $(SRCS) | build + $(CXX) $(CXXFLAGS) $(STATIC_FLAGS) -o $@ $(SRCS) + @echo " === C++ engine built: $@ ===" + +$(DLL_TARGET): $(LIB_SRC) | build + $(CXX) $(CXXFLAGS) $(STATIC_FLAGS) $(DLL_FLAGS) -o $@ $(LIB_SRC) + @echo " === C++ DLL built: $@ ===" + +build: + mkdir -p build + +clean: + rm -rf build *.o diff --git a/engines/cpp/_calib_cache.json b/engines/cpp/_calib_cache.json new file mode 100644 index 0000000..dffc098 --- /dev/null +++ b/engines/cpp/_calib_cache.json @@ -0,0 +1 @@ +{"n_atoms": 40, "nt": 200000, "step_time": 0.0022148028612136842} \ No newline at end of file diff --git a/engines/cpp/dynamics_lib.cpp b/engines/cpp/dynamics_lib.cpp new file mode 100644 index 0000000..bf5bcdc --- /dev/null +++ b/engines/cpp/dynamics_lib.cpp @@ -0,0 +1,450 @@ +/** + * engines/cpp/dynamics_lib.cpp + * ----------------------------- + * 纯计算 DLL(C++ 版):无文件 I/O,所有数据由 Python 以 NumPy 数组传入。 + * 算法与 main.cpp / compute.py 保持完全一致。 + * + * 编译(Windows): + * g++ -O3 -march=native -std=c++17 -shared -o build/dynamics_cpp.dll dynamics_lib.cpp + * 编译(Linux): + * g++ -O3 -march=native -std=c++17 -shared -fPIC -o build/dynamics_cpp.so dynamics_lib.cpp + * 编译(macOS): + * g++ -O3 -march=native -std=c++17 -dynamiclib -o build/dynamics_cpp.dylib dynamics_lib.cpp + */ + +#ifdef _WIN32 +# define EXPORT extern "C" __declspec(dllexport) +#else +# define EXPORT extern "C" __attribute__((visibility("default"))) +#endif + +#include +#include +#include +#include + +/* ── 驱动力结构体 ─────────────────────────────────────────── */ +struct Drivers { + int n_drivers = 0; + const int *idx = nullptr; + const double *amp = nullptr; + const double *freq = nullptr; + const double *phi = nullptr; + const double *eq = nullptr; + const double *ncycles = nullptr; + const int *has_period = nullptr; + std::vector freeze; /* [n_drivers*3] 冻结位置(period 结束时锁定)*/ +}; + +/* ── 加速度:保守力(弹簧键 + 均匀重力场)────────────────── */ +static void accel_conservative( + int n, const double *x, const double *y, const double *z, + const double *m, + double Gx, double Gy, double Gz, + int gravity_field, int elastic_force, + int n_bonds, const int *bond_pairs, + const double *bond_k, const double *bond_r0, + double *ax, double *ay, double *az) +{ + for (int i = 0; i < n; i++) { + ax[i] = gravity_field ? Gx : 0.0; + ay[i] = gravity_field ? Gy : 0.0; + az[i] = gravity_field ? Gz : 0.0; + } + if (!elastic_force || n_bonds == 0) return; + for (int b = 0; b < n_bonds; b++) { + int ii = bond_pairs[b*2]; + int jj = bond_pairs[b*2+1]; + double dx = x[jj]-x[ii], dy = y[jj]-y[ii], dz = z[jj]-z[ii]; + double dist = std::sqrt(dx*dx + dy*dy + dz*dz); + if (dist < 1e-12) continue; + double fac = bond_k[b] * (dist - bond_r0[b]) / dist; + double fx = fac*dx, fy = fac*dy, fz_b = fac*dz; + ax[ii] += fx/m[ii]; ay[ii] += fy/m[ii]; az[ii] += fz_b/m[ii]; + ax[jj] -= fx/m[jj]; ay[jj] -= fy/m[jj]; az[jj] -= fz_b/m[jj]; + } +} + +/* ── 完整加速度(含阻尼)────────────────────────────────── */ +static void accel_full( + int n, const double *x, const double *y, const double *z, + const double *vx, const double *vy, const double *vz, + const double *m, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bond_pairs, + const double *bond_k, const double *bond_r0, + double *ax, double *ay, double *az) +{ + accel_conservative(n, x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + n_bonds, bond_pairs, bond_k, bond_r0, + ax, ay, az); + if (damping_force) { + for (int i = 0; i < n; i++) { + ax[i] -= Bx * vx[i] / m[i]; + ay[i] -= By * vy[i] / m[i]; + az[i] -= Bz * vz[i] / m[i]; + } + } +} + +/* ── 边界:反弹 ──────────────────────────────────────────── */ +static inline void _limit1(double &p, double &v, double lo, double hi) { + if (p > hi) { p = hi; v = -std::fabs(v); } + if (p < lo) { p = lo; v = std::fabs(v); } +} + +/* ── 边界:回绕 ──────────────────────────────────────────── */ +static inline void _wrap1(double &p, double lo, double hi) { + if (p > hi) p = lo; + if (p < lo) p = hi; +} + +/* ── 边界 + 固定约束 ────────────────────────────────────── */ +static void apply_boundary_and_constraints( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const int *fixed, const double *pos_init, double box_a) +{ + double lo = -box_a, hi = box_a; + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + _limit1(x[i], vx[i], lo, hi); + _limit1(y[i], vy[i], lo, hi); + _limit1(z[i], vz[i], lo, hi); + } + for (int i = 0; i < n; i++) { + _wrap1(x[i], lo, hi); + _wrap1(y[i], lo, hi); + _wrap1(z[i], lo, hi); + } + for (int i = 0; i < n; i++) { + if (fixed[i*3+0]) { x[i] = pos_init[i*3+0]; vx[i] = 0.0; } + if (fixed[i*3+1]) { y[i] = pos_init[i*3+1]; vy[i] = 0.0; } + if (fixed[i*3+2]) { z[i] = pos_init[i*3+2]; vz[i] = 0.0; } + } +} + +/* ══════════════════════════════════════════════════════════ + * 蛙跳法(与 main.cpp leapfrog_step 完全一致) + * ══════════════════════════════════════════════════════════ */ +static void leapfrog_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, double dt) +{ + std::vector buf(n * 3); + double *ax = buf.data(), *ay = ax+n, *az = ay+n; + accel_conservative(n, x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + n_bonds, bp, bk, br0, ax, ay, az); + bool has_damp = damping_force && (Bx != 0.0 || By != 0.0 || Bz != 0.0); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + if (has_damp) { + double ax_ = Bx*dt/(2.0*m[i]); + double ay_ = By*dt/(2.0*m[i]); + double az_ = Bz*dt/(2.0*m[i]); + vx[i] = (vx[i]*(1.0-ax_) + ax[i]*dt) / (1.0+ax_); + vy[i] = (vy[i]*(1.0-ay_) + ay[i]*dt) / (1.0+ay_); + vz[i] = (vz[i]*(1.0-az_) + az[i]*dt) / (1.0+az_); + } else { + vx[i] += ax[i]*dt; + vy[i] += ay[i]*dt; + vz[i] += az[i]*dt; + } + x[i] += vx[i]*dt; + y[i] += vy[i]*dt; + z[i] += vz[i]*dt; + } +} + +/* ══════════════════════════════════════════════════════════ + * 显式欧拉法 + * ══════════════════════════════════════════════════════════ */ +static void euler_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, double dt) +{ + std::vector buf(n * 3); + double *ax = buf.data(), *ay = ax+n, *az = ay+n; + accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, ax, ay, az); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + x[i] += vx[i]*dt; y[i] += vy[i]*dt; z[i] += vz[i]*dt; + vx[i]+= ax[i]*dt; vy[i]+= ay[i]*dt; vz[i]+= az[i]*dt; + } +} + +/* ══════════════════════════════════════════════════════════ + * 隐式欧拉法(与 main.cpp implicit_euler_step 完全一致) + * ══════════════════════════════════════════════════════════ */ +static void implicit_euler_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, double dt) +{ + std::vector vbuf(n * 3), abuf(n * 3); + double *vxn = vbuf.data(), *vyn = vxn+n, *vzn = vyn+n; + double *ax = abuf.data(), *ay = ax+n, *az = ay+n; + + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) { + vxn[i] = vyn[i] = vzn[i] = 0.0; continue; + } + double gx = Bx/m[i], gy = By/m[i], gz = Bz/m[i]; + vxn[i] = (vx[i] + Gx*dt) / (1.0 + gx*dt); + vyn[i] = (vy[i] + Gy*dt) / (1.0 + gy*dt); + vzn[i] = (vz[i] + Gz*dt) / (1.0 + gz*dt); + } + accel_full(n, x, y, z, vxn, vyn, vzn, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, ax, ay, az); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + vx[i] += ax[i]*dt; vy[i] += ay[i]*dt; vz[i] += az[i]*dt; + x[i] += vx[i]*dt; y[i] += vy[i]*dt; z[i] += vz[i]*dt; + } +} + +/* ══════════════════════════════════════════════════════════ + * 中点法(与 main.cpp midpoint_step 完全一致) + * ══════════════════════════════════════════════════════════ */ +static void midpoint_step( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + const double *m, const int *fixed, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + int n_bonds, const int *bp, const double *bk, const double *br0, double dt) +{ + std::vector buf(n * 9); + double *ax = buf.data(); + double *ay = ax+n; double *az = ay+n; + double *xm = az+n; double *ym = xm+n; double *zm = ym+n; + double *vxm = zm+n; double *vym = vxm+n; double *vzm = vym+n; + + accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, ax, ay, az); + + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) { + xm[i]=x[i]; ym[i]=y[i]; zm[i]=z[i]; + vxm[i]=vym[i]=vzm[i]=0.0; continue; + } + xm[i] = x[i] + 0.5*vx[i]*dt; + ym[i] = y[i] + 0.5*vy[i]*dt; + zm[i] = z[i] + 0.5*vz[i]*dt; + vxm[i] = vx[i] + 0.5*ax[i]*dt; + vym[i] = vy[i] + 0.5*ay[i]*dt; + vzm[i] = vz[i] + 0.5*az[i]*dt; + x[i] = x[i] + vxm[i]*dt; + y[i] = y[i] + vym[i]*dt; + z[i] = z[i] + vzm[i]*dt; + } + + std::vector abuf(n * 3); + double *axm = abuf.data(), *aym = axm+n, *azm = aym+n; + accel_full(n, xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + n_bonds, bp, bk, br0, axm, aym, azm); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + vx[i] += axm[i]*dt; + vy[i] += aym[i]*dt; + vz[i] += azm[i]*dt; + } +} + +/* ── 驱动力 ─────────────────────────────────────────────── */ +static void apply_driving( + int n, double *x, double *y, double *z, + double *vx, double *vy, double *vz, + double t, int step, double dt, Drivers &drv) +{ + (void)n; + if (drv.n_drivers == 0) return; + constexpr double TWO_PI = 2.0 * 3.14159265358979323846; + + for (int d = 0; d < drv.n_drivers; d++) { + int idx = drv.idx[d]; + double fx = drv.freq[d*3+0]; + double fy = drv.freq[d*3+1]; + double fz = drv.freq[d*3+2]; + + if (drv.has_period[d]) { + double mf = std::fabs(fx) > std::fabs(fy) ? std::fabs(fx) : std::fabs(fy); + if (std::fabs(fz) > mf) mf = std::fabs(fz); + int period_steps = 0; + if (mf > 1e-12) + period_steps = (int)(drv.ncycles[d] / mf / dt); + if (step > period_steps) { + x[idx] = drv.freeze[d*3+0]; + y[idx] = drv.freeze[d*3+1]; + z[idx] = drv.freeze[d*3+2]; + vx[idx] = vy[idx] = vz[idx] = 0.0; + continue; + } + double px = drv.eq[d*3+0] + drv.amp[d*3+0]*std::cos(TWO_PI*fx*t + drv.phi[d*3+0]); + double py = drv.eq[d*3+1] + drv.amp[d*3+1]*std::cos(TWO_PI*fy*t + drv.phi[d*3+1]); + double pz = drv.eq[d*3+2] + drv.amp[d*3+2]*std::cos(TWO_PI*fz*t + drv.phi[d*3+2]); + if (step == period_steps) { + drv.freeze[d*3+0] = px; + drv.freeze[d*3+1] = py; + drv.freeze[d*3+2] = pz; + } + } + x[idx] = drv.eq[d*3+0] + drv.amp[d*3+0]*std::cos(TWO_PI*fx*t + drv.phi[d*3+0]); + y[idx] = drv.eq[d*3+1] + drv.amp[d*3+1]*std::cos(TWO_PI*fy*t + drv.phi[d*3+1]); + z[idx] = drv.eq[d*3+2] + drv.amp[d*3+2]*std::cos(TWO_PI*fz*t + drv.phi[d*3+2]); + vx[idx] = -drv.amp[d*3+0]*TWO_PI*fx*std::sin(TWO_PI*fx*t + drv.phi[d*3+0]); + vy[idx] = -drv.amp[d*3+1]*TWO_PI*fy*std::sin(TWO_PI*fy*t + drv.phi[d*3+1]); + vz[idx] = -drv.amp[d*3+2]*TWO_PI*fz*std::sin(TWO_PI*fz*t + drv.phi[d*3+2]); + } +} + +/* ══════════════════════════════════════════════════════════ + * 导出函数:run_dynamics(接口与 C 版完全相同) + * ══════════════════════════════════════════════════════════ */ +EXPORT int run_dynamics( + int n_atoms, + const double *pos_init, + const double *vel_init, + const double *masses, + const int *fixed, + int n_bonds, + const int *bond_pairs, + const double *bond_k, + const double *bond_r0, + double box_a, double dt, + int NT, int NSTEP, int warmup_steps, int method_id, + double Gx, double Gy, double Gz, + double Bx, double By, double Bz, + int gravity_field, int elastic_force, int damping_force, + double gravity_strength, + int n_drivers, + const int *drv_idx, + const double *drv_amp, + const double *drv_freq, + const double *drv_phi, + const double *drv_eq, + const double *drv_ncycles, + const int *drv_has_period, + int n_frames, + double *out_x, double *out_y, double *out_z, + double *out_vx, double *out_vy, double *out_vz, + void (*progress_cb)(int step, int total)) +{ + (void)gravity_strength; + int n = n_atoms; + + std::vector xv(n), yv(n), zv(n); + std::vector vxv(n), vyv(n), vzv(n); + for (int i = 0; i < n; i++) { + xv[i]=pos_init[i*3+0]; yv[i]=pos_init[i*3+1]; zv[i]=pos_init[i*3+2]; + vxv[i]=vel_init[i*3+0]; vyv[i]=vel_init[i*3+1]; vzv[i]=vel_init[i*3+2]; + } + double *x=xv.data(), *y=yv.data(), *z=zv.data(); + double *vx=vxv.data(), *vy=vyv.data(), *vz=vzv.data(); + + Drivers drv; + drv.n_drivers = n_drivers; + drv.idx = drv_idx; + drv.amp = drv_amp; + drv.freq = drv_freq; + drv.phi = drv_phi; + drv.eq = drv_eq; + drv.ncycles = drv_ncycles; + drv.has_period = drv_has_period; + if (n_drivers > 0) + drv.freeze.assign(n_drivers * 3, 0.0); + +#define DO_STEP() do { \ + switch (method_id) { \ + case 0: euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + case 1: implicit_euler_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + case 2: midpoint_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + default: leapfrog_step(n,x,y,z,vx,vy,vz,masses,fixed,Gx,Gy,Gz,Bx,By,Bz, \ + gravity_field,elastic_force,damping_force, \ + n_bonds,bond_pairs,bond_k,bond_r0,dt); break; \ + } \ + apply_boundary_and_constraints(n,x,y,z,vx,vy,vz,fixed,pos_init,box_a); \ +} while(0) + + /* 蛙跳法:初始化 v(-dt/2) */ + if (method_id == 3) { + std::vector ibuf(n * 3); + double *ax0=ibuf.data(), *ay0=ax0+n, *az0=ay0+n; + accel_conservative(n, x, y, z, masses, Gx, Gy, Gz, + gravity_field, elastic_force, + n_bonds, bond_pairs, bond_k, bond_r0, + ax0, ay0, az0); + for (int i = 0; i < n; i++) { + if (fixed[i*3] && fixed[i*3+1] && fixed[i*3+2]) continue; + vx[i] -= 0.5*ax0[i]*dt; + vy[i] -= 0.5*ay0[i]*dt; + vz[i] -= 0.5*az0[i]*dt; + } + } + + /* 初始驱动 t=0 */ + if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, 0.0, 0, dt, drv); + + /* 预热 */ + for (int s = 0; s < warmup_steps; s++) { + double tw = (s + 1) * dt; + if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, tw, s, dt, drv); + DO_STEP(); + } + + /* 记录循环 */ + int record_steps = NT - warmup_steps; + int prog_interval = std::max(1, record_steps / 100); + int frame_idx = 0; + + for (int s = 0; s < record_steps; s++) { + if (progress_cb && s % prog_interval == 0 && s > 0) + progress_cb(s, record_steps); + + double t = (s + warmup_steps) * dt; + if (n_drivers > 0) apply_driving(n, x, y, z, vx, vy, vz, t, s, dt, drv); + + if (s % NSTEP == 0 && frame_idx < n_frames) { + int base = frame_idx * n; + for (int i = 0; i < n; i++) { + out_x [base+i] = x[i]; out_y [base+i] = y[i]; out_z [base+i] = z[i]; + out_vx[base+i] = vx[i]; out_vy[base+i] = vy[i]; out_vz[base+i] = vz[i]; + } + frame_idx++; + } + DO_STEP(); + } + +#undef DO_STEP + return 0; +} diff --git a/engines/engine_dll.py b/engines/engine_dll.py new file mode 100644 index 0000000..cb32d34 --- /dev/null +++ b/engines/engine_dll.py @@ -0,0 +1,424 @@ +""" +engines/engine_dll.py +--------------------- +Python ctypes 包装器:加载 C/C++/Fortran 动态链接库并调用 run_dynamics()。 + +用法(由 compute.py 内部调用,不直接运行): + + from engines.engine_dll import load_dll, run_dynamics_dll + + dll = load_dll("c") # 自动查找 engines/c/build/dynamics_c.dll/.so/.dylib + arrays = run_dynamics_dll(dll, config, atom_data, bond_data, driver_data) + # arrays: dict with keys x, y, z, vx, vy, vz shape=(n_frames, n_atoms) + +DLL 编译(C 版本): + Windows: gcc -O3 -shared -o engines/c/build/dynamics_c.dll engines/c/dynamics_lib.c -lm + Linux: gcc -O3 -shared -fPIC -o engines/c/build/dynamics_c.so engines/c/dynamics_lib.c -lm + macOS: gcc -O3 -dynamiclib -o engines/c/build/dynamics_c.dylib engines/c/dynamics_lib.c -lm +""" + +import ctypes +import os +import platform +import numpy as np + +# ── DLL 文件名后缀 ───────────────────────────────────────────── +_SUFFIX = { + "windows": ".dll", + "linux": ".so", + "darwin": ".dylib", +} + +# ── method 字符串 → 整数 ID ──────────────────────────────────── +_METHOD_ID = { + "explicit_euler": 0, + "euler": 0, + "implicit_euler": 1, + "midpoint": 2, + "leapfrog": 3, +} + +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +_DLL_NAME = { + "c": "dynamics_c", + "cpp": "dynamics_cpp", + "c++": "dynamics_cpp", + "fortran": "dynamics_f90", + "f90": "dynamics_f90", + # "python" 引擎通过直接 import 调用,不使用 DLL +} + +# 引擎名规范化:将别名统一为目录名 +_ENGINE_DIR = { + "c": "c", + "cpp": "cpp", + "c++": "cpp", + "fortran": "fortran", + "f90": "fortran", + "python": "python", +} + + +def _dll_candidates(engine: str) -> list[str]: + """返回 DLL 候选路径列表(按优先级)。""" + sys = platform.system().lower() + ext = _SUFFIX.get(sys, ".so") + eng_dir = _ENGINE_DIR.get(engine, engine) + name = _DLL_NAME.get(engine, f"dynamics_{engine}") + base = os.path.join(_HERE, eng_dir, "build", name) + return [ + base + ext, + base + ".dll", + base + ".so", + base + ".dylib", + ] + + +def load_dll(engine: str = "c"): + """加载指定引擎。 + + - C/C++/Fortran: 返回 ctypes.CDLL 对象 + - Python: 返回模块对象(直接 import,无需编译) + + Args: + engine: "c", "cpp", "fortran", 或 "python" + Raises: + FileNotFoundError: DLL/模块文件不存在 + """ + if _ENGINE_DIR.get(engine, engine) == "python": + import importlib.util, sys as _sys + mod_path = os.path.join(_HERE, "python", "dynamics_lib.py") + if not os.path.exists(mod_path): + raise FileNotFoundError(f"Python 引擎未找到: {mod_path}") + spec = importlib.util.spec_from_file_location( + "engines.python.dynamics_lib", mod_path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod # 返回模块,不是 CDLL + + for p in _dll_candidates(engine): + if os.path.exists(p): + lib = ctypes.CDLL(p) + _setup_prototype(lib) + return lib + raise FileNotFoundError( + f"DLL 未找到(引擎 {engine}),候选路径:\n" + + "\n".join(f" {p}" for p in _dll_candidates(engine)) + + f"\n请先编译:cd engines/{engine} && make dll" + ) + + +def _setup_prototype(lib: ctypes.CDLL) -> None: + """配置 run_dynamics 的参数类型和返回类型。""" + c_dbl_p = ctypes.POINTER(ctypes.c_double) + c_int_p = ctypes.POINTER(ctypes.c_int) + cb_type = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int) + + lib.run_dynamics.restype = ctypes.c_int + lib.run_dynamics.argtypes = [ + ctypes.c_int, # n_atoms + c_dbl_p, # pos_init [n_atoms*3] + c_dbl_p, # vel_init [n_atoms*3] + c_dbl_p, # masses [n_atoms] + c_int_p, # fixed [n_atoms*3] + ctypes.c_int, # n_bonds + c_int_p, # bond_pairs [n_bonds*2] + c_dbl_p, # bond_k [n_bonds] + c_dbl_p, # bond_r0 [n_bonds] + ctypes.c_double, # box_a + ctypes.c_double, # dt + ctypes.c_int, # NT + ctypes.c_int, # NSTEP + ctypes.c_int, # warmup_steps + ctypes.c_int, # method_id + ctypes.c_double, # Gx + ctypes.c_double, # Gy + ctypes.c_double, # Gz + ctypes.c_double, # Bx + ctypes.c_double, # By + ctypes.c_double, # Bz + ctypes.c_int, # gravity_field + ctypes.c_int, # elastic_force + ctypes.c_int, # damping_force + ctypes.c_double, # gravity_strength + ctypes.c_int, # n_drivers + c_int_p, # drv_idx [n_drivers] + c_dbl_p, # drv_amp [n_drivers*3] + c_dbl_p, # drv_freq [n_drivers*3] + c_dbl_p, # drv_phi [n_drivers*3] + c_dbl_p, # drv_eq [n_drivers*3] + c_dbl_p, # drv_ncycles [n_drivers] + c_int_p, # drv_has_period [n_drivers] + ctypes.c_int, # n_frames + c_dbl_p, # out_x + c_dbl_p, # out_y + c_dbl_p, # out_z + c_dbl_p, # out_vx + c_dbl_p, # out_vy + c_dbl_p, # out_vz + cb_type, # progress_cb (可为 NULL) + ] + + +def _c_dbl(arr: np.ndarray): + """返回 float64 C 连续数组的 ctypes 指针。""" + a = np.ascontiguousarray(arr, dtype=np.float64) + return a.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), a + + +def _c_int(arr: np.ndarray): + """返回 int32 C 连续数组的 ctypes 指针。""" + a = np.ascontiguousarray(arr, dtype=np.int32) + return a.ctypes.data_as(ctypes.POINTER(ctypes.c_int)), a + + +def _is_python_module(lib) -> bool: + """判断 lib 是否为 Python 引擎模块(而非 ctypes.CDLL)。""" + return not isinstance(lib, ctypes.CDLL) + + +def _run_dynamics_python(lib, config, atom_positions, atom_velocities, atom_masses, + atom_fixed, bond_pairs, bond_stiffness, bond_rest_lengths, + driver_data, atom_ids, progress_cb=None) -> dict: + """调用 Python 引擎的 run_dynamics(),参数/返回值格式与 ctypes 版相同。""" + n = len(atom_masses) + NT = int(config["NT"]) + NSTEP = int(config.get("NSTEP", 1)) + warmup = int(config.get("warmup_steps", 0)) + dt = float(config["DT"]) + box_a = float(config.get("box_a", 300.0)) + method_str = str(config.get("method", "leapfrog")).lower().replace(" ", "_") + method_id = _METHOD_ID.get(method_str, 3) + + G = config.get("G", [0.0, 0.0, 0.0]) + B = config.get("B", [0.0, 0.0, 0.0]) + if hasattr(G, "tolist"): G = G.tolist() + if hasattr(B, "tolist"): B = B.tolist() + + gravity_field = int(config.get("gravity_field", 0)) + elastic_force = int(config.get("elastic_force", 1)) + damping_force = int(config.get("damping_force", 0)) + gravity_strength = float(config.get("gravity_strength", 1.0)) + + record_steps = NT - warmup + n_frames = max(1, record_steps // NSTEP) + + nd = len(driver_data) if driver_data else 0 + if nd > 0: + drv_idx = np.array([d["local_idx"] for d in driver_data], dtype=np.int64) + drv_amp = np.array([d["amp"] for d in driver_data], dtype=np.float64) + drv_freq = np.array([d["freq"] for d in driver_data], dtype=np.float64) + drv_phi = np.array([d["phi"] for d in driver_data], dtype=np.float64) + drv_eq = np.array([d["eq_pos"] for d in driver_data], dtype=np.float64) + drv_nc = np.array([d["n_cycles"] for d in driver_data], dtype=np.float64) + drv_hp = np.array([d["has_period"]for d in driver_data], dtype=np.int32) + else: + drv_idx = drv_amp = drv_freq = drv_phi = drv_eq = drv_nc = drv_hp = \ + np.zeros(0, dtype=np.int64) + + out_x, out_y, out_z, out_vx, out_vy, out_vz = lib.run_dynamics( + n_atoms=n, + pos_init=atom_positions, + vel_init=atom_velocities, + masses=atom_masses, + fixed=atom_fixed, + n_bonds=len(bond_pairs), + bond_pairs=bond_pairs, + bond_k=bond_stiffness, + bond_r0=bond_rest_lengths, + box_a=box_a, + dt=dt, + NT=NT, + NSTEP=NSTEP, + warmup_steps=warmup, + method_id=method_id, + Gx=float(G[0]), Gy=float(G[1]), Gz=float(G[2]), + Bx=float(B[0]), By=float(B[1]), Bz=float(B[2]), + gravity_field=gravity_field, + elastic_force=elastic_force, + damping_force=damping_force, + gravity_strength=gravity_strength, + n_drivers=nd, + drv_idx=drv_idx, + drv_amp=drv_amp, + drv_freq=drv_freq, + drv_phi=drv_phi, + drv_eq=drv_eq, + drv_ncycles=drv_nc, + drv_has_period=drv_hp, + n_frames=n_frames, + progress_cb=progress_cb, + ) + + shape = (n_frames, n) + t_arr = np.arange(n_frames) * NSTEP * dt + warmup * dt + return { + "x": out_x.reshape(shape), "y": out_y.reshape(shape), + "z": out_z.reshape(shape), "vx": out_vx.reshape(shape), + "vy": out_vy.reshape(shape), "vz": out_vz.reshape(shape), + "t": t_arr, + } + + +def run_dynamics_dll( + lib, + config: dict, + atom_positions: np.ndarray, # (n_atoms, 3) + atom_velocities: np.ndarray, # (n_atoms, 3) + atom_masses: np.ndarray, # (n_atoms,) + atom_fixed: np.ndarray, # (n_atoms, 3) int, 1=固定 + bond_pairs: np.ndarray, # (n_bonds, 2) int 0-based 局部索引 + bond_stiffness: np.ndarray, # (n_bonds,) + bond_rest_lengths: np.ndarray,# (n_bonds,) + driver_data: list, # 驱动原子列表(见下文) + atom_ids: np.ndarray, # (n_atoms,) 全局 atom id(用于驱动原子查找) + progress_cb=None, +) -> dict: + """调用 DLL 的 run_dynamics(),返回抽帧后的轨迹数组。 + + driver_data 格式(每个元素对应一个驱动原子): + { + "atom_id": int, # 全局 atom id + "local_idx": int, # 在 atom_ids 数组中的位置(0-based) + "amp": [ax, ay, az], + "freq": [fx, fy, fz], + "phi": [px, py, pz], + "eq_pos": [ex, ey, ez], + "n_cycles": float, # 0=不限 + "has_period": int, # 0/1 + } + + 返回: + { + "x": np.ndarray (n_frames, n_atoms), + "y": ..., + "z": ..., + "vx": ..., "vy": ..., "vz": ..., + "t": np.ndarray (n_frames,), # 时间轴 + } + """ + # Python 引擎:直接调用模块函数,不走 ctypes + if _is_python_module(lib): + return _run_dynamics_python( + lib, config, atom_positions, atom_velocities, atom_masses, + atom_fixed, bond_pairs, bond_stiffness, bond_rest_lengths, + driver_data, atom_ids, progress_cb) + + n = len(atom_masses) + NT = int(config["NT"]) + NSTEP = int(config.get("NSTEP", 1)) + warmup = int(config.get("warmup_steps", 0)) + dt = float(config["DT"]) + box_a = float(config.get("box_a", 300.0)) + method_str = str(config.get("method", "leapfrog")).lower().replace(" ", "_") + method_id = _METHOD_ID.get(method_str, 3) + + G = config.get("G", [0.0, 0.0, 0.0]) + B = config.get("B", [0.0, 0.0, 0.0]) + if hasattr(G, "tolist"): G = G.tolist() + if hasattr(B, "tolist"): B = B.tolist() + + gravity_field = int(config.get("gravity_field", 0)) + elastic_force = int(config.get("elastic_force", 1)) + damping_force = int(config.get("damping_force", 0)) + gravity_strength = float(config.get("gravity_strength", 1.0)) + + # ── 计算帧数 ────────────────────────────────────────────── + record_steps = NT - warmup + n_frames = max(1, record_steps // NSTEP) + + # ── 驱动原子数据 ────────────────────────────────────────── + nd = len(driver_data) if driver_data else 0 + if nd > 0: + drv_idx_arr = np.array([d["local_idx"] for d in driver_data], dtype=np.int32) + drv_amp_arr = np.array([d["amp"] for d in driver_data], dtype=np.float64).ravel() + drv_freq_arr = np.array([d["freq"] for d in driver_data], dtype=np.float64).ravel() + drv_phi_arr = np.array([d["phi"] for d in driver_data], dtype=np.float64).ravel() + drv_eq_arr = np.array([d["eq_pos"] for d in driver_data], dtype=np.float64).ravel() + drv_nc_arr = np.array([d["n_cycles"] for d in driver_data], dtype=np.float64) + drv_hp_arr = np.array([d["has_period"] for d in driver_data], dtype=np.int32) + else: + drv_idx_arr = np.zeros(1, dtype=np.int32) + drv_amp_arr = np.zeros(3, dtype=np.float64) + drv_freq_arr = np.zeros(3, dtype=np.float64) + drv_phi_arr = np.zeros(3, dtype=np.float64) + drv_eq_arr = np.zeros(3, dtype=np.float64) + drv_nc_arr = np.zeros(1, dtype=np.float64) + drv_hp_arr = np.zeros(1, dtype=np.int32) + + # ── 输出缓冲区 ──────────────────────────────────────────── + out_x = np.zeros(n_frames * n, dtype=np.float64) + out_y = np.zeros(n_frames * n, dtype=np.float64) + out_z = np.zeros(n_frames * n, dtype=np.float64) + out_vx = np.zeros(n_frames * n, dtype=np.float64) + out_vy = np.zeros(n_frames * n, dtype=np.float64) + out_vz = np.zeros(n_frames * n, dtype=np.float64) + + # ── ctypes 指针(保留 arr 引用防止 GC) ────────────────── + p_pos, _pos = _c_dbl(atom_positions.ravel()) + p_vel, _vel = _c_dbl(atom_velocities.ravel()) + p_mass, _mass = _c_dbl(atom_masses) + p_fixed, _fixed = _c_int(atom_fixed.ravel()) + p_bp, _bp = _c_int(bond_pairs.ravel() if len(bond_pairs) else np.zeros(2, dtype=np.int32)) + p_bk, _bk = _c_dbl(bond_stiffness if len(bond_stiffness) else np.zeros(1)) + p_br0, _br0 = _c_dbl(bond_rest_lengths if len(bond_rest_lengths) else np.zeros(1)) + p_didx, _didx = _c_int(drv_idx_arr) + p_damp, _damp = _c_dbl(drv_amp_arr) + p_dfrq, _dfrq = _c_dbl(drv_freq_arr) + p_dphi, _dphi = _c_dbl(drv_phi_arr) + p_deq, _deq = _c_dbl(drv_eq_arr) + p_dnc, _dnc = _c_dbl(drv_nc_arr) + p_dhp, _dhp = _c_int(drv_hp_arr) + + p_ox = out_x.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + p_oy = out_y.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + p_oz = out_z.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + p_ovx = out_vx.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + p_ovy = out_vy.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + p_ovz = out_vz.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) + + # 进度回调 + cb_type = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int) + if progress_cb is not None: + cb = cb_type(progress_cb) + else: + cb = ctypes.cast(None, cb_type) + + ret = lib.run_dynamics( + n, + p_pos, p_vel, p_mass, p_fixed, + len(bond_pairs), p_bp, p_bk, p_br0, + box_a, dt, NT, NSTEP, warmup, method_id, + float(G[0]), float(G[1]), float(G[2]), + float(B[0]), float(B[1]), float(B[2]), + gravity_field, elastic_force, damping_force, gravity_strength, + nd, p_didx, p_damp, p_dfrq, p_dphi, p_deq, p_dnc, p_dhp, + n_frames, + p_ox, p_oy, p_oz, p_ovx, p_ovy, p_ovz, + cb, + ) + + if ret != 0: + raise RuntimeError(f"run_dynamics() returned error code {ret}") + + shape = (n_frames, n) + t_arr = np.arange(n_frames) * NSTEP * dt + warmup * dt + + return { + "x": out_x.reshape(shape), + "y": out_y.reshape(shape), + "z": out_z.reshape(shape), + "vx": out_vx.reshape(shape), + "vy": out_vy.reshape(shape), + "vz": out_vz.reshape(shape), + "t": t_arr, + } + + +def is_dll_available(engine: str = "c") -> bool: + """检查指定引擎是否可用(DLL 已编译或 Python 模块存在)。""" + if _ENGINE_DIR.get(engine, engine) == "python": + return os.path.exists(os.path.join(_HERE, "python", "dynamics_lib.py")) + return any(os.path.exists(p) for p in _dll_candidates(engine)) diff --git a/engines/fortran/Makefile b/engines/fortran/Makefile new file mode 100644 index 0000000..5c2b6a3 --- /dev/null +++ b/engines/fortran/Makefile @@ -0,0 +1,47 @@ +# engines/fortran/Makefile + +FC = gfortran +FFLAGS = -O3 -march=native -Wall -Wextra +SRCS = main.f90 +LIB_SRC = dynamics_lib.f90 + +UNAME_S := $(shell uname -s 2>/dev/null || echo Windows) + +ifeq ($(UNAME_S),Windows) + STATIC_FLAGS = -static-libgcc -static-libgfortran -static-libquadmath +else + STATIC_FLAGS = +endif + +TARGET = build/dynamics_f90.exe + +ifeq ($(UNAME_S),Linux) + DLL_TARGET = build/dynamics_f90.so + DLL_FLAGS = -shared -fPIC +else ifeq ($(UNAME_S),Darwin) + DLL_TARGET = build/dynamics_f90.dylib + DLL_FLAGS = -dynamiclib +else + DLL_TARGET = build/dynamics_f90.dll + DLL_FLAGS = -shared -fPIC +endif + +.PHONY: all dll clean + +all: $(TARGET) + +dll: $(DLL_TARGET) + +$(TARGET): $(SRCS) | build + $(FC) $(FFLAGS) $(STATIC_FLAGS) -o $@ $(SRCS) + @echo " === Fortran engine built: $@ ===" + +$(DLL_TARGET): $(LIB_SRC) | build + $(FC) $(FFLAGS) $(STATIC_FLAGS) $(DLL_FLAGS) -o $@ $(LIB_SRC) + @echo " === Fortran DLL built: $@ ===" + +build: + mkdir -p build + +clean: + rm -rf build *.o *.mod diff --git a/engines/fortran/_calib_cache.json b/engines/fortran/_calib_cache.json new file mode 100644 index 0000000..2c07712 --- /dev/null +++ b/engines/fortran/_calib_cache.json @@ -0,0 +1 @@ +{"n_atoms": 40, "nt": 200000, "step_time": 0.005991018545627594} \ No newline at end of file diff --git a/engines/fortran/dynamics_lib.f90 b/engines/fortran/dynamics_lib.f90 new file mode 100644 index 0000000..565f0c4 --- /dev/null +++ b/engines/fortran/dynamics_lib.f90 @@ -0,0 +1,483 @@ +! engines/fortran/dynamics_lib.f90 +! --------------------------------- +! 纯计算 DLL(Fortran 版):无文件 I/O,由 Python ctypes 调用。 +! 算法与 main.f90 / compute.py 完全一致。 +! 使用 iso_c_binding 导出 C 兼容接口。 +! +! 编译(Windows): +! gfortran -O3 -march=native -shared -fPIC -o build/dynamics_f90.dll dynamics_lib.f90 +! 编译(Linux): +! gfortran -O3 -march=native -shared -fPIC -o build/dynamics_f90.so dynamics_lib.f90 +! 编译(macOS): +! gfortran -O3 -march=native -dynamiclib -o build/dynamics_f90.dylib dynamics_lib.f90 + +module dynamics_dll + use iso_c_binding, only: c_int, c_double, c_funptr, c_f_procpointer, c_associated + implicit none + private + + real(c_double), parameter :: TWO_PI = 2.0d0 * 3.14159265358979323846d0 + + public :: run_dynamics + +contains + +! ── 保守加速度 ─────────────────────────────────────────────── +subroutine accel_conservative(n, x, y, z, m, Gx, Gy, Gz, & + gravity_field, elastic_force, & + n_bonds, bond_pairs, bond_k, bond_r0, & + ax, ay, az) + integer, intent(in) :: n, gravity_field, elastic_force, n_bonds + real(c_double), intent(in) :: x(n), y(n), z(n), m(n) + real(c_double), intent(in) :: Gx, Gy, Gz + integer, intent(in) :: bond_pairs(2, n_bonds) + real(c_double), intent(in) :: bond_k(n_bonds), bond_r0(n_bonds) + real(c_double), intent(out) :: ax(n), ay(n), az(n) + + integer :: b, ii, jj + real(c_double) :: dx, dy, dz, dist, fac, fx, fy, fz_b + + if (gravity_field /= 0) then + ax = Gx; ay = Gy; az = Gz + else + ax = 0.0d0; ay = 0.0d0; az = 0.0d0 + end if + + if (elastic_force == 0 .or. n_bonds == 0) return + + do b = 1, n_bonds + ii = bond_pairs(1, b) + 1 ! 0-based → 1-based + jj = bond_pairs(2, b) + 1 + dx = x(jj)-x(ii); dy = y(jj)-y(ii); dz = z(jj)-z(ii) + dist = sqrt(dx*dx + dy*dy + dz*dz) + if (dist < 1.0d-12) cycle + fac = bond_k(b) * (dist - bond_r0(b)) / dist + fx = fac*dx; fy = fac*dy; fz_b = fac*dz + ax(ii) = ax(ii) + fx/m(ii); ay(ii) = ay(ii) + fy/m(ii); az(ii) = az(ii) + fz_b/m(ii) + ax(jj) = ax(jj) - fx/m(jj); ay(jj) = ay(jj) - fy/m(jj); az(jj) = az(jj) - fz_b/m(jj) + end do +end subroutine + +! ── 完整加速度(含阻尼)────────────────────────────────────── +subroutine accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az) + integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds + real(c_double), intent(in) :: x(n), y(n), z(n), vx(n), vy(n), vz(n), m(n) + real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz + integer, intent(in) :: bond_pairs(2, n_bonds) + real(c_double), intent(in) :: bond_k(n_bonds), bond_r0(n_bonds) + real(c_double), intent(out) :: ax(n), ay(n), az(n) + + integer :: i + + call accel_conservative(n, x, y, z, m, Gx, Gy, Gz, & + gravity_field, elastic_force, & + n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az) + if (damping_force /= 0) then + do i = 1, n + ax(i) = ax(i) - Bx*vx(i)/m(i) + ay(i) = ay(i) - By*vy(i)/m(i) + az(i) = az(i) - Bz*vz(i)/m(i) + end do + end if +end subroutine + +! ── 边界 + 固定约束 ────────────────────────────────────────── +subroutine apply_bc(n, x, y, z, vx, vy, vz, fixed, pos_init, box_a) + integer, intent(in) :: n + real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n) + integer, intent(in) :: fixed(3, n) + real(c_double), intent(in) :: pos_init(3, n), box_a + + integer :: i + real(c_double) :: lo, hi + + lo = -box_a; hi = box_a + + ! 反弹 + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle + if (x(i)>hi) then; x(i)=hi; vx(i)=-abs(vx(i)); end if + if (x(i)hi) then; y(i)=hi; vy(i)=-abs(vy(i)); end if + if (y(i)hi) then; z(i)=hi; vz(i)=-abs(vz(i)); end if + if (z(i)hi) x(i)=lo; if (x(i)hi) y(i)=lo; if (y(i)hi) z(i)=lo; if (z(i) 0.0d0) + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle + if (has_damp) then + ax_ = Bx*dt/(2.0d0*m(i)); ay_ = By*dt/(2.0d0*m(i)); az_ = Bz*dt/(2.0d0*m(i)) + vx(i) = (vx(i)*(1.0d0-ax_) + ax(i)*dt)/(1.0d0+ax_) + vy(i) = (vy(i)*(1.0d0-ay_) + ay(i)*dt)/(1.0d0+ay_) + vz(i) = (vz(i)*(1.0d0-az_) + az(i)*dt)/(1.0d0+az_) + else + vx(i) = vx(i)+ax(i)*dt; vy(i) = vy(i)+ay(i)*dt; vz(i) = vz(i)+az(i)*dt + end if + x(i) = x(i)+vx(i)*dt; y(i) = y(i)+vy(i)*dt; z(i) = z(i)+vz(i)*dt + end do +end subroutine + +! ── 显式欧拉法 ─────────────────────────────────────────────── +subroutine euler_step(n, x, y, z, vx, vy, vz, m, fixed, & + Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, dt) + integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds + real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n) + real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds) + integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds) + real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt + + real(c_double) :: ax(n), ay(n), az(n) + integer :: i + + call accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az) + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle + x(i) = x(i)+vx(i)*dt; y(i) = y(i)+vy(i)*dt; z(i) = z(i)+vz(i)*dt + vx(i)= vx(i)+ax(i)*dt; vy(i)= vy(i)+ay(i)*dt; vz(i)= vz(i)+az(i)*dt + end do +end subroutine + +! ── 隐式欧拉法 ─────────────────────────────────────────────── +subroutine implicit_euler_step(n, x, y, z, vx, vy, vz, m, fixed, & + Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, dt) + integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds + real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n) + real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds) + integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds) + real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt + + real(c_double) :: vxn(n), vyn(n), vzn(n), ax(n), ay(n), az(n) + real(c_double) :: gamma_x, gamma_y, gamma_z + integer :: i + + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) then + vxn(i)=0.0d0; vyn(i)=0.0d0; vzn(i)=0.0d0; cycle + end if + gamma_x = Bx/m(i); gamma_y = By/m(i); gamma_z = Bz/m(i) + vxn(i) = (vx(i)+Gx*dt)/(1.0d0+gamma_x*dt) + vyn(i) = (vy(i)+Gy*dt)/(1.0d0+gamma_y*dt) + vzn(i) = (vz(i)+Gz*dt)/(1.0d0+gamma_z*dt) + end do + call accel_full(n, x, y, z, vxn, vyn, vzn, m, Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az) + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle + vx(i)=vx(i)+ax(i)*dt; vy(i)=vy(i)+ay(i)*dt; vz(i)=vz(i)+az(i)*dt + x(i) =x(i) +vx(i)*dt; y(i) =y(i) +vy(i)*dt; z(i) =z(i) +vz(i)*dt + end do +end subroutine + +! ── 中点法 ─────────────────────────────────────────────────── +subroutine midpoint_step(n, x, y, z, vx, vy, vz, m, fixed, & + Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, dt) + integer, intent(in) :: n, gravity_field, elastic_force, damping_force, n_bonds + real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n) + real(c_double), intent(in) :: m(n), bond_k(n_bonds), bond_r0(n_bonds) + integer, intent(in) :: fixed(3,n), bond_pairs(2,n_bonds) + real(c_double), intent(in) :: Gx, Gy, Gz, Bx, By, Bz, dt + + real(c_double) :: ax(n), ay(n), az(n) + real(c_double) :: xm(n), ym(n), zm(n), vxm(n), vym(n), vzm(n) + real(c_double) :: axm(n), aym(n), azm(n) + integer :: i + + call accel_full(n, x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, ax, ay, az) + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) then + xm(i)=x(i); ym(i)=y(i); zm(i)=z(i) + vxm(i)=0.0d0; vym(i)=0.0d0; vzm(i)=0.0d0; cycle + end if + xm(i) = x(i) +0.5d0*vx(i)*dt; ym(i) = y(i) +0.5d0*vy(i)*dt; zm(i) = z(i) +0.5d0*vz(i)*dt + vxm(i) = vx(i)+0.5d0*ax(i)*dt; vym(i) = vy(i)+0.5d0*ay(i)*dt; vzm(i) = vz(i)+0.5d0*az(i)*dt + x(i) = x(i) +vxm(i)*dt; y(i) = y(i) +vym(i)*dt; z(i) = z(i) +vzm(i)*dt + end do + call accel_full(n, xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, axm, aym, azm) + do i = 1, n + if (fixed(1,i)/=0 .and. fixed(2,i)/=0 .and. fixed(3,i)/=0) cycle + vx(i)=vx(i)+axm(i)*dt; vy(i)=vy(i)+aym(i)*dt; vz(i)=vz(i)+azm(i)*dt + end do +end subroutine + +! ── 驱动力施加 ─────────────────────────────────────────────── +subroutine apply_drive(n, x, y, z, vx, vy, vz, t, step, dt, & + nd, drv_idx, drv_amp, drv_freq, drv_phi, & + drv_eq, drv_ncycles, drv_has_period, freeze) + integer, intent(in) :: n, nd, step + real(c_double), intent(inout) :: x(n), y(n), z(n), vx(n), vy(n), vz(n) + real(c_double), intent(in) :: t, dt + integer, intent(in) :: drv_idx(nd), drv_has_period(nd) + real(c_double), intent(in) :: drv_amp(3,nd), drv_freq(3,nd) + real(c_double), intent(in) :: drv_phi(3,nd), drv_eq(3,nd) + real(c_double), intent(in) :: drv_ncycles(nd) + real(c_double), intent(inout) :: freeze(3,nd) + + integer :: d, idx, ps + real(c_double) :: fx, fy, fz, mf, px, py, pz + + do d = 1, nd + idx = drv_idx(d) + 1 ! 0-based → 1-based + fx = drv_freq(1,d); fy = drv_freq(2,d); fz = drv_freq(3,d) + + if (drv_has_period(d) /= 0) then + mf = max(abs(fx), max(abs(fy), abs(fz))) + ps = 0 + if (mf > 1.0d-12) ps = int(drv_ncycles(d)/mf/dt) + if (step > ps) then + x(idx)=freeze(1,d); y(idx)=freeze(2,d); z(idx)=freeze(3,d) + vx(idx)=0.0d0; vy(idx)=0.0d0; vz(idx)=0.0d0 + cycle + end if + px = drv_eq(1,d)+drv_amp(1,d)*cos(TWO_PI*fx*t+drv_phi(1,d)) + py = drv_eq(2,d)+drv_amp(2,d)*cos(TWO_PI*fy*t+drv_phi(2,d)) + pz = drv_eq(3,d)+drv_amp(3,d)*cos(TWO_PI*fz*t+drv_phi(3,d)) + if (step == ps) then + freeze(1,d)=px; freeze(2,d)=py; freeze(3,d)=pz + end if + end if + x(idx) = drv_eq(1,d)+drv_amp(1,d)*cos(TWO_PI*fx*t+drv_phi(1,d)) + y(idx) = drv_eq(2,d)+drv_amp(2,d)*cos(TWO_PI*fy*t+drv_phi(2,d)) + z(idx) = drv_eq(3,d)+drv_amp(3,d)*cos(TWO_PI*fz*t+drv_phi(3,d)) + vx(idx) = -drv_amp(1,d)*TWO_PI*fx*sin(TWO_PI*fx*t+drv_phi(1,d)) + vy(idx) = -drv_amp(2,d)*TWO_PI*fy*sin(TWO_PI*fy*t+drv_phi(2,d)) + vz(idx) = -drv_amp(3,d)*TWO_PI*fz*sin(TWO_PI*fz*t+drv_phi(3,d)) + end do +end subroutine + +! ══════════════════════════════════════════════════════════════ +! 导出函数:run_dynamics(C 兼容接口,bind(C)) +! 接口与 C/C++ DLL 完全相同(扁平 C-contiguous 数组)。 +! ══════════════════════════════════════════════════════════════ +integer(c_int) function run_dynamics( & + n_atoms, pos_init, vel_init, masses, fixed, & + n_bonds, bond_pairs, bond_k, bond_r0, & + box_a, dt, NT, NSTEP, warmup_steps, method_id, & + Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, gravity_strength, & + n_drivers, drv_idx, drv_amp, drv_freq, drv_phi, drv_eq, & + drv_ncycles, drv_has_period, & + n_frames, out_x, out_y, out_z, out_vx, out_vy, out_vz, & + progress_cb) & + bind(C, name="run_dynamics") + + integer(c_int), value, intent(in) :: n_atoms, n_bonds, NT, NSTEP + integer(c_int), value, intent(in) :: warmup_steps, method_id + integer(c_int), value, intent(in) :: gravity_field, elastic_force, damping_force + integer(c_int), value, intent(in) :: n_drivers, n_frames + real(c_double), value, intent(in) :: box_a, dt + real(c_double), value, intent(in) :: Gx, Gy, Gz, Bx, By, Bz + real(c_double), value, intent(in) :: gravity_strength + + ! 扁平数组:Python 传入 C-contiguous int32/float64 + ! Fortran 以列优先解释,维度反转:(3,n) 对应 C 的 n×3 + real(c_double), intent(in) :: pos_init(3, n_atoms) + real(c_double), intent(in) :: vel_init(3, n_atoms) + real(c_double), intent(in) :: masses(n_atoms) + integer(c_int), intent(in) :: fixed(3, n_atoms) + integer(c_int), intent(in) :: bond_pairs(2, n_bonds) + real(c_double), intent(in) :: bond_k(n_bonds), bond_r0(n_bonds) + integer(c_int), intent(in) :: drv_idx(n_drivers) + real(c_double), intent(in) :: drv_amp(3, n_drivers) + real(c_double), intent(in) :: drv_freq(3, n_drivers) + real(c_double), intent(in) :: drv_phi(3, n_drivers) + real(c_double), intent(in) :: drv_eq(3, n_drivers) + real(c_double), intent(in) :: drv_ncycles(n_drivers) + integer(c_int), intent(in) :: drv_has_period(n_drivers) + + real(c_double), intent(out) :: out_x(n_atoms, n_frames) + real(c_double), intent(out) :: out_y(n_atoms, n_frames) + real(c_double), intent(out) :: out_z(n_atoms, n_frames) + real(c_double), intent(out) :: out_vx(n_atoms, n_frames) + real(c_double), intent(out) :: out_vy(n_atoms, n_frames) + real(c_double), intent(out) :: out_vz(n_atoms, n_frames) + + type(c_funptr), value, intent(in) :: progress_cb + + ! 进度回调接口 + abstract interface + subroutine cb_iface(step, total) bind(C) + use iso_c_binding + integer(c_int), value :: step, total + end subroutine + end interface + procedure(cb_iface), pointer :: cb_ptr + + integer :: n, s, frame_idx, record_steps, prog_interval, nd + real(c_double) :: t, tw + real(c_double), allocatable :: x(:), y(:), z(:), vx(:), vy(:), vz(:) + real(c_double), allocatable :: ax0(:), ay0(:), az0(:) + real(c_double), allocatable :: freeze(:,:) + logical :: has_cb + + n = n_atoms + nd = n_drivers + + allocate(x(n), y(n), z(n), vx(n), vy(n), vz(n)) + do s = 1, n + x(s) = pos_init(1,s); y(s) = pos_init(2,s); z(s) = pos_init(3,s) + vx(s) = vel_init(1,s); vy(s) = vel_init(2,s); vz(s) = vel_init(3,s) + end do + + allocate(freeze(3, max(nd,1))) + freeze = 0.0d0 + + has_cb = c_associated(progress_cb) + if (has_cb) call c_f_procpointer(progress_cb, cb_ptr) + + ! ── 蛙跳法:初始化 v(-dt/2) ───────────────────────────── + if (method_id == 3) then + allocate(ax0(n), ay0(n), az0(n)) + call accel_conservative(n, x, y, z, masses, Gx, Gy, Gz, & + gravity_field, elastic_force, & + n_bonds, bond_pairs, bond_k, bond_r0, ax0, ay0, az0) + do s = 1, n + if (fixed(1,s)/=0 .and. fixed(2,s)/=0 .and. fixed(3,s)/=0) cycle + vx(s)=vx(s)-0.5d0*ax0(s)*dt + vy(s)=vy(s)-0.5d0*ay0(s)*dt + vz(s)=vz(s)-0.5d0*az0(s)*dt + end do + deallocate(ax0, ay0, az0) + end if + + ! ── 初始驱动 t=0 ───────────────────────────────────────── + if (nd > 0) call apply_drive(n, x, y, z, vx, vy, vz, 0.0d0, 0, dt, & + nd, drv_idx, drv_amp, drv_freq, drv_phi, & + drv_eq, drv_ncycles, drv_has_period, freeze) + + ! ── 预热 ───────────────────────────────────────────────── + do s = 0, warmup_steps-1 + tw = (s+1)*dt + if (nd>0) call apply_drive(n, x, y, z, vx, vy, vz, tw, s, dt, & + nd, drv_idx, drv_amp, drv_freq, drv_phi, & + drv_eq, drv_ncycles, drv_has_period, freeze) + call do_step(n, x, y, z, vx, vy, vz, masses, fixed, & + Gx, Gy, Gz, Bx, By, Bz, & + gravity_field, elastic_force, damping_force, & + n_bonds, bond_pairs, bond_k, bond_r0, dt, method_id, & + pos_init, box_a) + end do + + ! ── 记录循环 ───────────────────────────────────────────── + record_steps = NT - warmup_steps + prog_interval = max(1, record_steps/100) + frame_idx = 0 + + do s = 0, record_steps-1 + if (has_cb .and. mod(s, prog_interval)==0 .and. s>0) call cb_ptr(s, record_steps) + + t = (s+warmup_steps)*dt + if (nd>0) call apply_drive(n, x, y, z, vx, vy, vz, t, s, dt, & + nd, drv_idx, drv_amp, drv_freq, drv_phi, & + drv_eq, drv_ncycles, drv_has_period, freeze) + + if (mod(s, NSTEP)==0 .and. frame_idx 0) then + if (mod(s, prog_step) == 0) then write(*, '("[Fortran-engine] progress: ", i0, "/", i0)') s, record_steps + flush(6) + end if + ! 采帧:在每个 NSTEP 区间的起始时刻记录 + if (mod(s-1, max(1, NSTEP)) == 0 .and. frame_idx < n_frames) then + frame_idx = frame_idx + 1 + traj_x(frame_idx, :) = x; traj_y(frame_idx, :) = y; traj_z(frame_idx, :) = z + traj_vx(frame_idx, :) = vx; traj_vy(frame_idx, :) = vy; traj_vz(frame_idx, :) = vz end if if (driving_force /= 0 .and. n_drivers > 0) then tw = ((s-1 + warmup_steps) * 1.0d0) * DT @@ -178,8 +186,6 @@ program dynamics_f90 drv_eq_x, drv_eq_y, drv_eq_z, & drv_freeze_x, drv_freeze_y, drv_freeze_z) end if - traj_x(s, :) = x; traj_y(s, :) = y; traj_z(s, :) = z - traj_vx(s, :) = vx; traj_vy(s, :) = vy; traj_vz(s, :) = vz call apply_step(method, n, x, y, z, vx, vy, vz, masses, G, B, & n_bonds, bond_pairs, bond_stiffness, bond_rest_lengths, & fixed, box_a, DT, & @@ -188,13 +194,14 @@ program dynamics_f90 pos_0) end do - ! 输出轨迹 - write(*, '("[Fortran-engine] 正在写入轨迹数据…")') - call write_json(output_dir, traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, & - record_steps, n_atoms, atom_ids, masses, & - NT, DT, NSTEP, warmup_steps, method, G, B, & - n_bonds, bond_pairs, bond_stiffness, bond_rest_lengths, & - driving_force) + ! 输出 display.txt + write(*, '("[Fortran-engine] 正在写入 display.txt (", i0, " 帧)…")') n_frames + flush(6) + call write_display_txt(output_dir, n_frames, n_atoms, atom_ids, & + traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, & + NT, DT, NSTEP, warmup_steps, method, G, B, & + n_bonds, gravity_field, elastic_force, damping_force, & + driving_force, box_a, gravity_strength) call cpu_time(t1) elapsed = t1 - t0 @@ -985,179 +992,80 @@ subroutine apply_driving(n, x, y, z, vx, vy, vz, t, step, dt, & end subroutine apply_driving ! ======================================================================== -! JSON 输出 +! display.txt 输出(与 compute.py save_display_txt 格式一致) ! ======================================================================== -subroutine write_json(outdir, tx, ty, tz, tvx, tvy, tvz, & - nsteps, nat, aid, amass, & - NT, DT, NSTEP, warmup, method, G, B, & - nb, bp, bk, br, driving_force) +subroutine write_display_txt(outdir, n_frames, nat, aid, & + tx, ty, tz, tvx, tvy, tvz, & + NT, DT, NSTEP, warmup, method, G, B, & + nb, gravity_field, elastic_force, damping_force, & + driving_force, box_a, gravity_strength) character(len=*), intent(in) :: outdir, method - integer, intent(in) :: nsteps, nat, NT, NSTEP, warmup, nb, bp(nb, 2), aid(nat), driving_force - double precision, intent(in) :: tx(nsteps, nat), ty(nsteps, nat), tz(nsteps, nat) - double precision, intent(in) :: tvx(nsteps, nat), tvy(nsteps, nat), tvz(nsteps, nat) - double precision, intent(in) :: DT, G(3), B(3), bk(nb), br(nb), amass(nat) + integer, intent(in) :: n_frames, nat, NT, NSTEP, warmup, nb + integer, intent(in) :: gravity_field, elastic_force, damping_force, driving_force + integer, intent(in) :: aid(nat) + double precision, intent(in) :: tx(n_frames, nat), ty(n_frames, nat), tz(n_frames, nat) + double precision, intent(in) :: tvx(n_frames, nat), tvy(n_frames, nat), tvz(n_frames, nat) + double precision, intent(in) :: DT, G(3), B(3), box_a, gravity_strength character(len=512) :: path, buf - integer :: u, s, i, ib, ios + integer :: u, f, a, ios + integer :: dynamic_steps + double precision :: T_total - path = trim(outdir) // '/trajectory.txt' + dynamic_steps = NT - warmup + T_total = NT * DT + + path = trim(outdir) // '/display.txt' open(newunit=u, file=trim(path), status='replace', action='write', iostat=ios) if (ios /= 0) then write(*, '("[Fortran-engine] 错误: 无法写入 ", a)') trim(path) - stop + return end if - write(u, '(a)') '{' - - ! traj_x - write(u, '(a)') ' "traj_x": [' - do s = 1, nsteps - call json_arr(u, tx(s, :), nat, s < nsteps, ' ') - end do - write(u, '(a)') ' ],' - - ! traj_y - write(u, '(a)') ' "traj_y": [' - do s = 1, nsteps - call json_arr(u, ty(s, :), nat, s < nsteps, ' ') - end do - write(u, '(a)') ' ],' - - ! traj_z - write(u, '(a)') ' "traj_z": [' - do s = 1, nsteps - call json_arr(u, tz(s, :), nat, s < nsteps, ' ') - end do - write(u, '(a)') ' ],' - - ! traj_vx - write(u, '(a)') ' "traj_vx": [' - do s = 1, nsteps - call json_arr(u, tvx(s, :), nat, s < nsteps, ' ') - end do - write(u, '(a)') ' ],' - - ! traj_vy - write(u, '(a)') ' "traj_vy": [' - do s = 1, nsteps - call json_arr(u, tvy(s, :), nat, s < nsteps, ' ') - end do - write(u, '(a)') ' ],' - - ! traj_vz - write(u, '(a)') ' "traj_vz": [' - do s = 1, nsteps - call json_arr(u, tvz(s, :), nat, s < nsteps, ' ') - end do - write(u, '(a)') ' ],' - - ! 标量参数 - write(buf, '(a, i0, a)') ' "NT": ', NT, ',' + ! ── header ──────────────────────────────────────────────────────────── + write(u, '("number of frames: ", i0)') n_frames + write(u, '("number of particles: ", i0)') nat + write(u, '("DT: ", g0)') DT + write(u, '("NSTEP: ", i0)') NSTEP + write(u, '("method: ", a)') trim(method) + write(u, '("NT: ", i0)') NT + write(u, '("warmup_steps: ", i0)') warmup + write(u, '("dynamic_steps: ", i0)') dynamic_steps + write(u, '("T_total: ", g0)') T_total + write(u, '("box_a: ", g0)') box_a + write(u, '("gravity_field: ", i0)') gravity_field + write(u, '("elastic_force: ", i0)') elastic_force + write(u, '("damping_force: ", i0)') damping_force + write(u, '("driving_force: ", i0)') driving_force + write(u, '("gravity_strength: ", g0)') gravity_strength + write(buf, '("G: [", g0, ", ", g0, ", ", g0, "]")') G(1), G(2), G(3) write(u, '(a)') trim(buf) - write(buf, '(a, g0, a)') ' "DT": ', DT, ',' - write(u, '(a)') trim(buf) - write(buf, '(a, i0, a)') ' "NSTEP": ', NSTEP, ',' - write(u, '(a)') trim(buf) - write(buf, '(a, a, a)') ' "method": "', trim(method), '",' - write(u, '(a)') trim(buf) - write(buf, '(a, i0, a)') ' "warmup_steps": ', warmup, ',' + write(buf, '("B: [", g0, ", ", g0, ", ", g0, "]")') B(1), B(2), B(3) write(u, '(a)') trim(buf) + write(u, '("number_of_frames: ", i0)') n_frames + write(u, '("number_of_particles: ", i0)') nat + write(buf, '("X_MIN: ", g0)') -box_a; write(u, '(a)') trim(buf) + write(buf, '("X_MAX: ", g0)') box_a; write(u, '(a)') trim(buf) + write(buf, '("Y_MIN: ", g0)') -box_a; write(u, '(a)') trim(buf) + write(buf, '("Y_MAX: ", g0)') box_a; write(u, '(a)') trim(buf) + write(buf, '("Z_MIN: ", g0)') -box_a; write(u, '(a)') trim(buf) + write(buf, '("Z_MAX: ", g0)') box_a; write(u, '(a)') trim(buf) - write(buf, '(a, g0, a, g0, a, g0, a)') & - ' "G": [', G(1), ', ', G(2), ', ', G(3), '],' - write(u, '(a)') trim(buf) - write(buf, '(a, g0, a, g0, a, g0, a)') & - ' "B": [', B(1), ', ', B(2), ', ', B(3), '],' - write(u, '(a)') trim(buf) - - ! 原子信息 - write(u, '(a)', advance='no') ' "atom_ids": [' - do i = 1, nat - if (i > 1) write(u, '(a)', advance='no') ',' - write(u, '(i0)', advance='no') aid(i) + ! ── frame data ──────────────────────────────────────────────────────── + do f = 1, n_frames + write(u, '()') ! 空行 + write(u, '("frame: ", i0)') f + write(u, '("n x y z vx vy vz")') + do a = 1, nat + write(u, '(i0, 6(f13.6))') aid(a), & + tx(f,a), ty(f,a), tz(f,a), tvx(f,a), tvy(f,a), tvz(f,a) + end do end do - write(u, '(a)') '],' - write(u, '(a)', advance='no') ' "atom_masses": [' - do i = 1, nat - if (i > 1) write(u, '(a)', advance='no') ',' - write(u, '(g0)', advance='no') amass(i) - end do - write(u, '(a)') '],' - - ! 成键 - if (nb > 0) then - call write_int2_arr(u, 'bond_pairs', bp, nb, .true.) - call write_dbl_arr(u, 'bond_stiffness', bk, nb, .true.) - call write_dbl_arr(u, 'bond_rest_lengths', br, nb, .true.) - else - write(u, '(a)') ' "bond_pairs": [],' - write(u, '(a)') ' "bond_stiffness": [],' - write(u, '(a)') ' "bond_rest_lengths": [],' - end if - - write(buf, '(a, i0)') ' "driving_force": ', driving_force - write(u, '(a)') trim(buf) - - write(u, '(a)') '}' close(u) -end subroutine write_json - -! 写出单行 JSON 数组 [v1, v2, ...] -subroutine json_arr(u, vals, n, has_next, indent) - integer, intent(in) :: u, n - double precision, intent(in) :: vals(n) - logical, intent(in) :: has_next - character(len=*), intent(in) :: indent - integer :: i - write(u, '(a)', advance='no') indent // '[' - do i = 1, n - if (i > 1) write(u, '(a)', advance='no') ',' - write(u, '(g0.8)', advance='no') vals(i) - end do - if (has_next) then - write(u, '(a)') '],' - else - write(u, '(a)') ']' - end if -end subroutine json_arr - -subroutine write_int2_arr(u, name, arr, n, has_next) - integer, intent(in) :: u, n, arr(n, 2) - character(len=*), intent(in) :: name - logical, intent(in) :: has_next - character(len=65536) :: buf - integer :: i, pos - write(u, '(a)', advance='no') ' "' // trim(name) // '": [' - do i = 1, n - if (i > 1) write(u, '(a)', advance='no') ',' - write(buf, '(a, i0, a, i0, a)') '[', arr(i, 1), ',', arr(i, 2), ']' - write(u, '(a)', advance='no') trim(buf) - end do - if (has_next) then - write(u, '(a)') '],' - else - write(u, '(a)') ']' - end if -end subroutine write_int2_arr - -subroutine write_dbl_arr(u, name, arr, n, has_next) - integer, intent(in) :: u, n - double precision, intent(in) :: arr(n) - character(len=*), intent(in) :: name - logical, intent(in) :: has_next - integer :: i - write(u, '(a)', advance='no') ' "' // trim(name) // '": [' - do i = 1, n - if (i > 1) write(u, '(a)', advance='no') ',' - write(u, '(g0.8)', advance='no') arr(i) - end do - if (has_next) then - write(u, '(a)') '],' - else - write(u, '(a)') ']' - end if -end subroutine write_dbl_arr + write(*, '("[Fortran-engine] display.txt 已保存: ", a)') trim(path) + flush(6) +end subroutine write_display_txt end program dynamics_f90 diff --git a/engines/python/__init__.py b/engines/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/engines/python/dynamics_lib.py b/engines/python/dynamics_lib.py new file mode 100644 index 0000000..5aa679b --- /dev/null +++ b/engines/python/dynamics_lib.py @@ -0,0 +1,404 @@ +""" +engines/python/dynamics_lib.py +------------------------------- +纯 NumPy 计算引擎:无文件 I/O,所有数据以 NumPy 数组传入, +结果作为 NumPy 数组返回。 + +接口与 C/C++/Fortran DLL 的 run_dynamics() 完全一致, +算法与 compute.py 的 run_simulation() 保持一致。 + +用法(由 engine_dll.py 内部调用): + from engines.python.dynamics_lib import run_dynamics + out_x, out_y, out_z, out_vx, out_vy, out_vz = run_dynamics(...) +""" + +import numpy as np + +TWO_PI = 2.0 * np.pi + +# ── method_id 映射 ────────────────────────────────────────── +# 0=euler 1=implicit_euler 2=midpoint 3=leapfrog + + +# ── 保守加速度(弹簧键 + 均匀重力场,不含阻尼)────────────── +def _accel_conservative(x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + bond_pairs, bond_k, bond_r0): + ax = np.full_like(x, Gx) if gravity_field else np.zeros_like(x) + ay = np.full_like(y, Gy) if gravity_field else np.zeros_like(y) + az = np.full_like(z, Gz) if gravity_field else np.zeros_like(z) + + if elastic_force and len(bond_pairs) > 0: + i1 = bond_pairs[:, 0] + i2 = bond_pairs[:, 1] + dx = x[i2] - x[i1] + dy = y[i2] - y[i1] + dz = z[i2] - z[i1] + dist = np.sqrt(dx*dx + dy*dy + dz*dz) + valid = dist > 1e-12 + fac = np.where(valid, bond_k * (dist - bond_r0) / dist, 0.0) + fx = fac * dx + fy = fac * dy + fz_b = fac * dz + np.add.at(ax, i1, fx / m[i1]); np.add.at(ax, i2, -fx / m[i2]) + np.add.at(ay, i1, fy / m[i1]); np.add.at(ay, i2, -fy / m[i2]) + np.add.at(az, i1, fz_b / m[i1]); np.add.at(az, i2, -fz_b / m[i2]) + + return ax, ay, az + + +# ── 完整加速度(含阻尼)────────────────────────────────────── +def _accel_full(x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0): + ax, ay, az = _accel_conservative(x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + bond_pairs, bond_k, bond_r0) + if damping_force: + ax -= Bx * vx / m + ay -= By * vy / m + az -= Bz * vz / m + return ax, ay, az + + +# ── 蛙跳法(半隐式阻尼,与 compute.py leapfrog_staggered_step 一致)─ +def _leapfrog_step(x, y, z, vx, vy, vz, fixed, m, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt): + ax, ay, az = _accel_conservative(x, y, z, m, Gx, Gy, Gz, + gravity_field, elastic_force, + bond_pairs, bond_k, bond_r0) + has_damp = damping_force and (Bx != 0.0 or By != 0.0 or Bz != 0.0) + if has_damp: + alpha_x = Bx * dt / (2.0 * m) + alpha_y = By * dt / (2.0 * m) + alpha_z = Bz * dt / (2.0 * m) + vx_new = (vx * (1.0 - alpha_x) + ax * dt) / (1.0 + alpha_x) + vy_new = (vy * (1.0 - alpha_y) + ay * dt) / (1.0 + alpha_y) + vz_new = (vz * (1.0 - alpha_z) + az * dt) / (1.0 + alpha_z) + else: + vx_new = vx + ax * dt + vy_new = vy + ay * dt + vz_new = vz + az * dt + # 全固定原子保持不变 + all_fixed = np.all(fixed, axis=1) + vx_new = np.where(all_fixed, vx, vx_new) + vy_new = np.where(all_fixed, vy, vy_new) + vz_new = np.where(all_fixed, vz, vz_new) + x_new = x + vx_new * dt + y_new = y + vy_new * dt + z_new = z + vz_new * dt + return x_new, y_new, z_new, vx_new, vy_new, vz_new + + +# ── 显式欧拉法 ─────────────────────────────────────────────── +def _euler_step(x, y, z, vx, vy, vz, fixed, m, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt): + ax, ay, az = _accel_full(x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0) + all_fixed = np.all(fixed, axis=1) + mask = ~all_fixed + x_new = np.where(mask, x + vx * dt, x) + y_new = np.where(mask, y + vy * dt, y) + z_new = np.where(mask, z + vz * dt, z) + vx_new = np.where(mask, vx + ax * dt, vx) + vy_new = np.where(mask, vy + ay * dt, vy) + vz_new = np.where(mask, vz + az * dt, vz) + return x_new, y_new, z_new, vx_new, vy_new, vz_new + + +# ── 隐式欧拉法(与 compute.py Implicit_Euler_Method 一致)────── +def _implicit_euler_step(x, y, z, vx, vy, vz, fixed, m, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt): + gamma_x = Bx / m + gamma_y = By / m + gamma_z = Bz / m + vx_next = (vx + Gx * dt) / (1.0 + gamma_x * dt) + vy_next = (vy + Gy * dt) / (1.0 + gamma_y * dt) + vz_next = (vz + Gz * dt) / (1.0 + gamma_z * dt) + ax, ay, az = _accel_full(x, y, z, vx_next, vy_next, vz_next, m, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0) + all_fixed = np.all(fixed, axis=1) + mask = ~all_fixed + vx_new = np.where(mask, vx + ax * dt, vx) + vy_new = np.where(mask, vy + ay * dt, vy) + vz_new = np.where(mask, vz + az * dt, vz) + x_new = np.where(mask, x + vx_new * dt, x) + y_new = np.where(mask, y + vy_new * dt, y) + z_new = np.where(mask, z + vz_new * dt, z) + return x_new, y_new, z_new, vx_new, vy_new, vz_new + + +# ── 中点法(与 compute.py Midpoint_Method 一致)──────────────── +def _midpoint_step(x, y, z, vx, vy, vz, fixed, m, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt): + ax, ay, az = _accel_full(x, y, z, vx, vy, vz, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0) + all_fixed = np.all(fixed, axis=1) + mask = ~all_fixed + xm = np.where(mask, x + 0.5*vx*dt, x) + ym = np.where(mask, y + 0.5*vy*dt, y) + zm = np.where(mask, z + 0.5*vz*dt, z) + vxm = np.where(mask, vx + 0.5*ax*dt, 0.0) + vym = np.where(mask, vy + 0.5*ay*dt, 0.0) + vzm = np.where(mask, vz + 0.5*az*dt, 0.0) + x_new = np.where(mask, x + vxm * dt, x) + y_new = np.where(mask, y + vym * dt, y) + z_new = np.where(mask, z + vzm * dt, z) + axm, aym, azm = _accel_full(xm, ym, zm, vxm, vym, vzm, m, Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0) + vx_new = np.where(mask, vx + axm * dt, vx) + vy_new = np.where(mask, vy + aym * dt, vy) + vz_new = np.where(mask, vz + azm * dt, vz) + return x_new, y_new, z_new, vx_new, vy_new, vz_new + + +# ── 边界:反弹 + 回绕 + 逐自由度固定约束 ─────────────────────── +def _apply_bc(x, y, z, vx, vy, vz, fixed, pos_init, box_a): + lo, hi = -box_a, box_a + + # 反弹(全固定原子跳过) + all_fixed = np.all(fixed, axis=1) + do_bc = ~all_fixed + over_x = do_bc & (x > hi); under_x = do_bc & (x < lo) + over_y = do_bc & (y > hi); under_y = do_bc & (y < lo) + over_z = do_bc & (z > hi); under_z = do_bc & (z < lo) + x = np.where(over_x, hi, np.where(under_x, lo, x)) + y = np.where(over_y, hi, np.where(under_y, lo, y)) + z = np.where(over_z, hi, np.where(under_z, lo, z)) + vx = np.where(over_x | under_x, -np.abs(vx)*np.sign(np.where(over_x, 1, -1)), vx) + vy = np.where(over_y | under_y, -np.abs(vy)*np.sign(np.where(over_y, 1, -1)), vy) + vz = np.where(over_z | under_z, -np.abs(vz)*np.sign(np.where(over_z, 1, -1)), vz) + # 反弹速度简化:越界则取反绝对值(与 C 版 _limit1 一致) + vx = np.where(over_x, -np.abs(vx), np.where(under_x, np.abs(vx), vx)) + vy = np.where(over_y, -np.abs(vy), np.where(under_y, np.abs(vy), vy)) + vz = np.where(over_z, -np.abs(vz), np.where(under_z, np.abs(vz), vz)) + + # 回绕 + x = np.where(x > hi, lo, np.where(x < lo, hi, x)) + y = np.where(y > hi, lo, np.where(y < lo, hi, y)) + z = np.where(z > hi, lo, np.where(z < lo, hi, z)) + + # 逐自由度固定约束 + fx = fixed[:, 0].astype(bool) + fy = fixed[:, 1].astype(bool) + fz = fixed[:, 2].astype(bool) + x = np.where(fx, pos_init[:, 0], x); vx = np.where(fx, 0.0, vx) + y = np.where(fy, pos_init[:, 1], y); vy = np.where(fy, 0.0, vy) + z = np.where(fz, pos_init[:, 2], z); vz = np.where(fz, 0.0, vz) + return x, y, z, vx, vy, vz + + +# ── 驱动力(与 compute.py apply_driving_force 逻辑一致)───────── +def _apply_driving(x, y, z, vx, vy, vz, t, step, dt, + drv_idx, drv_amp, drv_freq, drv_phi, drv_eq, + drv_ncycles, drv_has_period, freeze): + """freeze: (n_drivers, 3) mutable array for frozen positions.""" + nd = len(drv_idx) + for d in range(nd): + idx = drv_idx[d] + fx_ = drv_freq[d, 0]; fy_ = drv_freq[d, 1]; fz_ = drv_freq[d, 2] + + if drv_has_period[d]: + mf = max(abs(fx_), abs(fy_), abs(fz_)) + ps = int(drv_ncycles[d] / mf / dt) if mf > 1e-12 else 0 + if step > ps: + x[idx] = freeze[d, 0]; y[idx] = freeze[d, 1]; z[idx] = freeze[d, 2] + vx[idx] = vy[idx] = vz[idx] = 0.0 + continue + px = drv_eq[d,0] + drv_amp[d,0]*np.cos(TWO_PI*fx_*t + drv_phi[d,0]) + py = drv_eq[d,1] + drv_amp[d,1]*np.cos(TWO_PI*fy_*t + drv_phi[d,1]) + pz = drv_eq[d,2] + drv_amp[d,2]*np.cos(TWO_PI*fz_*t + drv_phi[d,2]) + if step == ps: + freeze[d, 0] = px; freeze[d, 1] = py; freeze[d, 2] = pz + + x[idx] = drv_eq[d,0] + drv_amp[d,0]*np.cos(TWO_PI*fx_*t + drv_phi[d,0]) + y[idx] = drv_eq[d,1] + drv_amp[d,1]*np.cos(TWO_PI*fy_*t + drv_phi[d,1]) + z[idx] = drv_eq[d,2] + drv_amp[d,2]*np.cos(TWO_PI*fz_*t + drv_phi[d,2]) + vx[idx] = -drv_amp[d,0]*TWO_PI*fx_*np.sin(TWO_PI*fx_*t + drv_phi[d,0]) + vy[idx] = -drv_amp[d,1]*TWO_PI*fy_*np.sin(TWO_PI*fy_*t + drv_phi[d,1]) + vz[idx] = -drv_amp[d,2]*TWO_PI*fz_*np.sin(TWO_PI*fz_*t + drv_phi[d,2]) + + +def _do_step(x, y, z, vx, vy, vz, fixed, masses, method_id, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt, pos_init, box_a): + if method_id == 0: + x, y, z, vx, vy, vz = _euler_step( + x, y, z, vx, vy, vz, fixed, masses, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt) + elif method_id == 1: + x, y, z, vx, vy, vz = _implicit_euler_step( + x, y, z, vx, vy, vz, fixed, masses, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt) + elif method_id == 2: + x, y, z, vx, vy, vz = _midpoint_step( + x, y, z, vx, vy, vz, fixed, masses, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt) + else: + x, y, z, vx, vy, vz = _leapfrog_step( + x, y, z, vx, vy, vz, fixed, masses, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt) + x, y, z, vx, vy, vz = _apply_bc(x, y, z, vx, vy, vz, fixed, pos_init, box_a) + return x, y, z, vx, vy, vz + + +# ══════════════════════════════════════════════════════════════ +# 主函数:run_dynamics +# 接口与 C/C++/Fortran DLL 的 run_dynamics() 对应, +# 参数格式:numpy 数组(替代 ctypes 指针)。 +# +# method_id: 0=euler 1=implicit_euler 2=midpoint 3=leapfrog +# drv_amp/freq/phi/eq: (n_drivers, 3) float64 +# drv_ncycles: (n_drivers,) float64 0=不限 +# drv_has_period: (n_drivers,) int +# +# 返回:(out_x, out_y, out_z, out_vx, out_vy, out_vz) +# 各 shape=(n_frames, n_atoms) +# ══════════════════════════════════════════════════════════════ +def run_dynamics( + n_atoms, pos_init, vel_init, masses, fixed, + n_bonds, bond_pairs, bond_k, bond_r0, + box_a, dt, + NT, NSTEP, warmup_steps, method_id, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, gravity_strength, + n_drivers, drv_idx, drv_amp, drv_freq, drv_phi, drv_eq, + drv_ncycles, drv_has_period, + n_frames, + progress_cb=None, +): + """运行动力学模拟,返回抽帧轨迹数组。 + + Args: + pos_init: (n_atoms, 3) float64 + vel_init: (n_atoms, 3) float64 + masses: (n_atoms,) float64 + fixed: (n_atoms, 3) int — 1=固定 + bond_pairs: (n_bonds, 2) int — 0-based 局部索引 + bond_k: (n_bonds,) float64 + bond_r0: (n_bonds,) float64 + drv_idx: (n_drivers,) int — 0-based + drv_amp/freq/phi/eq: (n_drivers, 3) float64 + drv_ncycles: (n_drivers,) float64 + drv_has_period: (n_drivers,) int + n_frames: 预分配的输出帧数 + + Returns: + out_x, out_y, out_z, out_vx, out_vy, out_vz — 各 (n_frames, n_atoms) + """ + pos_init = np.asarray(pos_init, dtype=np.float64) + vel_init = np.asarray(vel_init, dtype=np.float64) + masses = np.asarray(masses, dtype=np.float64) + fixed = np.asarray(fixed, dtype=np.int32) + bond_pairs = np.asarray(bond_pairs, dtype=np.int64).reshape(-1, 2) if n_bonds else np.zeros((0,2), dtype=np.int64) + bond_k = np.asarray(bond_k, dtype=np.float64) if n_bonds else np.zeros(0) + bond_r0 = np.asarray(bond_r0, dtype=np.float64) if n_bonds else np.zeros(0) + + n = n_atoms + x = pos_init[:, 0].copy() + y = pos_init[:, 1].copy() + z = pos_init[:, 2].copy() + vx = vel_init[:, 0].copy() + vy = vel_init[:, 1].copy() + vz = vel_init[:, 2].copy() + + # 驱动力数据(保证正确形状) + nd = n_drivers + if nd > 0: + drv_idx = np.asarray(drv_idx, dtype=np.int64) + drv_amp = np.asarray(drv_amp, dtype=np.float64).reshape(nd, 3) + drv_freq = np.asarray(drv_freq, dtype=np.float64).reshape(nd, 3) + drv_phi = np.asarray(drv_phi, dtype=np.float64).reshape(nd, 3) + drv_eq = np.asarray(drv_eq, dtype=np.float64).reshape(nd, 3) + drv_nc = np.asarray(drv_ncycles, dtype=np.float64) + drv_hp = np.asarray(drv_has_period, dtype=np.int32) + freeze = np.zeros((nd, 3), dtype=np.float64) + else: + drv_idx = drv_amp = drv_freq = drv_phi = drv_eq = drv_nc = drv_hp = freeze = None + + def _drive(t_, step_): + if nd > 0: + _apply_driving(x, y, z, vx, vy, vz, t_, step_, dt, + drv_idx, drv_amp, drv_freq, drv_phi, drv_eq, + drv_nc, drv_hp, freeze) + + # ── 蛙跳法:初始化 v(-dt/2) ───────────────────────────── + if method_id == 3: + ax0, ay0, az0 = _accel_conservative(x, y, z, masses, Gx, Gy, Gz, + gravity_field, elastic_force, + bond_pairs, bond_k, bond_r0) + all_fixed = np.all(fixed, axis=1) + vx = np.where(all_fixed, vx, vx - 0.5 * ax0 * dt) + vy = np.where(all_fixed, vy, vy - 0.5 * ay0 * dt) + vz = np.where(all_fixed, vz, vz - 0.5 * az0 * dt) + + # ── 初始驱动 t=0 ───────────────────────────────────────── + _drive(0.0, 0) + + # ── 预热 ───────────────────────────────────────────────── + for s in range(warmup_steps): + tw = (s + 1) * dt + _drive(tw, s) + x, y, z, vx, vy, vz = _do_step( + x, y, z, vx, vy, vz, fixed, masses, method_id, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt, pos_init, box_a) + + # ── 记录循环 ───────────────────────────────────────────── + record_steps = NT - warmup_steps + prog_interval = max(1, record_steps // 100) + + out_x = np.zeros((n_frames, n), dtype=np.float64) + out_y = np.zeros((n_frames, n), dtype=np.float64) + out_z = np.zeros((n_frames, n), dtype=np.float64) + out_vx = np.zeros((n_frames, n), dtype=np.float64) + out_vy = np.zeros((n_frames, n), dtype=np.float64) + out_vz = np.zeros((n_frames, n), dtype=np.float64) + frame_idx = 0 + + for s in range(record_steps): + if progress_cb is not None and s % prog_interval == 0 and s > 0: + progress_cb(s, record_steps) + + t = (s + warmup_steps) * dt + _drive(t, s) + + if s % NSTEP == 0 and frame_idx < n_frames: + out_x[frame_idx] = x + out_y[frame_idx] = y + out_z[frame_idx] = z + out_vx[frame_idx] = vx + out_vy[frame_idx] = vy + out_vz[frame_idx] = vz + frame_idx += 1 + + x, y, z, vx, vy, vz = _do_step( + x, y, z, vx, vy, vz, fixed, masses, method_id, + Gx, Gy, Gz, Bx, By, Bz, + gravity_field, elastic_force, damping_force, + bond_pairs, bond_k, bond_r0, dt, pos_init, box_a) + + return out_x, out_y, out_z, out_vx, out_vy, out_vz diff --git a/engines/python/main.py b/engines/python/main.py new file mode 100644 index 0000000..39b2284 --- /dev/null +++ b/engines/python/main.py @@ -0,0 +1,283 @@ +""" +engines/python/main.py +----------------------- +独立 Python 计算引擎。 + +与 main.c / main.cpp / main.f90 结构一致: + 输入: /coord.txt, connection.txt, bond.txt, [driver.txt] + (同 engines/c/param.json 格式) + 输出: /display.txt (+ display.npz) + /trajectory.txt (若 save_trajectory=1) + +用法: + python main.py + +内部调用 dynamics_lib.run_dynamics(),算法与 compute.py 完全一致。 +""" + +import json +import os +import sys +import time +import numpy as np + +# 将父目录(engines/python 的上级 engines)加入 sys.path, +# 以便在独立运行时也能找到 dynamics_lib +_HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, _HERE) + +from dynamics_lib import run_dynamics + +# 为读取 coord/bond/display,复用 compute.py 中的 I/O 函数 +_COMPUTE = os.path.join(_HERE, "..", "..") +sys.path.insert(0, _COMPUTE) +import compute as _c + +_METHOD_ID = { + "explicit_euler": 0, + "euler": 0, + "implicit_euler": 1, + "midpoint": 2, + "leapfrog": 3, +} + + +def _load_params(param_path): + """读取 param.json(与 C 引擎格式相同).""" + with open(param_path, "r", encoding="utf-8") as f: + p = json.load(f) + return p + + +def main(): + if len(sys.argv) < 4: + print("用法: python main.py ") + sys.exit(1) + + input_dir = sys.argv[1] + output_dir = sys.argv[2] + param_path = sys.argv[3] + + os.makedirs(output_dir, exist_ok=True) + + # ── 读取参数 ───────────────────────────────────────────── + p = _load_params(param_path) + box_a = float(p.get("box_a", 10.0)) + NT = int(p.get("NT", 10000)) + dt = float(p.get("DT", 0.001)) + NSTEP = int(p.get("NSTEP", 100)) + warmup_steps = int(p.get("warmup_steps", 0)) + method_str = str(p.get("method", "leapfrog")).lower().replace(" ", "_") + method_id = _METHOD_ID.get(method_str, 3) + G = p.get("G", [0.0, 0.0, -9.8]) + B = p.get("B", [0.0, 0.0, 0.0]) + gravity_field = int(p.get("gravity_field", 1)) + elastic_force = int(p.get("elastic_force", 1)) + damping_force = int(p.get("damping_force", 0)) + gravity_strength = float(p.get("gravity_strength", 1.0)) + driving_force = int(p.get("driving_force", 0)) + save_traj = int(p.get("save_trajectory", 0)) + + # ── 读取原子数据 ────────────────────────────────────────── + coord_path = os.path.join(input_dir, "coord.txt") + atom_ids, masses, radii, positions, velocities, fixed = _c.load_coord_file(coord_path) + + # ── 读取键数据 ──────────────────────────────────────────── + conn_path = os.path.join(input_dir, "connection.txt") + bond_path = os.path.join(input_dir, "bond.txt") + bond_map = _c.load_bond_parameters(bond_path) + bond_pairs, bond_names, bond_stiffness, bond_rest_lengths = \ + _c.load_bond_connections(conn_path, atom_ids, positions, bond_map) + n_bonds = len(bond_pairs) + + # ── 读取驱动力 ──────────────────────────────────────────── + drv_list = [] + if driving_force: + driver_path = os.path.join(input_dir, "driver.txt") + raw_drivers = _c.load_driver_file(driver_path, atom_ids) + if raw_drivers: + atom_id_map = {int(aid): i for i, aid in enumerate(atom_ids)} + for d in raw_drivers: + aid = int(d["atom_id"]) + if aid not in atom_id_map: + continue + lidx = atom_id_map[aid] + eq = positions[lidx].tolist() + d["eq_pos"] = np.array(eq) + pc = d.get("period_cycles") + nc = float(pc) if pc is not None else 0.0 + hp = 1 if nc > 0 else 0 + drv_list.append({ + "local_idx": lidx, + "amp": d["amp"].tolist(), + "freq": d["freq"].tolist(), + "phi": d["phi"].tolist(), # radians + "eq": eq, + "nc": nc, + "hp": hp, + }) + + nd = len(drv_list) + if nd > 0: + drv_idx = np.array([d["local_idx"] for d in drv_list], dtype=np.int64) + drv_amp = np.array([d["amp"] for d in drv_list], dtype=np.float64) + drv_freq = np.array([d["freq"] for d in drv_list], dtype=np.float64) + drv_phi = np.array([d["phi"] for d in drv_list], dtype=np.float64) + drv_eq = np.array([d["eq"] for d in drv_list], dtype=np.float64) + drv_nc = np.array([d["nc"] for d in drv_list], dtype=np.float64) + drv_hp = np.array([d["hp"] for d in drv_list], dtype=np.int32) + else: + drv_idx = drv_amp = drv_freq = drv_phi = drv_eq = drv_nc = drv_hp = \ + np.zeros(0, dtype=np.int64) + + # ── 计算帧数 ────────────────────────────────────────────── + record_steps = NT - warmup_steps + n_frames = max(1, record_steps // NSTEP) + + # ── 进度回调 ────────────────────────────────────────────── + def _progress(step, total): + pct = step * 100 // total + print(f"[python-engine] progress: {step}/{total} ({pct}%)", flush=True) + + # ── 运行计算 ────────────────────────────────────────────── + t0 = time.time() + print(f"[python-engine] NT={NT} NSTEP={NSTEP} method={method_str} " + f"n_atoms={len(atom_ids)} n_bonds={n_bonds}") + + out_x, out_y, out_z, out_vx, out_vy, out_vz = run_dynamics( + n_atoms=len(atom_ids), + pos_init=positions, + vel_init=velocities, + masses=masses, + fixed=fixed, + n_bonds=n_bonds, + bond_pairs=bond_pairs, + bond_k=bond_stiffness, + bond_r0=bond_rest_lengths, + box_a=box_a, + dt=dt, + NT=NT, + NSTEP=NSTEP, + warmup_steps=warmup_steps, + method_id=method_id, + Gx=float(G[0]), Gy=float(G[1]), Gz=float(G[2]), + Bx=float(B[0]), By=float(B[1]), Bz=float(B[2]), + gravity_field=gravity_field, + elastic_force=elastic_force, + damping_force=damping_force, + gravity_strength=gravity_strength, + n_drivers=nd, + drv_idx=drv_idx, + drv_amp=drv_amp, + drv_freq=drv_freq, + drv_phi=drv_phi, + drv_eq=drv_eq, + drv_ncycles=drv_nc, + drv_has_period=drv_hp, + n_frames=n_frames, + progress_cb=_progress, + ) + elapsed = time.time() - t0 + print(f"[python-engine] 完成: {n_frames} 帧 {elapsed:.3f} s") + + # ── 构建 display header ─────────────────────────────────── + ball_radius = float(p.get("ball_radius", 0.5)) + ball_color = p.get("ball_color", [0.9, 0.2, 0.2]) + box_color = p.get("box_color", [0.8, 0.8, 0.85]) + use_marker = int(p.get("use_marker", 0)) + alpha_val = p.get("alpha", 0.2) + cam_dist = float(p.get("camera_distance", 40.0)) + cam_elev = float(p.get("camera_elevation", 0.0)) + cam_azim = float(p.get("camera_azimuth", 0.0)) + cam_cx = float(p.get("camera_center_x", 0.0)) + cam_cy = float(p.get("camera_center_y", 0.0)) + cam_cz = float(p.get("camera_center_z", 0.0)) + + header = { + "DT": str(dt), + "NSTEP": str(NSTEP), + "method": method_str, + "NT": str(NT), + "warmup_steps": str(warmup_steps), + "dynamic_steps": str(record_steps), + "T_total": str(NT * dt), + "box_a": str(box_a), + "gravity_field": str(gravity_field), + "elastic_force": str(elastic_force), + "damping_force": str(damping_force), + "driving_force": str(driving_force), + "gravity_strength": str(gravity_strength), + "G": json.dumps([float(v) for v in G]), + "B": json.dumps([float(v) for v in B]), + "number_of_frames": str(n_frames), + "number_of_particles": str(len(atom_ids)), + "use_marker": str(use_marker), + "ball_radius": str(ball_radius), + "ball_color_r": str(ball_color[0]), + "ball_color_g": str(ball_color[1]), + "ball_color_b": str(ball_color[2]), + "box_color_r": str(box_color[0]), + "box_color_g": str(box_color[1]), + "box_color_b": str(box_color[2]), + "alpha": str(alpha_val) if not isinstance(alpha_val, list) + else ",".join(str(a) for a in alpha_val), + "atom_radii": ",".join(str(r) for r in radii), + "atom_masses": json.dumps([float(m) for m in masses]), + "atom_positions": json.dumps(positions.tolist()), + "bond_pairs": json.dumps(bond_pairs.tolist() if n_bonds else []), + "bond_stiffness": json.dumps(bond_stiffness.tolist() if n_bonds else []), + "bond_rest_lengths": json.dumps(bond_rest_lengths.tolist() if n_bonds else []), + "X_MIN": str(-box_a), "X_MAX": str(box_a), + "Y_MIN": str(-box_a), "Y_MAX": str(box_a), + "Z_MIN": str(-box_a), "Z_MAX": str(box_a), + "camera_distance": str(cam_dist), + "camera_elevation": str(cam_elev), + "camera_azimuth": str(cam_azim), + "camera_center_x": str(cam_cx), + "camera_center_y": str(cam_cy), + "camera_center_z": str(cam_cz), + "camera_keyframes": "", + } + + # ── 保存 display.txt + display.npz ─────────────────────── + disp_txt = os.path.join(output_dir, "display.txt") + _c.save_display_txt( + disp_txt, + out_x, out_y, out_z, out_vx, out_vy, out_vz, + atom_ids, record_steps, len(atom_ids), + header_fields=header, + ) + print(f"[python-engine] display.txt 已保存: {disp_txt}") + + disp_npz = os.path.join(output_dir, "display.npz") + _c.save_display_npz( + disp_npz, + out_x, out_y, out_z, out_vx, out_vy, out_vz, + atom_ids, header_fields=header, + ) + print(f"[python-engine] display.npz 已保存: {disp_npz}") + + # ── 可选:保存 trajectory.txt ───────────────────────────── + if save_traj: + traj_payload = { + "traj_x": out_x, "traj_y": out_y, "traj_z": out_z, + "traj_vx": out_vx, "traj_vy": out_vy, "traj_vz": out_vz, + "NT": record_steps, "DT": dt, "NSTEP": NSTEP, + "method": method_str, + "atom_ids": atom_ids, + "atom_masses": masses, + "atom_radii": radii, + "atom_positions": positions, + "bond_pairs": bond_pairs, + "bond_stiffness": bond_stiffness, + "bond_rest_lengths": bond_rest_lengths, + "G": [float(v) for v in G], + "B": [float(v) for v in B], + } + traj_path = os.path.join(output_dir, "trajectory.txt") + _c.save_text_data(traj_path, traj_payload) + print(f"[python-engine] trajectory.txt 已保存: {traj_path}") + + +if __name__ == "__main__": + main() diff --git a/examples/Readme.html b/examples/Readme.html new file mode 100644 index 0000000..5aa9d2b --- /dev/null +++ b/examples/Readme.html @@ -0,0 +1,436 @@ + + + + + +Dynamics 示例案例总览 + + + +
+ +

Dynamics 示例案例 v2.0

+

10 个从简单到复杂的物理模拟案例,展示分子动力学模拟框架的多种应用场景

+ +

📋 案例总览

+
+ +
+ 01 +

双粒子弹簧系统

+

两个原子由弹簧连接,在重力场中运动

+
+ Python + 弹簧 + 重力 +
+
+ +
+ 02 +

行星运动

+

地球绕太阳椭圆公转(万有引力)

+
+ Python + 万有引力 +
+
+ +
+ 03 +

日地月系统(失稳)

+

三体系统参数不当导致轨道发散

+
+ Python + 万有引力 + 失败案例 +
+
+ +
+ 04 +

日地月系统(稳定)

+

三体系统稳定椭圆轨道

+
+ Python + 万有引力 +
+
+ +
+ 05 +

一维原子链纵波

+

驱动原子 1 沿 x 振动,产生纵波传播

+
+ Python + 弹簧 + 驱动力 +
+
+ +
+ 06 +

一维原子链横波

+

带阻尼的横波传播(FPU 非线性)

+
+ C 引擎 + 弹簧 + 驱动力 +
+
+ +
+ 07 +

一维链横波·双端驱动

+

原子 1 + 原子 120 同时驱动,波相遇干涉

+
+ C 引擎 + 弹簧 + 驱动力 +
+
+ +
+ 08 +

双原子弹簧·C 引擎测试

+

2 原子快速验证 C 引擎正确性

+
+ C 引擎 + 弹簧 +
+
+ +
+ 09 +

一维链纵波·Fortran 引擎

+

Fortran 引擎驱动的纵波传播测试

+
+ Fortran + 弹簧 + 驱动力 +
+
+ +
+ 10 +

一维链纵波·能量分析

+

纵波传播 + 轨迹/能量图绘制

+
+ C 引擎 + 弹簧 + 驱动力 +
+
+ +
+ +

📖 各案例详情

+ +
+

case01 — 双粒子弹簧系统

+
    +
  • Python 引擎
  • +
  • 弹簧键力
  • +
  • 重力场
  • +
+

两个原子通过弹簧连接,在均匀重力场(G=[0,0,-9.8])中自由运动。展示重力作用下的耦合振动与落体运动的复合。

+
🔬 教学案例:算法 leapfrog,渲染 Sphere 模式,建议作为入门第一个案例
+
+ +
+

case02 — 行星运动

+
    +
  • Python 引擎
  • +
  • 万有引力
  • +
+

模拟地球绕太阳的椭圆轨道运动。大质量中心体固定,小质量体绕行。

+
🌍 万有引力强度 gravity_strength=100.0,leapfrog 算法确保能量守恒
+
+ +
+

case03 — 日地月系统(失败案例)

+
    +
  • Python 引擎
  • +
  • 万有引力
  • +
  • 失稳
  • +
+

三体系统(太阳-地球-月球)。初始条件或参数设置不当,轨道不稳定。展示数值模拟中参数选择的重要性。

+
+ +
+

case04 — 日地月系统(成功案例)

+
    +
  • Python 引擎
  • +
  • 万有引力
  • +
+

与 case03 相同的三体系统,但采用恰当的初始条件,地球和月球维持稳定的椭圆轨道运动。

+
✅ 与 case03 对比学习:初始条件对数值稳定性的影响
+
+ +
+

case05 — 一维原子链纵波

+
    +
  • Python 引擎
  • +
  • 弹簧键力
  • +
  • 驱动力
  • +
+

60 原子沿 x 轴排列。原子 1 受 x 方向驱动力,产生沿链传播的纵波(压缩波)。原子 x 自由,y/z 锁定。

+
📈 纵波波速快(x 方向弹簧力线性),T_total=10, NSTEP=50
+
+ +
+

case06 — 一维原子链横波(带阻尼)

+
    +
  • C 引擎
  • +
  • 弹簧键力
  • +
  • 驱动力
  • +
+

120 原子沿 x 轴排列,带横向阻尼。驱动沿 z 方向,原子 z 自由,x/y 锁定。横波传播具有 FPU 型非线性。

+
⚡ C 引擎高性能计算,T_total=1000, NSTEP=500,支持运动相机
+
+ +
+

case07 — 一维链横波·双端驱动

+
    +
  • C 引擎
  • +
  • 弹簧键力
  • +
  • 驱动力
  • +
+

120 原子,原子 1 和原子 120 同时受 z 方向驱动(同频率、相位差 90°),两端向中间传播的横波相遇。

+
🌊 波干涉演示,视觉放大 display_amp=[1,1,10] 便于观察小幅度振动
+
+ +
+

case08 — 双原子弹簧(C 引擎快速测试)

+
    +
  • C 引擎
  • +
  • 弹簧键力
  • +
+

2 原子弹簧系统,T_total=100 的短时模拟。用于快速验证 C 引擎的正确性和性能。

+
🧪 引擎快速验证用例,无动画输出
+
+ +
+

case09 — 一维链纵波·Fortran 引擎

+
    +
  • Fortran 引擎
  • +
  • 弹簧键力
  • +
  • 驱动力
  • +
+

40 原子沿 x 轴排列,Fortran 引擎驱动的纵波传播测试。验证 Fortran 引擎的输出兼容性。

+
🔧 Fortran 引擎兼容性验证,T_total=200, NSTEP=100
+
+ +
+

case10 — 一维链纵波·能量分析

+
    +
  • C 引擎
  • +
  • 弹簧键力
  • +
  • 驱动力
  • +
+

40 原子纵波传播,支持轨迹/能量图绘制(step_plot=1)。原子 x/y/z 全部自由。

+
📊 能量分析演示,T_total=10, NSTEP=20
+
+ +

🚀 使用方法

+
+

命令行

+ # 进入案例目录并运行
cd examples/case05
python run_dynamics.py

# 仅运行模拟,跳过动画
python run_dynamics.py --no-plot

# 手动启动 3D 动画
python ../../draw.py output/
+ +

案例选择指南

+ + + + + + + + + +
目标推荐案例
快速上手框架case01
天体力学/万有引力case02 / case04
波动物理(纵波)case05
波动物理(横波/非线性/阻尼)case06
双端驱动/波干涉case07
引擎性能对比case08 (C) / case09 (Fortran)
能量分析case10
+ +

配置

+

+ 每个案例的 input/input.txt 可配置物理参数、力开关、算法、引擎、渲染方式等。 + 从 case06 起支持 save_trajectory 开关、摄像机初始位置、display_amp 视觉放大等高级功能。 +

+
+ +

📁 框架结构

+
+
+dynamics/
+├── dynamics.py          # 统一运行入口
+├── compute.py           # Python 物理引擎
+├── draw.py              # VisPy 3D 动画
+├── plot_wave.py         # 波形能量图
+├── engines/             # C / C++ / Fortran 引擎
+├── examples/            # 案例目录
+│   ├── case01/  ~  case10/
+└── output/              # 默认输出目录
+
+
+ +
+ + diff --git a/examples/Readme.md b/examples/Readme.md index 2ffd74f..6c3d16b 100644 --- a/examples/Readme.md +++ b/examples/Readme.md @@ -1,19 +1,23 @@ # Dynamics 示例案例 -本目录包含 6 个从简单到复杂的物理模拟案例,均基于 `../dynamics.py` 框架运行。 +本目录包含 10 个从简单到复杂的物理模拟案例,均基于 `../dynamics.py` 框架运行。 --- ## 案例一览 -| 案例 | 标题 | 简介 | 原子数 | 力类型 | -|---|---|---|---|---| -| [case01](./case01/) | **双粒子弹簧系统** | 两个原子由弹簧连接,在重力场中运动 | 2 | 重力 + 弹簧 | -| [case02](./case02/) | **行星运动** | 地球绕太阳椭圆公转(万有引力) | 2 | 万有引力 | -| [case03](./case03/) | **日地月系统(失败)** | 地球绕太阳、月球绕地球,参数不当导致失稳 | 3 | 万有引力 | -| [case04](./case04/) | **日地月系统(成功)** | 地球绕太阳、月球绕地球,稳定轨道 | 3 | 万有引力 | -| [case05](./case05/) | **一维原子链纵波** | 驱动原子 1 沿 x 轴振动,产生纵波传播 | 60 | 弹簧 + 驱动力 | -| [case06](./case06/) | **一维原子链横波** | 驱动原子 1 沿 z 轴振动,产生横波传播 | 120 | 弹簧 + 驱动力 | +| 案例 | 标题 | 简介 | 原子数 | 引擎 | 力类型 | +|------|------|------|--------|------|--------| +| [case01](./case01/) | **双粒子弹簧系统** | 两个原子由弹簧连接,在重力场中运动 | 2 | Python | 重力 + 弹簧 | +| [case02](./case02/) | **行星运动** | 地球绕太阳椭圆公转(万有引力) | 2 | Python | 万有引力 | +| [case03](./case03/) | **日地月系统(失稳)** | 地球绕太阳、月球绕地球,参数不当导致失稳 | 3 | Python | 万有引力 | +| [case04](./case04/) | **日地月系统(稳定)** | 地球绕太阳、月球绕地球,稳定轨道 | 3 | Python | 万有引力 | +| [case05](./case05/) | **一维原子链纵波** | 驱动原子 1 沿 x 轴振动,产生纵波传播 | 60 | Python | 弹簧 + 驱动力 | +| [case06](./case06/) | **一维原子链横波(阻尼)** | 驱动原子 1 沿 z 轴振动,带阻尼的横波传播 | 120 | C | 弹簧 + 阻尼 + 驱动力 | +| [case07](./case07/) | **一维原子链横波(双端驱动)** | 原子 1 和原子 120 同时受 z 方向驱动 | 120 | C | 弹簧 + 阻尼 + 驱动力 | +| [case08](./case08/) | **双原子弹簧(C 引擎测试)** | 两个原子弹簧系统,C 引擎快速验证 | 2 | C | 弹簧 + 阻尼 + 驱动力 | +| [case09](./case09/) | **一维链纵波(Fortran 引擎)** | Fortran 引擎驱动的纵波传播测试 | 40 | Fortran | 弹簧 + 驱动力 | +| [case10](./case10/) | **一维链纵波(能量分析)** | 纵波传播 + 轨迹/能量图绘制 | 40 | C | 弹簧 + 驱动力 | --- @@ -26,14 +30,16 @@ - **力开关**:重力场开,弹簧键力开 - **算法**:leapfrog(蛙跳法) - **物理**:重力 m·g + 弹簧胡克力 +- **渲染**:Sphere 模式(精细网格球体) ### case02 — 行星运动 模拟地球绕太阳的椭圆轨道运动(一个固定大质量中心体 + 一个绕行小质量体)。采用万有引力相互作用。 -- **力开关**:万有引力开(含强度参数) +- **力开关**:万有引力开(含强度参数 `gravity_strength: 100.0`) - **算法**:leapfrog(蛙跳法) - **物理**:牛顿万有引力 F = G·m₁·m₂/r² +- **渲染**:Sphere 模式 ### case03 — 日地月系统(失败案例) @@ -60,14 +66,53 @@ - **波速**:快(x 方向弹簧力为线性) - **渲染**:Marker 模式(GPU 实例化,60 原子) -### case06 — 一维原子链横波 +### case06 — 一维原子链横波(带阻尼) -120 个原子沿 x 轴等间距排列(间距 1),相邻原子用弹簧(k=1.0, L₀=1.0)连接。驱动力沿 z 方向 `z(t)=0.5·cos(2π·0.1·t+90°)`,原子 z 方向自由(fix_z=0),x/y 锁定。振动在横向传播,形成**横波**。 +120 个原子沿 x 轴等间距排列(间距 1),相邻原子用弹簧(k=1.0, L₀=1.0)连接,带横向阻尼(B=[0.01, 0, 0.01])。驱动在 z 方向 `z(t)=0.5·cos(2π·0.1·t+90°)`,原子 z 方向自由(fix_z=0),x/y 锁定。 -- **力开关**:弹簧键力开,驱动力开 +- **力开关**:弹簧键力开,驱动力开,阻尼开 - **算法**:leapfrog(蛙跳法) +- **引擎**:C(高性能) +- **参数**:T_total=1000, NSTEP=500 - **波速**:慢(z 方向弹簧力呈几何非线性,类似 FPU 系统) -- **渲染**:Marker 模式(GPU 实例化,120 原子) + +### case07 — 一维原子链横波(双端驱动) + +120 个原子沿 x 轴排列,原子 1 **和**原子 120 同时受 z 方向驱动力驱动(频率相同,初相位错开 90°),产生两端向中间传播的横波相遇。 + +- **力开关**:弹簧键力开,驱动力开,阻尼开 +- **引擎**:C +- **参数**:T_total=1000, NSTEP=500 +- **视觉放大**:z 方向位移放大 10 倍(`display_amp: [1, 1, 10]`),便于观察小幅度横波 + +### case08 — 双原子弹簧(C 引擎快速测试) + +2 个原子的简单弹簧系统,用于快速验证 C 引擎的正确性和性能。T_total 仅为 100s。 + +- **力开关**:弹簧键力开,驱动力开,阻尼开 +- **引擎**:C +- **用途**:引擎验证 / 调试 +- **视觉放大**:z 方向位移放大 10 倍 +- **动画**:关闭(step_animation=0),仅输出波形图 + +### case09 — 一维链纵波(Fortran 引擎) + +40 个原子沿 x 轴排列,使用 **Fortran 引擎**驱动的纵波传播模拟。验证 Fortran 引擎的输出兼容性和性能。 + +- **力开关**:弹簧键力开,驱动力开,阻尼关 +- **引擎**:Fortran +- **参数**:T_total=200, NSTEP=100 +- **视觉放大**:z 方向位移放大 10 倍 + +### case10 — 一维链纵波(能量分析) + +40 个原子沿 x 轴排列,纵波传播。与 case05 相比,x/y/z 全部自由(fix_x/y/z 均为 0),物理行为更复杂。支持轨迹/能量图绘制(step_plot=1)。 + +- **力开关**:弹簧键力开,驱动力开,阻尼关 +- **引擎**:C +- **参数**:T_total=10, NSTEP=20 +- **视觉放大**:z 方向位移放大 10 倍 +- **动画**:关闭(step_animation=0),仅输出轨迹/能量图 --- @@ -77,7 +122,7 @@ # 进入任意案例目录 cd examples/case05 -# 完整运行(模拟 + 采样 + 3D 动画) +# 完整运行(模拟 + 3D 动画) python run_dynamics.py # 仅运行模拟,跳过 3D 动画 @@ -89,6 +134,18 @@ python ../../draw.py output/ 每个案例的 `input/input.txt` 中可配置所有物理参数、力开关、算法、渲染方式等。 +## 案例选择指南 + +| 你想做什么 | 推荐案例 | +|-----------|---------| +| 快速上手、理解基本框架 | case01 | +| 天体力学 / 万有引力 | case02 / case04 | +| 波动物理(纵波) | case05 | +| 波动物理(横波、非线性、阻尼) | case06 | +| 双端驱动/波干涉 | case07 | +| 对比不同引擎性能 | case08 (C) / case09 (Fortran) | +| 能量分析 | case10 | + ## 框架结构 ``` @@ -101,6 +158,6 @@ dynamics/ ├── examples/ # 案例(本目录) │ ├── case01/ │ ├── ... -│ └── case06/ +│ └── case10/ └── output/ # 默认输出目录 ``` diff --git a/examples/case06/input/bond.txt b/examples/case06/input/bond.txt index b841478..19b8c7d 100644 --- a/examples/case06/input/bond.txt +++ b/examples/case06/input/bond.txt @@ -1,2 +1,2 @@ -bond_name k rest_length -k1 50.0 1.0 +bond_name k rest_length +k1 10.0 1.0 diff --git a/examples/case06/input/coord.txt b/examples/case06/input/coord.txt index c274431..ae980a5 100644 --- a/examples/case06/input/coord.txt +++ b/examples/case06/input/coord.txt @@ -1,121 +1,121 @@ n mass radius x y z vx vy vz fix_x fix_y fix_z -1 1 0.1 0 0 0 0 0 0 1 1 0 -2 1 0.1 1 0 0 0 0 0 1 1 0 -3 1 0.1 2 0 0 0 0 0 1 1 0 -4 1 0.1 3 0 0 0 0 0 1 1 0 -5 1 0.1 4 0 0 0 0 0 1 1 0 -6 1 0.1 5 0 0 0 0 0 1 1 0 -7 1 0.1 6 0 0 0 0 0 1 1 0 -8 1 0.1 7 0 0 0 0 0 1 1 0 -9 1 0.1 8 0 0 0 0 0 1 1 0 -10 1 0.1 9 0 0 0 0 0 1 1 0 -11 1 0.1 10 0 0 0 0 0 1 1 0 -12 1 0.1 11 0 0 0 0 0 1 1 0 -13 1 0.1 12 0 0 0 0 0 1 1 0 -14 1 0.1 13 0 0 0 0 0 1 1 0 -15 1 0.1 14 0 0 0 0 0 1 1 0 -16 1 0.1 15 0 0 0 0 0 1 1 0 -17 1 0.1 16 0 0 0 0 0 1 1 0 -18 1 0.1 17 0 0 0 0 0 1 1 0 -19 1 0.1 18 0 0 0 0 0 1 1 0 -20 1 0.1 19 0 0 0 0 0 1 1 0 -21 1 0.1 20 0 0 0 0 0 1 1 0 -22 1 0.1 21 0 0 0 0 0 1 1 0 -23 1 0.1 22 0 0 0 0 0 1 1 0 -24 1 0.1 23 0 0 0 0 0 1 1 0 -25 1 0.1 24 0 0 0 0 0 1 1 0 -26 1 0.1 25 0 0 0 0 0 1 1 0 -27 1 0.1 26 0 0 0 0 0 1 1 0 -28 1 0.1 27 0 0 0 0 0 1 1 0 -29 1 0.1 28 0 0 0 0 0 1 1 0 -30 1 0.1 29 0 0 0 0 0 1 1 0 -31 1 0.1 30 0 0 0 0 0 1 1 0 -32 1 0.1 31 0 0 0 0 0 1 1 0 -33 1 0.1 32 0 0 0 0 0 1 1 0 -34 1 0.1 33 0 0 0 0 0 1 1 0 -35 1 0.1 34 0 0 0 0 0 1 1 0 -36 1 0.1 35 0 0 0 0 0 1 1 0 -37 1 0.1 36 0 0 0 0 0 1 1 0 -38 1 0.1 37 0 0 0 0 0 1 1 0 -39 1 0.1 38 0 0 0 0 0 1 1 0 -40 1 0.1 39 0 0 0 0 0 1 1 0 -41 1 0.1 40 0 0 0 0 0 1 1 0 -42 1 0.1 41 0 0 0 0 0 1 1 0 -43 1 0.1 42 0 0 0 0 0 1 1 0 -44 1 0.1 43 0 0 0 0 0 1 1 0 -45 1 0.1 44 0 0 0 0 0 1 1 0 -46 1 0.1 45 0 0 0 0 0 1 1 0 -47 1 0.1 46 0 0 0 0 0 1 1 0 -48 1 0.1 47 0 0 0 0 0 1 1 0 -49 1 0.1 48 0 0 0 0 0 1 1 0 -50 1 0.1 49 0 0 0 0 0 1 1 0 -51 1 0.1 50 0 0 0 0 0 1 1 0 -52 1 0.1 51 0 0 0 0 0 1 1 0 -53 1 0.1 52 0 0 0 0 0 1 1 0 -54 1 0.1 53 0 0 0 0 0 1 1 0 -55 1 0.1 54 0 0 0 0 0 1 1 0 -56 1 0.1 55 0 0 0 0 0 1 1 0 -57 1 0.1 56 0 0 0 0 0 1 1 0 -58 1 0.1 57 0 0 0 0 0 1 1 0 -59 1 0.1 58 0 0 0 0 0 1 1 0 -60 1 0.1 59 0 0 0 0 0 1 1 0 -61 1 0.1 60 0 0 0 0 0 1 1 0 -62 1 0.1 61 0 0 0 0 0 1 1 0 -63 1 0.1 62 0 0 0 0 0 1 1 0 -64 1 0.1 63 0 0 0 0 0 1 1 0 -65 1 0.1 64 0 0 0 0 0 1 1 0 -66 1 0.1 65 0 0 0 0 0 1 1 0 -67 1 0.1 66 0 0 0 0 0 1 1 0 -68 1 0.1 67 0 0 0 0 0 1 1 0 -69 1 0.1 68 0 0 0 0 0 1 1 0 -70 1 0.1 69 0 0 0 0 0 1 1 0 -71 1 0.1 70 0 0 0 0 0 1 1 0 -72 1 0.1 71 0 0 0 0 0 1 1 0 -73 1 0.1 72 0 0 0 0 0 1 1 0 -74 1 0.1 73 0 0 0 0 0 1 1 0 -75 1 0.1 74 0 0 0 0 0 1 1 0 -76 1 0.1 75 0 0 0 0 0 1 1 0 -77 1 0.1 76 0 0 0 0 0 1 1 0 -78 1 0.1 77 0 0 0 0 0 1 1 0 -79 1 0.1 78 0 0 0 0 0 1 1 0 -80 1 0.1 79 0 0 0 0 0 1 1 0 -81 1 0.1 80 0 0 0 0 0 1 1 0 -82 1 0.1 81 0 0 0 0 0 1 1 0 -83 1 0.1 82 0 0 0 0 0 1 1 0 -84 1 0.1 83 0 0 0 0 0 1 1 0 -85 1 0.1 84 0 0 0 0 0 1 1 0 -86 1 0.1 85 0 0 0 0 0 1 1 0 -87 1 0.1 86 0 0 0 0 0 1 1 0 -88 1 0.1 87 0 0 0 0 0 1 1 0 -89 1 0.1 88 0 0 0 0 0 1 1 0 -90 1 0.1 89 0 0 0 0 0 1 1 0 -91 1 0.1 90 0 0 0 0 0 1 1 0 -92 1 0.1 91 0 0 0 0 0 1 1 0 -93 1 0.1 92 0 0 0 0 0 1 1 0 -94 1 0.1 93 0 0 0 0 0 1 1 0 -95 1 0.1 94 0 0 0 0 0 1 1 0 -96 1 0.1 95 0 0 0 0 0 1 1 0 -97 1 0.1 96 0 0 0 0 0 1 1 0 -98 1 0.1 97 0 0 0 0 0 1 1 0 -99 1 0.1 98 0 0 0 0 0 1 1 0 -100 1 0.1 99 0 0 0 0 0 1 1 0 -101 1 0.1 100 0 0 0 0 0 1 1 0 -102 1 0.1 101 0 0 0 0 0 1 1 0 -103 1 0.1 102 0 0 0 0 0 1 1 0 -104 1 0.1 103 0 0 0 0 0 1 1 0 -105 1 0.1 104 0 0 0 0 0 1 1 0 -106 1 0.1 105 0 0 0 0 0 1 1 0 -107 1 0.1 106 0 0 0 0 0 1 1 0 -108 1 0.1 107 0 0 0 0 0 1 1 0 -109 1 0.1 108 0 0 0 0 0 1 1 0 -110 1 0.1 109 0 0 0 0 0 1 1 0 -111 1 0.1 110 0 0 0 0 0 1 1 0 -112 1 0.1 111 0 0 0 0 0 1 1 0 -113 1 0.1 112 0 0 0 0 0 1 1 0 -114 1 0.1 113 0 0 0 0 0 1 1 0 -115 1 0.1 114 0 0 0 0 0 1 1 0 -116 1 0.1 115 0 0 0 0 0 1 1 0 -117 1 0.1 116 0 0 0 0 0 1 1 0 -118 1 0.1 117 0 0 0 0 0 1 1 0 -119 1 0.1 118 0 0 0 0 0 1 1 0 -120 1 0.1 119 0 0 0 0 0 1 1 1 +1 1 0.1 0 0 0 0 0 0 0 1 0 +2 1 0.1 1 0 0 0 0 0 0 1 0 +3 1 0.1 2 0 0 0 0 0 0 1 0 +4 1 0.1 3 0 0 0 0 0 0 1 0 +5 1 0.1 4 0 0 0 0 0 0 1 0 +6 1 0.1 5 0 0 0 0 0 0 1 0 +7 1 0.1 6 0 0 0 0 0 0 1 0 +8 1 0.1 7 0 0 0 0 0 0 1 0 +9 1 0.1 8 0 0 0 0 0 0 1 0 +10 1 0.1 9 0 0 0 0 0 0 1 0 +11 1 0.1 10 0 0 0 0 0 0 1 0 +12 1 0.1 11 0 0 0 0 0 0 1 0 +13 1 0.1 12 0 0 0 0 0 0 1 0 +14 1 0.1 13 0 0 0 0 0 0 1 0 +15 1 0.1 14 0 0 0 0 0 0 1 0 +16 1 0.1 15 0 0 0 0 0 0 1 0 +17 1 0.1 16 0 0 0 0 0 0 1 0 +18 1 0.1 17 0 0 0 0 0 0 1 0 +19 1 0.1 18 0 0 0 0 0 0 1 0 +20 1 0.1 19 0 0 0 0 0 0 1 0 +21 1 0.1 20 0 0 0 0 0 0 1 0 +22 1 0.1 21 0 0 0 0 0 0 1 0 +23 1 0.1 22 0 0 0 0 0 0 1 0 +24 1 0.1 23 0 0 0 0 0 0 1 0 +25 1 0.1 24 0 0 0 0 0 0 1 0 +26 1 0.1 25 0 0 0 0 0 0 1 0 +27 1 0.1 26 0 0 0 0 0 0 1 0 +28 1 0.1 27 0 0 0 0 0 0 1 0 +29 1 0.1 28 0 0 0 0 0 0 1 0 +30 1 0.1 29 0 0 0 0 0 0 1 0 +31 1 0.1 30 0 0 0 0 0 0 1 0 +32 1 0.1 31 0 0 0 0 0 0 1 0 +33 1 0.1 32 0 0 0 0 0 0 1 0 +34 1 0.1 33 0 0 0 0 0 0 1 0 +35 1 0.1 34 0 0 0 0 0 0 1 0 +36 1 0.1 35 0 0 0 0 0 0 1 0 +37 1 0.1 36 0 0 0 0 0 0 1 0 +38 1 0.1 37 0 0 0 0 0 0 1 0 +39 1 0.1 38 0 0 0 0 0 0 1 0 +40 1 0.1 39 0 0 0 0 0 0 1 0 +41 1 0.1 40 0 0 0 0 0 0 1 0 +42 1 0.1 41 0 0 0 0 0 0 1 0 +43 1 0.1 42 0 0 0 0 0 0 1 0 +44 1 0.1 43 0 0 0 0 0 0 1 0 +45 1 0.1 44 0 0 0 0 0 0 1 0 +46 1 0.1 45 0 0 0 0 0 0 1 0 +47 1 0.1 46 0 0 0 0 0 0 1 0 +48 1 0.1 47 0 0 0 0 0 0 1 0 +49 1 0.1 48 0 0 0 0 0 0 1 0 +50 1 0.1 49 0 0 0 0 0 0 1 0 +51 1 0.1 50 0 0 0 0 0 0 1 0 +52 1 0.1 51 0 0 0 0 0 0 1 0 +53 1 0.1 52 0 0 0 0 0 0 1 0 +54 1 0.1 53 0 0 0 0 0 0 1 0 +55 1 0.1 54 0 0 0 0 0 0 1 0 +56 1 0.1 55 0 0 0 0 0 0 1 0 +57 1 0.1 56 0 0 0 0 0 0 1 0 +58 1 0.1 57 0 0 0 0 0 0 1 0 +59 1 0.1 58 0 0 0 0 0 0 1 0 +60 1 0.1 59 0 0 0 0 0 0 1 0 +61 1 0.1 60 0 0 0 0 0 0 1 0 +62 1 0.1 61 0 0 0 0 0 0 1 0 +63 1 0.1 62 0 0 0 0 0 0 1 0 +64 1 0.1 63 0 0 0 0 0 0 1 0 +65 1 0.1 64 0 0 0 0 0 0 1 0 +66 1 0.1 65 0 0 0 0 0 0 1 0 +67 1 0.1 66 0 0 0 0 0 0 1 0 +68 1 0.1 67 0 0 0 0 0 0 1 0 +69 1 0.1 68 0 0 0 0 0 0 1 0 +70 1 0.1 69 0 0 0 0 0 0 1 0 +71 1 0.1 70 0 0 0 0 0 0 1 0 +72 1 0.1 71 0 0 0 0 0 0 1 0 +73 1 0.1 72 0 0 0 0 0 0 1 0 +74 1 0.1 73 0 0 0 0 0 0 1 0 +75 1 0.1 74 0 0 0 0 0 0 1 0 +76 1 0.1 75 0 0 0 0 0 0 1 0 +77 1 0.1 76 0 0 0 0 0 0 1 0 +78 1 0.1 77 0 0 0 0 0 0 1 0 +79 1 0.1 78 0 0 0 0 0 0 1 0 +80 1 0.1 79 0 0 0 0 0 0 1 0 +81 1 0.1 80 0 0 0 0 0 0 1 0 +82 1 0.1 81 0 0 0 0 0 0 1 0 +83 1 0.1 82 0 0 0 0 0 0 1 0 +84 1 0.1 83 0 0 0 0 0 0 1 0 +85 1 0.1 84 0 0 0 0 0 0 1 0 +86 1 0.1 85 0 0 0 0 0 0 1 0 +87 1 0.1 86 0 0 0 0 0 0 1 0 +88 1 0.1 87 0 0 0 0 0 0 1 0 +89 1 0.1 88 0 0 0 0 0 0 1 0 +90 1 0.1 89 0 0 0 0 0 0 1 0 +91 1 0.1 90 0 0 0 0 0 0 1 0 +92 1 0.1 91 0 0 0 0 0 0 1 0 +93 1 0.1 92 0 0 0 0 0 0 1 0 +94 1 0.1 93 0 0 0 0 0 0 1 0 +95 1 0.1 94 0 0 0 0 0 0 1 0 +96 1 0.1 95 0 0 0 0 0 0 1 0 +97 1 0.1 96 0 0 0 0 0 0 1 0 +98 1 0.1 97 0 0 0 0 0 0 1 0 +99 1 0.1 98 0 0 0 0 0 0 1 0 +100 1 0.1 99 0 0 0 0 0 0 1 0 +101 1 0.1 100 0 0 0 0 0 0 1 0 +102 1 0.1 101 0 0 0 0 0 0 1 0 +103 1 0.1 102 0 0 0 0 0 0 1 0 +104 1 0.1 103 0 0 0 0 0 0 1 0 +105 1 0.1 104 0 0 0 0 0 0 1 0 +106 1 0.1 105 0 0 0 0 0 0 1 0 +107 1 0.1 106 0 0 0 0 0 0 1 0 +108 1 0.1 107 0 0 0 0 0 0 1 0 +109 1 0.1 108 0 0 0 0 0 0 1 0 +110 1 0.1 109 0 0 0 0 0 0 1 0 +111 1 0.1 110 0 0 0 0 0 0 1 0 +112 1 0.1 111 0 0 0 0 0 0 1 0 +113 1 0.1 112 0 0 0 0 0 0 1 0 +114 1 0.1 113 0 0 0 0 0 0 1 0 +115 1 0.1 114 0 0 0 0 0 0 1 0 +116 1 0.1 115 0 0 0 0 0 0 1 0 +117 1 0.1 116 0 0 0 0 0 0 1 0 +118 1 0.1 117 0 0 0 0 0 0 1 0 +119 1 0.1 118 0 0 0 0 0 0 1 0 +120 1 0.1 119 0 0 0 0 0 1 1 1 diff --git a/examples/case06/input/input.txt b/examples/case06/input/input.txt index d7ffffe..60bb166 100644 --- a/examples/case06/input/input.txt +++ b/examples/case06/input/input.txt @@ -19,10 +19,10 @@ save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt( # ── 计算引擎 ────────────────────────────────── # 可选: python, c, cpp, fortran, java -engine: python # 默认使用 python 引擎 +engine: c # 默认使用 python 引擎 # ── 盒子 ────────────────────────────────────── -box_a: 80.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 +box_a: 300.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 # ── 初始构型 ────────────────────────────────── # 坐标文件格式: @@ -39,7 +39,7 @@ plot_atom: 1 # ── 物理参数 ────────────────────────────────── # 三个方向分量分别对应 x, y, z G: [0.00, 0.00, 0.00] # 重力场分量 (m/s²) -B: [0.02, 0.00, 0.02] # 阻尼分量 +B: [0.01, 0.00, 0.01] # 阻尼分量 # ── 力开关(0=关闭, 1=开启)────────────────── gravity_field: 0 # 均匀重力场 (G) @@ -66,13 +66,13 @@ warmup_steps: 0 # 默认 0(立即开始记录) # 总模拟时间(秒),程序自动计算 NT = T_total / DT # 如果同时指定了 NT,以 NT 为准 -T_total: 100.0 +T_total: 1000.0 # 抽帧间隔(每 NSTEP 步取一帧用于动画) -NSTEP: 10 +NSTEP: 500 # ── 时间步长 ────────────────────────────────── -DT: 0.01 # 时间步长 (s) +DT: 0.001 # 时间步长 (s) # 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 sample_start: null # null 表示从头开始(帧索引从 0 起) @@ -102,7 +102,10 @@ box_color_g: 0.80 box_color_b: 0.85 # ── 摄像机初始位置 ──────────────────────────── -camera_distance: 40.0 # 摄像机到场景中心的距离 -camera_elevation: 0 # 俯仰角(度),负值=俯视 -camera_azimuth: 0 # 方位角(度) -move_camera: 1 # 0=固定视角, 1=按 move_camera.txt 运动 +camera_distance: 120.0 # 摄像机到场景中心的距离 +camera_elevation: 0.0 # 俯仰角(度),负值=俯视 +camera_azimuth: 0.0 # 方位角(度) +camera_center_x: 60.0 # 摄像机注视点 x +camera_center_y: 0.0 # 摄像机注视点 y +camera_center_z: 0.0 # 摄像机注视点 z +move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动 diff --git a/examples/case07/Readme.md b/examples/case07/Readme.md new file mode 100644 index 0000000..151e74c --- /dev/null +++ b/examples/case07/Readme.md @@ -0,0 +1,40 @@ +# case06: 一维原子链横波模拟 + +60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。 + +## 物理设定 + +| 参数 | 值 | +|---|---| +| 原子数 | 120 | +| 排列 | 沿 x 轴等间距排列,间距为 1 | +| 约束 | 原子**沿 z 方向自由振动**(fix_x=1, fix_y=1, fix_z=0),x, y 锁定 | +| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 | +| 重力 | 无 | +| 万有引力 | 无 | +| 阻尼 | 无 | +| 驱动力 | 原子 1(z 方向驱动) | +| 算法 | leapfrog(蛙跳法,能量守恒) | + +## 驱动力 + +原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定: + +```math +z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z) +``` + +当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。 + +## 动力学行为 + +原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。 + +## 使用方法 + +```bash +cd examples/case06 +python run_dynamics.py +``` + +配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`。 diff --git a/examples/case07/doc/index.html b/examples/case07/doc/index.html new file mode 100644 index 0000000..c50710e --- /dev/null +++ b/examples/case07/doc/index.html @@ -0,0 +1,477 @@ + + + + + +case06 — 一维原子链驱动力学模拟 | 物理原理 & 使用文档 + + + + + + + +
+

一维原子链驱动力学模拟

+

120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动

+ case06 · examples/case06 +
+ +
+ + + + +
+

目录

+
    +
  1. 物理原理
  2. +
  3. 数值算法
  4. +
  5. 驱动力模型
  6. +
  7. 使用方法
  8. +
  9. 参数参考
  10. +
  11. 文件结构
  12. +
  13. 常见问题
  14. +
+
+ + + + +
+

一、物理原理

+ +
+

1.1 一维原子链

+

120 个原子沿 x 轴 等间距排列,原子间距为 1。相邻原子之间用 理想弹簧 连接,弹簧的劲度系数 k = 1.0,原长 L₀ = 1.0(与原子间距一致,初始状态弹簧无拉伸)。

+

每个原子被限制在 z 方向 自由振动,x 和 y 方向锁定(fix_x=1, fix_y=1, fix_z=0)。

+
+ +
+

1.2 弹簧力(胡克定律)

+

当原子 ij 之间有弹簧连接时,原子 i 受到的弹簧力为:

+
+ F = −k · (dL₀) · uij +
+

其中 d = |rjri| 为两原子间距离,uij 为从 i 指向 j 的单位向量。由于原子只在 z 方向振动,弹簧在 z 方向的分量是 几何非线性 的——对于小振幅近似,z 方向等效于一个三次方恢复力(FPU 型非线性)。

+
+ +
+

1.3 运动方程

+

对于第 i 个自由原子(非受驱),牛顿第二定律给出:

+
+ m · ai = Fispring + Fidriving +
+

本案例中 唯一的外力 来自驱动力(仅施加于原子 1)。无重力、无万有引力、无阻尼,系统总能量守恒。

+
+ +
+

1.4 波传播

+

原子 1 的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的 横波。由于横向振动的几何非线性(弹簧大部分张力在 x 方向,z 方向的有效刚度远小于 1),波的传播速度较慢,且高阶频率成分会在链中产生复杂的非线性动力学行为(类似 FPU 回波现象)。

+
+
+ + + + +
+

二、数值算法

+ +
+

2.1 蛙跳法(Leapfrog / Velocity-Verlet)

+

采用能量守恒特性优异的 蛙跳法(二阶辛积分器),更新公式为:

+
+ v(t + ½Δt) = v(t) + ½ a(t) · Δt
+ r(t + Δt) = r(t) + v(t + ½Δt) · Δt
+ a(t + Δt) = F(r(t + Δt), v(t + ½Δt)) / m
+ v(t + Δt) = v(t + ½Δt) + ½ a(t + Δt) · Δt +
+

蛙跳法在长时间模拟中能量漂移极小(本案例验证 < 0.004%),适合无阻尼的保守系统。

+
+ +
+

2.2 时间步长与采样

+ + + + + + +
参数说明
DT0.01 s积分步长(远小于 1/ω ≈ 0.16 s,满足稳定性条件)
T_total100 s总模拟时间 → NT = 10000 步
NSTEP50每 NSTEP 步取一帧用于动画 → 200 帧
methodleapfrog蛙跳法(Velocity-Verlet)
+
+ +
+

2.3 计算流程

+
+ 读入 coord.txt
connection.txt
bond.txt
+ + 施加驱动力
(驱动原子 1)
+ + 记录轨迹 + + 蛙跳法
更新位置/速度
+ + 固定约束
(x, y 锁定)
+ + 循环
NT 次
+
+

注意:驱动力在 每次积分前 施加,确保受驱原子的位置正确传递给弹簧力计算。

+
+
+ + + + +
+

三、驱动力模型

+ +
+

3.1 定义文件

+

驱动力由 input/driver.txt 定义,格式如下:

+
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
+1     0     0     5      0      0      1     0     0    90    all
+
+ +
+

3.2 数学公式

+

受驱原子的位置由下式决定(完全替换 coord.txt 中的初始坐标和固定约束):

+
+ r(t) = A · cos(2πf · t + φ) +
+

速度由解析导数给出:

+
+ v(t) = −A · 2πf · sin(2πf · t + φ) +
+

其中 A = (amp_x, amp_y, amp_z),f = (freq_x, freq_y, freq_z) 为不同方向的驱动频率,φ = (phi_x, phi_y, phi_z) 为相位(角度制,代码自动转换为弧度)。

+
+ +
+

3.3 本案例驱动参数

+ + + + + + +
参数含义
amp_z5.0z 方向驱动振幅
freq_z1.0 Hz驱动频率(周期 1 s)
phi_z90°驱动相位 → z(0) = 5·cos(90°) = 0
periodall全程驱动,永不停止
+
+ z(t) = 5.0 · cos(2π · 1.0 · t + 90°) +
+
+ +
+

3.4 有限周期驱动

+

period 参数支持三种模式:

+
    +
  • all — 全程驱动
  • +
  • 数值 — 驱动指定周期数后 静止(冻结在最终位置,速度归零)。例如 period: 1 表示驱动 1 个完整周期后停止。
  • +
+
+ +
+

3.5 驱动与固定约束的关系

+

对于受驱原子(driver.txtn 指定的原子),其在 coord.txt 中的初始坐标和 fix_x/fix_y/fix_z 约束被 完全忽略。原子的位置和速度完全由驱动力公式决定。

+
+
+ + + + +
+

四、使用方法

+ +
+

4.1 完整运行(模拟 + 动画)

+
cd examples/case06
+python run_dynamics.py
+

这步会依次执行:物理模拟 → 抽帧 → 打开 VisPy 3D 动画窗口。

+
+ +
+

4.2 仅查看已有结果

+

如果已经跑完模拟且生成了 output/display.txt,可以通过修改 input.txt 跳过计算,只开动画:

+
step_simulate:  0   # 跳过模拟
+step_sample:    0   # 跳过抽帧
+step_animation: 1   # 播放动画
+

然后运行:python run_dynamics.py

+
+ +
+

4.3 手动 3D 动画

+

也可以单独启动 VisPy 窗口:

+
python ../../draw.py output/
+
+ +
+

4.4 强制重新计算

+

修改参数后需要重新运行模拟时,设置:

+
force_calc: 1        # 忽略缓存,强制重新计算
+
+ +
+

4.5 动画交互

+ + + + + + + + + + + + +
操作效果
鼠标拖动旋转视角
滚轮缩放
W / S 键相机沿 Z 轴向前 / 向后移动(靠近/远离场景)
A / D 键视角向右 / 向左平移
E / Q 键视角上升 / 下降(屏幕方向)
C / X 键增大 / 减小步长
V 键切换透视 / 正交投影
左上角 reset 按钮复位视角到初始位置
左上角 info 按钮切换信息面板显示/隐藏
左上角 axes 按钮切换坐标轴显示/隐藏
+
+
+ + + + +
+

五、参数参考

+ +
+

5.1 input.txt 关键参数

+ + + + + + + + + + + + + +
参数默认值说明
gravity_field0均匀重力场(已关闭)
gravity_interaction0原子间万有引力(已关闭)
elastic_force1弹簧键力(已开启)
damping_force0阻尼(已关闭)
driving_force1驱动力开关(1=开启,需 driver.txt)
methodleapfrog数值积分方法
DT0.01积分步长 (s)
T_total100.0总模拟时间 (s)
NSTEP50抽帧步数间隔
enginepython计算引擎(python / c / cpp / fortran)
use_marker1渲染模式(0=Sphere 网格, 1=Marker GPU 实例化)
+
+ +
+

5.2 流程控制参数

+ + + + + + + + +
参数01
step_simulate跳过模拟(加载已有轨迹)运行物理模拟
step_sample跳过抽帧从轨迹抽取显示帧
step_plot不生成图表生成轨迹/能量图
step_plot_wave不生成波形图生成波形能量动画 GIF
step_animation不启动动画自动打开 VisPy 3D 窗口
force_calc自动检测缓存强制重新计算
+
+
+ + + + +
+

六、文件结构

+ +
case06/
+├── input/
+│   ├── input.txt          # 主配置文件(YAML 格式)
+│   ├── coord.txt          # 原子坐标(120 个原子)
+│   ├── connection.txt     # 弹簧连接关系(59 条键)
+│   ├── bond.txt           # 弹簧参数(k=1.0, L₀=1.0)
+│   └── driver.txt       # 驱动力定义(本案例新增)
+├── output/
+│   ├── trajectory.txt     # 全量轨迹数据(50000 步 × 120 原子)
+│   ├── display.txt        # 抽帧后的动画数据(500 帧 × 120 原子)
+│   ├── dynamics.log       # 计算日志
+│   ├── animation.log      # 动画启动日志(闪退时排查用)
+│   └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
+├── doc/
+│   └── index.html         # 本文档
+├── Readme.md              # 案例简介
+└── run_dynamics.py        # 案例运行入口
+
+ + + + +
+

七、常见问题

+ +
+

7.1 动画窗口闪退

+

如果 VisPy 窗口一闪就消失,请检查:

+
    +
  • output/animation.log 中是否有错误信息
  • +
  • output/display.txt 是否存在(需先跑 step_sample: 1
  • +
+
+ +
+

7.2 原子不振动

+

可能原因:

+
    +
  • NSTEP 过大:抽帧间隔大于驱动周期的一半时,动画会丢失振动细节。建议 NSTEP ≤ 1/(freq · DT · 10)
  • +
  • 相位 φ 使采样点落在零值:试试 phi_z: 0 让原子在 t=0 处于振幅峰值
  • +
  • 确认 driving_force: 1driver.txt 中 amp_z 不为 0
  • +
+
+ +
+

7.3 渲染性能慢

+

原子数多时动画卡顿:

+
    +
  • 设置 use_marker: 1(使用 GPU 实例化渲染替代独立网格球体)
  • +
  • 增大 NSTEP 减少动画帧数
  • +
+
+
+ +
+ +
+ Dynamics Simulation Framework  ·  生成于 2026-06-10 +
+ +
+ + diff --git a/examples/case07/input/bond.txt b/examples/case07/input/bond.txt new file mode 100644 index 0000000..ad95690 --- /dev/null +++ b/examples/case07/input/bond.txt @@ -0,0 +1,2 @@ +bond_name k rest_length +k1 500.0 1.0 diff --git a/examples/case07/input/connection.txt b/examples/case07/input/connection.txt new file mode 100644 index 0000000..e57b7a3 --- /dev/null +++ b/examples/case07/input/connection.txt @@ -0,0 +1,120 @@ +n1 n2 bond_name +1 2 k1 +2 3 k1 +3 4 k1 +4 5 k1 +5 6 k1 +6 7 k1 +7 8 k1 +8 9 k1 +9 10 k1 +10 11 k1 +11 12 k1 +12 13 k1 +13 14 k1 +14 15 k1 +15 16 k1 +16 17 k1 +17 18 k1 +18 19 k1 +19 20 k1 +20 21 k1 +21 22 k1 +22 23 k1 +23 24 k1 +24 25 k1 +25 26 k1 +26 27 k1 +27 28 k1 +28 29 k1 +29 30 k1 +30 31 k1 +31 32 k1 +32 33 k1 +33 34 k1 +34 35 k1 +35 36 k1 +36 37 k1 +37 38 k1 +38 39 k1 +39 40 k1 +40 41 k1 +41 42 k1 +42 43 k1 +43 44 k1 +44 45 k1 +45 46 k1 +46 47 k1 +47 48 k1 +48 49 k1 +49 50 k1 +50 51 k1 +51 52 k1 +52 53 k1 +53 54 k1 +54 55 k1 +55 56 k1 +56 57 k1 +57 58 k1 +58 59 k1 +59 60 k1 +60 61 k1 +61 62 k1 +62 63 k1 +63 64 k1 +64 65 k1 +65 66 k1 +66 67 k1 +67 68 k1 +68 69 k1 +69 70 k1 +70 71 k1 +71 72 k1 +72 73 k1 +73 74 k1 +74 75 k1 +75 76 k1 +76 77 k1 +77 78 k1 +78 79 k1 +79 80 k1 +80 81 k1 +81 82 k1 +82 83 k1 +83 84 k1 +84 85 k1 +85 86 k1 +86 87 k1 +87 88 k1 +88 89 k1 +89 90 k1 +90 91 k1 +91 92 k1 +92 93 k1 +93 94 k1 +94 95 k1 +95 96 k1 +96 97 k1 +97 98 k1 +98 99 k1 +99 100 k1 +100 101 k1 +101 102 k1 +102 103 k1 +103 104 k1 +104 105 k1 +105 106 k1 +106 107 k1 +107 108 k1 +108 109 k1 +109 110 k1 +110 111 k1 +111 112 k1 +112 113 k1 +113 114 k1 +114 115 k1 +115 116 k1 +116 117 k1 +117 118 k1 +118 119 k1 +119 120 k1 diff --git a/examples/case07/input/coord.txt b/examples/case07/input/coord.txt new file mode 100644 index 0000000..ae980a5 --- /dev/null +++ b/examples/case07/input/coord.txt @@ -0,0 +1,121 @@ +n mass radius x y z vx vy vz fix_x fix_y fix_z +1 1 0.1 0 0 0 0 0 0 0 1 0 +2 1 0.1 1 0 0 0 0 0 0 1 0 +3 1 0.1 2 0 0 0 0 0 0 1 0 +4 1 0.1 3 0 0 0 0 0 0 1 0 +5 1 0.1 4 0 0 0 0 0 0 1 0 +6 1 0.1 5 0 0 0 0 0 0 1 0 +7 1 0.1 6 0 0 0 0 0 0 1 0 +8 1 0.1 7 0 0 0 0 0 0 1 0 +9 1 0.1 8 0 0 0 0 0 0 1 0 +10 1 0.1 9 0 0 0 0 0 0 1 0 +11 1 0.1 10 0 0 0 0 0 0 1 0 +12 1 0.1 11 0 0 0 0 0 0 1 0 +13 1 0.1 12 0 0 0 0 0 0 1 0 +14 1 0.1 13 0 0 0 0 0 0 1 0 +15 1 0.1 14 0 0 0 0 0 0 1 0 +16 1 0.1 15 0 0 0 0 0 0 1 0 +17 1 0.1 16 0 0 0 0 0 0 1 0 +18 1 0.1 17 0 0 0 0 0 0 1 0 +19 1 0.1 18 0 0 0 0 0 0 1 0 +20 1 0.1 19 0 0 0 0 0 0 1 0 +21 1 0.1 20 0 0 0 0 0 0 1 0 +22 1 0.1 21 0 0 0 0 0 0 1 0 +23 1 0.1 22 0 0 0 0 0 0 1 0 +24 1 0.1 23 0 0 0 0 0 0 1 0 +25 1 0.1 24 0 0 0 0 0 0 1 0 +26 1 0.1 25 0 0 0 0 0 0 1 0 +27 1 0.1 26 0 0 0 0 0 0 1 0 +28 1 0.1 27 0 0 0 0 0 0 1 0 +29 1 0.1 28 0 0 0 0 0 0 1 0 +30 1 0.1 29 0 0 0 0 0 0 1 0 +31 1 0.1 30 0 0 0 0 0 0 1 0 +32 1 0.1 31 0 0 0 0 0 0 1 0 +33 1 0.1 32 0 0 0 0 0 0 1 0 +34 1 0.1 33 0 0 0 0 0 0 1 0 +35 1 0.1 34 0 0 0 0 0 0 1 0 +36 1 0.1 35 0 0 0 0 0 0 1 0 +37 1 0.1 36 0 0 0 0 0 0 1 0 +38 1 0.1 37 0 0 0 0 0 0 1 0 +39 1 0.1 38 0 0 0 0 0 0 1 0 +40 1 0.1 39 0 0 0 0 0 0 1 0 +41 1 0.1 40 0 0 0 0 0 0 1 0 +42 1 0.1 41 0 0 0 0 0 0 1 0 +43 1 0.1 42 0 0 0 0 0 0 1 0 +44 1 0.1 43 0 0 0 0 0 0 1 0 +45 1 0.1 44 0 0 0 0 0 0 1 0 +46 1 0.1 45 0 0 0 0 0 0 1 0 +47 1 0.1 46 0 0 0 0 0 0 1 0 +48 1 0.1 47 0 0 0 0 0 0 1 0 +49 1 0.1 48 0 0 0 0 0 0 1 0 +50 1 0.1 49 0 0 0 0 0 0 1 0 +51 1 0.1 50 0 0 0 0 0 0 1 0 +52 1 0.1 51 0 0 0 0 0 0 1 0 +53 1 0.1 52 0 0 0 0 0 0 1 0 +54 1 0.1 53 0 0 0 0 0 0 1 0 +55 1 0.1 54 0 0 0 0 0 0 1 0 +56 1 0.1 55 0 0 0 0 0 0 1 0 +57 1 0.1 56 0 0 0 0 0 0 1 0 +58 1 0.1 57 0 0 0 0 0 0 1 0 +59 1 0.1 58 0 0 0 0 0 0 1 0 +60 1 0.1 59 0 0 0 0 0 0 1 0 +61 1 0.1 60 0 0 0 0 0 0 1 0 +62 1 0.1 61 0 0 0 0 0 0 1 0 +63 1 0.1 62 0 0 0 0 0 0 1 0 +64 1 0.1 63 0 0 0 0 0 0 1 0 +65 1 0.1 64 0 0 0 0 0 0 1 0 +66 1 0.1 65 0 0 0 0 0 0 1 0 +67 1 0.1 66 0 0 0 0 0 0 1 0 +68 1 0.1 67 0 0 0 0 0 0 1 0 +69 1 0.1 68 0 0 0 0 0 0 1 0 +70 1 0.1 69 0 0 0 0 0 0 1 0 +71 1 0.1 70 0 0 0 0 0 0 1 0 +72 1 0.1 71 0 0 0 0 0 0 1 0 +73 1 0.1 72 0 0 0 0 0 0 1 0 +74 1 0.1 73 0 0 0 0 0 0 1 0 +75 1 0.1 74 0 0 0 0 0 0 1 0 +76 1 0.1 75 0 0 0 0 0 0 1 0 +77 1 0.1 76 0 0 0 0 0 0 1 0 +78 1 0.1 77 0 0 0 0 0 0 1 0 +79 1 0.1 78 0 0 0 0 0 0 1 0 +80 1 0.1 79 0 0 0 0 0 0 1 0 +81 1 0.1 80 0 0 0 0 0 0 1 0 +82 1 0.1 81 0 0 0 0 0 0 1 0 +83 1 0.1 82 0 0 0 0 0 0 1 0 +84 1 0.1 83 0 0 0 0 0 0 1 0 +85 1 0.1 84 0 0 0 0 0 0 1 0 +86 1 0.1 85 0 0 0 0 0 0 1 0 +87 1 0.1 86 0 0 0 0 0 0 1 0 +88 1 0.1 87 0 0 0 0 0 0 1 0 +89 1 0.1 88 0 0 0 0 0 0 1 0 +90 1 0.1 89 0 0 0 0 0 0 1 0 +91 1 0.1 90 0 0 0 0 0 0 1 0 +92 1 0.1 91 0 0 0 0 0 0 1 0 +93 1 0.1 92 0 0 0 0 0 0 1 0 +94 1 0.1 93 0 0 0 0 0 0 1 0 +95 1 0.1 94 0 0 0 0 0 0 1 0 +96 1 0.1 95 0 0 0 0 0 0 1 0 +97 1 0.1 96 0 0 0 0 0 0 1 0 +98 1 0.1 97 0 0 0 0 0 0 1 0 +99 1 0.1 98 0 0 0 0 0 0 1 0 +100 1 0.1 99 0 0 0 0 0 0 1 0 +101 1 0.1 100 0 0 0 0 0 0 1 0 +102 1 0.1 101 0 0 0 0 0 0 1 0 +103 1 0.1 102 0 0 0 0 0 0 1 0 +104 1 0.1 103 0 0 0 0 0 0 1 0 +105 1 0.1 104 0 0 0 0 0 0 1 0 +106 1 0.1 105 0 0 0 0 0 0 1 0 +107 1 0.1 106 0 0 0 0 0 0 1 0 +108 1 0.1 107 0 0 0 0 0 0 1 0 +109 1 0.1 108 0 0 0 0 0 0 1 0 +110 1 0.1 109 0 0 0 0 0 0 1 0 +111 1 0.1 110 0 0 0 0 0 0 1 0 +112 1 0.1 111 0 0 0 0 0 0 1 0 +113 1 0.1 112 0 0 0 0 0 0 1 0 +114 1 0.1 113 0 0 0 0 0 0 1 0 +115 1 0.1 114 0 0 0 0 0 0 1 0 +116 1 0.1 115 0 0 0 0 0 0 1 0 +117 1 0.1 116 0 0 0 0 0 0 1 0 +118 1 0.1 117 0 0 0 0 0 0 1 0 +119 1 0.1 118 0 0 0 0 0 0 1 0 +120 1 0.1 119 0 0 0 0 0 1 1 1 diff --git a/examples/case07/input/driver.txt b/examples/case07/input/driver.txt new file mode 100644 index 0000000..3670c67 --- /dev/null +++ b/examples/case07/input/driver.txt @@ -0,0 +1,3 @@ + n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period + 1 0 0 0.1 0 0 0.01333 0 0 90 all +120 0 0 0.1 0 0 0.01333 0 0 90 all diff --git a/examples/case07/input/input.txt b/examples/case07/input/input.txt new file mode 100644 index 0000000..8f12ef5 --- /dev/null +++ b/examples/case07/input/input.txt @@ -0,0 +1,114 @@ +# 物理模拟参数配置 +# 格式:YAML +# 用法:python run_dynamics.py + +# ── 流程控制 ────────────────────────────────── +# 每步用 0/1 单独开关,1=执行,0=跳过 +# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧 +step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧) +step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行 +step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png +step_animation: 0 # 自动播放 VisPy 3D 动画窗口(需安装 vispy) +step_plot_wave: 1 # 绘制波形能量动画 +force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出 +plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1) +plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1) + +# ── 文件保存 ────────────────────────────────── +save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧) + +# ── 计算引擎 ────────────────────────────────── +# 可选: python, c, cpp, fortran, java +engine: c # 默认使用 python 引擎 + +# ── 盒子 ────────────────────────────────────── +box_a: 300.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 + +# ── 初始构型 ────────────────────────────────── +# 坐标文件格式: +# 第一行:n mass radius x y z vx vy vz fix_x fix_y fix_z +# 后续行:原子序号 质量 半径 x y z vx vy vz fix_x fix_y fix_z +coord_file: input/coord.txt +connection_file: input/connection.txt +bond_file: input/bond.txt +driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效) + +# 绘图/动画展示的原子序号(对应 coord_file 第一列 n) +plot_atom: 1 + +# ── 物理参数 ────────────────────────────────── +# 三个方向分量分别对应 x, y, z +G: [0.000, 0.000, 0.000] # 重力场分量 (m/s²) +B: [0.005, 0.000, 0.005] # 阻尼分量 + +# ── 力开关(0=关闭, 1=开启)────────────────── +gravity_field: 0 # 均匀重力场 (G) +gravity_interaction: 0 # 原子间万有引力 +elastic_force: 1 # 弹簧键力 +damping_force: 1 # 阻尼 (B) +driving_force: 1 # 驱动力(需 driver_file 定义) +# +gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效) + +# ── 数值算法 ────────────────────────────────── +# 可选: +# explicit_euler 显式欧拉法 +# implicit_euler 隐式欧拉法 +# midpoint 中点法 +# leapfrog 蛙跳法 +method: leapfrog + +# ── 步骤控制 ────────────────────────────────── +# 以下参数控制哪些步骤被执行和保存 + +# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态) +warmup_steps: 0 # 默认 0(立即开始记录) + +# 总模拟时间(秒),程序自动计算 NT = T_total / DT +# 如果同时指定了 NT,以 NT 为准 +T_total: 1000.0 + +# 抽帧间隔(每 NSTEP 步取一帧用于动画) +NSTEP: 500 + +# ── 时间步长 ────────────────────────────────── +DT: 0.001 # 时间步长 (s) + +# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 +sample_start: null # null 表示从头开始(帧索引从 0 起) +sample_end: null # null 表示到末尾 + + + +# ── 渲染方式 ────────────────────────────────── +# 3D 动画中原子渲染方式: +# 0 = Sphere (网格球体,效果精细,原子数少时推荐) +# 1 = Marker (GPU 实例化点,原子数多时性能更佳) +use_marker: 1 + +# ── 显示参数 ────────────────────────────────── +# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 +alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + +# 小球颜色 +# 小球半径从 coord_file 的 radius 列读取 +ball_color_r: 0.20 # R 分量 (0~1) +ball_color_g: 0.60 # G 分量 +ball_color_b: 0.90 # B 分量 + +# 盒子面颜色 +box_color_r: 0.80 +box_color_g: 0.80 +box_color_b: 0.85 + +# ── 摄像机初始位置 ──────────────────────────── +camera_distance: 120.0 # 摄像机到场景中心的距离 +camera_elevation: 0.0 # 俯仰角(度),负值=俯视 +camera_azimuth: 0.0 # 方位角(度) +camera_center_x: 60.0 # 摄像机注视点 x +camera_center_y: 0.0 # 摄像机注视点 y +camera_center_z: 0.0 # 摄像机注视点 z +move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动 + +# ── 视觉放大 ────────────────────────────────── +display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理) diff --git a/examples/case07/input/move_camera.txt b/examples/case07/input/move_camera.txt new file mode 100644 index 0000000..37d5c88 --- /dev/null +++ b/examples/case07/input/move_camera.txt @@ -0,0 +1,9 @@ +# move_camera.txt — 摄像机速度段驱动 +# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d +# vx/vy/vz: 平移速度(每帧移动单位) +# rx/ry/rz: 旋转速度(每帧度数) +# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留) +# +# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转 +all vx=0.02 +# 30-90 vy=0.02 ry=1 rz=1 diff --git a/examples/case07/run_dynamics.py b/examples/case07/run_dynamics.py new file mode 100644 index 0000000..3a3f2b5 --- /dev/null +++ b/examples/case07/run_dynamics.py @@ -0,0 +1,54 @@ +""" +Case runner for Dynamics case06 — 1D atomic chain (transverse wave). + +This script keeps program and data separated: + - program: ../../dynamics.py + - input: ./input + - output: ./output +""" + +from __future__ import annotations + +import argparse +import importlib.util +from pathlib import Path + + +CASE_DIR = Path(__file__).resolve().parent +DYNAMICS_PATH = Path("..") / ".." / "dynamics.py" +INPUT_DIR = Path("input") +OUTPUT_DIR = Path("output") +CONFIG_FILE = INPUT_DIR / "input.txt" + + +def load_dynamics_module(module_path: Path): + spec = importlib.util.spec_from_file_location("dynamics_module", module_path) + if spec is None or spec.loader is None: + raise ImportError(f"无法加载 dynamics.py: {module_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(): + parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case06") + parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图") + args = parser.parse_args() + + dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve() + input_dir = (CASE_DIR / INPUT_DIR).resolve() + output_dir = (CASE_DIR / OUTPUT_DIR).resolve() + config_path = (CASE_DIR / CONFIG_FILE).resolve() + + module = load_dynamics_module(dynamics_path) + module.run_case( + config_path=config_path, + runtime_base=CASE_DIR, + input_dir=input_dir, + output_dir=output_dir, + no_plot=args.no_plot, + ) + + +if __name__ == "__main__": + main() diff --git a/examples/case08/Readme.md b/examples/case08/Readme.md new file mode 100644 index 0000000..151e74c --- /dev/null +++ b/examples/case08/Readme.md @@ -0,0 +1,40 @@ +# case06: 一维原子链横波模拟 + +60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。 + +## 物理设定 + +| 参数 | 值 | +|---|---| +| 原子数 | 120 | +| 排列 | 沿 x 轴等间距排列,间距为 1 | +| 约束 | 原子**沿 z 方向自由振动**(fix_x=1, fix_y=1, fix_z=0),x, y 锁定 | +| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 | +| 重力 | 无 | +| 万有引力 | 无 | +| 阻尼 | 无 | +| 驱动力 | 原子 1(z 方向驱动) | +| 算法 | leapfrog(蛙跳法,能量守恒) | + +## 驱动力 + +原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定: + +```math +z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z) +``` + +当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。 + +## 动力学行为 + +原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。 + +## 使用方法 + +```bash +cd examples/case06 +python run_dynamics.py +``` + +配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`。 diff --git a/examples/case08/doc/index.html b/examples/case08/doc/index.html new file mode 100644 index 0000000..c50710e --- /dev/null +++ b/examples/case08/doc/index.html @@ -0,0 +1,477 @@ + + + + + +case06 — 一维原子链驱动力学模拟 | 物理原理 & 使用文档 + + + + + + + +
+

一维原子链驱动力学模拟

+

120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动

+ case06 · examples/case06 +
+ +
+ + + + +
+

目录

+
    +
  1. 物理原理
  2. +
  3. 数值算法
  4. +
  5. 驱动力模型
  6. +
  7. 使用方法
  8. +
  9. 参数参考
  10. +
  11. 文件结构
  12. +
  13. 常见问题
  14. +
+
+ + + + +
+

一、物理原理

+ +
+

1.1 一维原子链

+

120 个原子沿 x 轴 等间距排列,原子间距为 1。相邻原子之间用 理想弹簧 连接,弹簧的劲度系数 k = 1.0,原长 L₀ = 1.0(与原子间距一致,初始状态弹簧无拉伸)。

+

每个原子被限制在 z 方向 自由振动,x 和 y 方向锁定(fix_x=1, fix_y=1, fix_z=0)。

+
+ +
+

1.2 弹簧力(胡克定律)

+

当原子 ij 之间有弹簧连接时,原子 i 受到的弹簧力为:

+
+ F = −k · (dL₀) · uij +
+

其中 d = |rjri| 为两原子间距离,uij 为从 i 指向 j 的单位向量。由于原子只在 z 方向振动,弹簧在 z 方向的分量是 几何非线性 的——对于小振幅近似,z 方向等效于一个三次方恢复力(FPU 型非线性)。

+
+ +
+

1.3 运动方程

+

对于第 i 个自由原子(非受驱),牛顿第二定律给出:

+
+ m · ai = Fispring + Fidriving +
+

本案例中 唯一的外力 来自驱动力(仅施加于原子 1)。无重力、无万有引力、无阻尼,系统总能量守恒。

+
+ +
+

1.4 波传播

+

原子 1 的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的 横波。由于横向振动的几何非线性(弹簧大部分张力在 x 方向,z 方向的有效刚度远小于 1),波的传播速度较慢,且高阶频率成分会在链中产生复杂的非线性动力学行为(类似 FPU 回波现象)。

+
+
+ + + + +
+

二、数值算法

+ +
+

2.1 蛙跳法(Leapfrog / Velocity-Verlet)

+

采用能量守恒特性优异的 蛙跳法(二阶辛积分器),更新公式为:

+
+ v(t + ½Δt) = v(t) + ½ a(t) · Δt
+ r(t + Δt) = r(t) + v(t + ½Δt) · Δt
+ a(t + Δt) = F(r(t + Δt), v(t + ½Δt)) / m
+ v(t + Δt) = v(t + ½Δt) + ½ a(t + Δt) · Δt +
+

蛙跳法在长时间模拟中能量漂移极小(本案例验证 < 0.004%),适合无阻尼的保守系统。

+
+ +
+

2.2 时间步长与采样

+ + + + + + +
参数说明
DT0.01 s积分步长(远小于 1/ω ≈ 0.16 s,满足稳定性条件)
T_total100 s总模拟时间 → NT = 10000 步
NSTEP50每 NSTEP 步取一帧用于动画 → 200 帧
methodleapfrog蛙跳法(Velocity-Verlet)
+
+ +
+

2.3 计算流程

+
+ 读入 coord.txt
connection.txt
bond.txt
+ + 施加驱动力
(驱动原子 1)
+ + 记录轨迹 + + 蛙跳法
更新位置/速度
+ + 固定约束
(x, y 锁定)
+ + 循环
NT 次
+
+

注意:驱动力在 每次积分前 施加,确保受驱原子的位置正确传递给弹簧力计算。

+
+
+ + + + +
+

三、驱动力模型

+ +
+

3.1 定义文件

+

驱动力由 input/driver.txt 定义,格式如下:

+
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
+1     0     0     5      0      0      1     0     0    90    all
+
+ +
+

3.2 数学公式

+

受驱原子的位置由下式决定(完全替换 coord.txt 中的初始坐标和固定约束):

+
+ r(t) = A · cos(2πf · t + φ) +
+

速度由解析导数给出:

+
+ v(t) = −A · 2πf · sin(2πf · t + φ) +
+

其中 A = (amp_x, amp_y, amp_z),f = (freq_x, freq_y, freq_z) 为不同方向的驱动频率,φ = (phi_x, phi_y, phi_z) 为相位(角度制,代码自动转换为弧度)。

+
+ +
+

3.3 本案例驱动参数

+ + + + + + +
参数含义
amp_z5.0z 方向驱动振幅
freq_z1.0 Hz驱动频率(周期 1 s)
phi_z90°驱动相位 → z(0) = 5·cos(90°) = 0
periodall全程驱动,永不停止
+
+ z(t) = 5.0 · cos(2π · 1.0 · t + 90°) +
+
+ +
+

3.4 有限周期驱动

+

period 参数支持三种模式:

+
    +
  • all — 全程驱动
  • +
  • 数值 — 驱动指定周期数后 静止(冻结在最终位置,速度归零)。例如 period: 1 表示驱动 1 个完整周期后停止。
  • +
+
+ +
+

3.5 驱动与固定约束的关系

+

对于受驱原子(driver.txtn 指定的原子),其在 coord.txt 中的初始坐标和 fix_x/fix_y/fix_z 约束被 完全忽略。原子的位置和速度完全由驱动力公式决定。

+
+
+ + + + +
+

四、使用方法

+ +
+

4.1 完整运行(模拟 + 动画)

+
cd examples/case06
+python run_dynamics.py
+

这步会依次执行:物理模拟 → 抽帧 → 打开 VisPy 3D 动画窗口。

+
+ +
+

4.2 仅查看已有结果

+

如果已经跑完模拟且生成了 output/display.txt,可以通过修改 input.txt 跳过计算,只开动画:

+
step_simulate:  0   # 跳过模拟
+step_sample:    0   # 跳过抽帧
+step_animation: 1   # 播放动画
+

然后运行:python run_dynamics.py

+
+ +
+

4.3 手动 3D 动画

+

也可以单独启动 VisPy 窗口:

+
python ../../draw.py output/
+
+ +
+

4.4 强制重新计算

+

修改参数后需要重新运行模拟时,设置:

+
force_calc: 1        # 忽略缓存,强制重新计算
+
+ +
+

4.5 动画交互

+ + + + + + + + + + + + +
操作效果
鼠标拖动旋转视角
滚轮缩放
W / S 键相机沿 Z 轴向前 / 向后移动(靠近/远离场景)
A / D 键视角向右 / 向左平移
E / Q 键视角上升 / 下降(屏幕方向)
C / X 键增大 / 减小步长
V 键切换透视 / 正交投影
左上角 reset 按钮复位视角到初始位置
左上角 info 按钮切换信息面板显示/隐藏
左上角 axes 按钮切换坐标轴显示/隐藏
+
+
+ + + + +
+

五、参数参考

+ +
+

5.1 input.txt 关键参数

+ + + + + + + + + + + + + +
参数默认值说明
gravity_field0均匀重力场(已关闭)
gravity_interaction0原子间万有引力(已关闭)
elastic_force1弹簧键力(已开启)
damping_force0阻尼(已关闭)
driving_force1驱动力开关(1=开启,需 driver.txt)
methodleapfrog数值积分方法
DT0.01积分步长 (s)
T_total100.0总模拟时间 (s)
NSTEP50抽帧步数间隔
enginepython计算引擎(python / c / cpp / fortran)
use_marker1渲染模式(0=Sphere 网格, 1=Marker GPU 实例化)
+
+ +
+

5.2 流程控制参数

+ + + + + + + + +
参数01
step_simulate跳过模拟(加载已有轨迹)运行物理模拟
step_sample跳过抽帧从轨迹抽取显示帧
step_plot不生成图表生成轨迹/能量图
step_plot_wave不生成波形图生成波形能量动画 GIF
step_animation不启动动画自动打开 VisPy 3D 窗口
force_calc自动检测缓存强制重新计算
+
+
+ + + + +
+

六、文件结构

+ +
case06/
+├── input/
+│   ├── input.txt          # 主配置文件(YAML 格式)
+│   ├── coord.txt          # 原子坐标(120 个原子)
+│   ├── connection.txt     # 弹簧连接关系(59 条键)
+│   ├── bond.txt           # 弹簧参数(k=1.0, L₀=1.0)
+│   └── driver.txt       # 驱动力定义(本案例新增)
+├── output/
+│   ├── trajectory.txt     # 全量轨迹数据(50000 步 × 120 原子)
+│   ├── display.txt        # 抽帧后的动画数据(500 帧 × 120 原子)
+│   ├── dynamics.log       # 计算日志
+│   ├── animation.log      # 动画启动日志(闪退时排查用)
+│   └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
+├── doc/
+│   └── index.html         # 本文档
+├── Readme.md              # 案例简介
+└── run_dynamics.py        # 案例运行入口
+
+ + + + +
+

七、常见问题

+ +
+

7.1 动画窗口闪退

+

如果 VisPy 窗口一闪就消失,请检查:

+
    +
  • output/animation.log 中是否有错误信息
  • +
  • output/display.txt 是否存在(需先跑 step_sample: 1
  • +
+
+ +
+

7.2 原子不振动

+

可能原因:

+
    +
  • NSTEP 过大:抽帧间隔大于驱动周期的一半时,动画会丢失振动细节。建议 NSTEP ≤ 1/(freq · DT · 10)
  • +
  • 相位 φ 使采样点落在零值:试试 phi_z: 0 让原子在 t=0 处于振幅峰值
  • +
  • 确认 driving_force: 1driver.txt 中 amp_z 不为 0
  • +
+
+ +
+

7.3 渲染性能慢

+

原子数多时动画卡顿:

+
    +
  • 设置 use_marker: 1(使用 GPU 实例化渲染替代独立网格球体)
  • +
  • 增大 NSTEP 减少动画帧数
  • +
+
+
+ +
+ +
+ Dynamics Simulation Framework  ·  生成于 2026-06-10 +
+ +
+ + diff --git a/examples/case08/input/bond.txt b/examples/case08/input/bond.txt new file mode 100644 index 0000000..8645886 --- /dev/null +++ b/examples/case08/input/bond.txt @@ -0,0 +1,2 @@ +bond_name k rest_length +k1 0.1 1.0 diff --git a/examples/case08/input/connection.txt b/examples/case08/input/connection.txt new file mode 100644 index 0000000..2f8c03d --- /dev/null +++ b/examples/case08/input/connection.txt @@ -0,0 +1,2 @@ +n1 n2 bond_name +1 2 k1 \ No newline at end of file diff --git a/examples/case08/input/coord.txt b/examples/case08/input/coord.txt new file mode 100644 index 0000000..c804d33 --- /dev/null +++ b/examples/case08/input/coord.txt @@ -0,0 +1,3 @@ +n mass radius x y z vx vy vz fix_x fix_y fix_z +1 1 0.1 0 0 0 0 0 0 0 1 0 +2 1 0.1 1 0 0 0 0 0 0 1 0 \ No newline at end of file diff --git a/examples/case08/input/driver.txt b/examples/case08/input/driver.txt new file mode 100644 index 0000000..384c450 --- /dev/null +++ b/examples/case08/input/driver.txt @@ -0,0 +1,2 @@ + n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period + 1 0.1 0 0.0 0.02 0 0 90 0 0 all diff --git a/examples/case08/input/input.txt b/examples/case08/input/input.txt new file mode 100644 index 0000000..2214b3d --- /dev/null +++ b/examples/case08/input/input.txt @@ -0,0 +1,114 @@ +# 物理模拟参数配置 +# 格式:YAML +# 用法:python run_dynamics.py + +# ── 流程控制 ────────────────────────────────── +# 每步用 0/1 单独开关,1=执行,0=跳过 +# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧 +step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧) +step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行 +step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png +step_animation: 0 # 自动播放 VisPy 3D 动画窗口(需安装 vispy) +step_plot_wave: 1 # 绘制波形能量动画 +force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出 +plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1) +plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1) + +# ── 文件保存 ────────────────────────────────── +save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧) + +# ── 计算引擎 ────────────────────────────────── +# 可选: python, c, cpp, fortran, java +engine: c # 默认使用 python 引擎 + +# ── 盒子 ────────────────────────────────────── +box_a: 300.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 + +# ── 初始构型 ────────────────────────────────── +# 坐标文件格式: +# 第一行:n mass radius x y z vx vy vz fix_x fix_y fix_z +# 后续行:原子序号 质量 半径 x y z vx vy vz fix_x fix_y fix_z +coord_file: input/coord.txt +connection_file: input/connection.txt +bond_file: input/bond.txt +driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效) + +# 绘图/动画展示的原子序号(对应 coord_file 第一列 n) +plot_atom: 1 + +# ── 物理参数 ────────────────────────────────── +# 三个方向分量分别对应 x, y, z +G: [0.000, 0.000, 0.000] # 重力场分量 (m/s²) +B: [0.005, 0.000, 0.005] # 阻尼分量 + +# ── 力开关(0=关闭, 1=开启)────────────────── +gravity_field: 0 # 均匀重力场 (G) +gravity_interaction: 0 # 原子间万有引力 +elastic_force: 1 # 弹簧键力 +damping_force: 1 # 阻尼 (B) +driving_force: 1 # 驱动力(需 driver_file 定义) +# +gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效) + +# ── 数值算法 ────────────────────────────────── +# 可选: +# explicit_euler 显式欧拉法 +# implicit_euler 隐式欧拉法 +# midpoint 中点法 +# leapfrog 蛙跳法 +method: leapfrog + +# ── 步骤控制 ────────────────────────────────── +# 以下参数控制哪些步骤被执行和保存 + +# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态) +warmup_steps: 0 # 默认 0(立即开始记录) + +# 总模拟时间(秒),程序自动计算 NT = T_total / DT +# 如果同时指定了 NT,以 NT 为准 +T_total: 100.0 + +# 抽帧间隔(每 NSTEP 步取一帧用于动画) +NSTEP: 500 + +# ── 时间步长 ────────────────────────────────── +DT: 0.001 # 时间步长 (s) + +# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 +sample_start: null # null 表示从头开始(帧索引从 0 起) +sample_end: null # null 表示到末尾 + + + +# ── 渲染方式 ────────────────────────────────── +# 3D 动画中原子渲染方式: +# 0 = Sphere (网格球体,效果精细,原子数少时推荐) +# 1 = Marker (GPU 实例化点,原子数多时性能更佳) +use_marker: 1 + +# ── 显示参数 ────────────────────────────────── +# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 +alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + +# 小球颜色 +# 小球半径从 coord_file 的 radius 列读取 +ball_color_r: 0.20 # R 分量 (0~1) +ball_color_g: 0.60 # G 分量 +ball_color_b: 0.90 # B 分量 + +# 盒子面颜色 +box_color_r: 0.80 +box_color_g: 0.80 +box_color_b: 0.85 + +# ── 摄像机初始位置 ──────────────────────────── +camera_distance: 10.0 # 摄像机到场景中心的距离 +camera_elevation: 0.0 # 俯仰角(度),负值=俯视 +camera_azimuth: 0.0 # 方位角(度) +camera_center_x: 0.0 # 摄像机注视点 x +camera_center_y: 0.0 # 摄像机注视点 y +camera_center_z: 0.0 # 摄像机注视点 z +move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动 + +# ── 视觉放大 ────────────────────────────────── +display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理) diff --git a/examples/case08/input/move_camera.txt b/examples/case08/input/move_camera.txt new file mode 100644 index 0000000..37d5c88 --- /dev/null +++ b/examples/case08/input/move_camera.txt @@ -0,0 +1,9 @@ +# move_camera.txt — 摄像机速度段驱动 +# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d +# vx/vy/vz: 平移速度(每帧移动单位) +# rx/ry/rz: 旋转速度(每帧度数) +# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留) +# +# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转 +all vx=0.02 +# 30-90 vy=0.02 ry=1 rz=1 diff --git a/examples/case08/run_dynamics.py b/examples/case08/run_dynamics.py new file mode 100644 index 0000000..3a3f2b5 --- /dev/null +++ b/examples/case08/run_dynamics.py @@ -0,0 +1,54 @@ +""" +Case runner for Dynamics case06 — 1D atomic chain (transverse wave). + +This script keeps program and data separated: + - program: ../../dynamics.py + - input: ./input + - output: ./output +""" + +from __future__ import annotations + +import argparse +import importlib.util +from pathlib import Path + + +CASE_DIR = Path(__file__).resolve().parent +DYNAMICS_PATH = Path("..") / ".." / "dynamics.py" +INPUT_DIR = Path("input") +OUTPUT_DIR = Path("output") +CONFIG_FILE = INPUT_DIR / "input.txt" + + +def load_dynamics_module(module_path: Path): + spec = importlib.util.spec_from_file_location("dynamics_module", module_path) + if spec is None or spec.loader is None: + raise ImportError(f"无法加载 dynamics.py: {module_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(): + parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case06") + parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图") + args = parser.parse_args() + + dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve() + input_dir = (CASE_DIR / INPUT_DIR).resolve() + output_dir = (CASE_DIR / OUTPUT_DIR).resolve() + config_path = (CASE_DIR / CONFIG_FILE).resolve() + + module = load_dynamics_module(dynamics_path) + module.run_case( + config_path=config_path, + runtime_base=CASE_DIR, + input_dir=input_dir, + output_dir=output_dir, + no_plot=args.no_plot, + ) + + +if __name__ == "__main__": + main() diff --git a/examples/case09/Readme.md b/examples/case09/Readme.md new file mode 100644 index 0000000..151e74c --- /dev/null +++ b/examples/case09/Readme.md @@ -0,0 +1,40 @@ +# case06: 一维原子链横波模拟 + +60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。 + +## 物理设定 + +| 参数 | 值 | +|---|---| +| 原子数 | 120 | +| 排列 | 沿 x 轴等间距排列,间距为 1 | +| 约束 | 原子**沿 z 方向自由振动**(fix_x=1, fix_y=1, fix_z=0),x, y 锁定 | +| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 | +| 重力 | 无 | +| 万有引力 | 无 | +| 阻尼 | 无 | +| 驱动力 | 原子 1(z 方向驱动) | +| 算法 | leapfrog(蛙跳法,能量守恒) | + +## 驱动力 + +原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定: + +```math +z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z) +``` + +当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。 + +## 动力学行为 + +原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。 + +## 使用方法 + +```bash +cd examples/case06 +python run_dynamics.py +``` + +配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`。 diff --git a/examples/case09/doc/index.html b/examples/case09/doc/index.html new file mode 100644 index 0000000..c50710e --- /dev/null +++ b/examples/case09/doc/index.html @@ -0,0 +1,477 @@ + + + + + +case06 — 一维原子链驱动力学模拟 | 物理原理 & 使用文档 + + + + + + + +
+

一维原子链驱动力学模拟

+

120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动

+ case06 · examples/case06 +
+ +
+ + + + +
+

目录

+
    +
  1. 物理原理
  2. +
  3. 数值算法
  4. +
  5. 驱动力模型
  6. +
  7. 使用方法
  8. +
  9. 参数参考
  10. +
  11. 文件结构
  12. +
  13. 常见问题
  14. +
+
+ + + + +
+

一、物理原理

+ +
+

1.1 一维原子链

+

120 个原子沿 x 轴 等间距排列,原子间距为 1。相邻原子之间用 理想弹簧 连接,弹簧的劲度系数 k = 1.0,原长 L₀ = 1.0(与原子间距一致,初始状态弹簧无拉伸)。

+

每个原子被限制在 z 方向 自由振动,x 和 y 方向锁定(fix_x=1, fix_y=1, fix_z=0)。

+
+ +
+

1.2 弹簧力(胡克定律)

+

当原子 ij 之间有弹簧连接时,原子 i 受到的弹簧力为:

+
+ F = −k · (dL₀) · uij +
+

其中 d = |rjri| 为两原子间距离,uij 为从 i 指向 j 的单位向量。由于原子只在 z 方向振动,弹簧在 z 方向的分量是 几何非线性 的——对于小振幅近似,z 方向等效于一个三次方恢复力(FPU 型非线性)。

+
+ +
+

1.3 运动方程

+

对于第 i 个自由原子(非受驱),牛顿第二定律给出:

+
+ m · ai = Fispring + Fidriving +
+

本案例中 唯一的外力 来自驱动力(仅施加于原子 1)。无重力、无万有引力、无阻尼,系统总能量守恒。

+
+ +
+

1.4 波传播

+

原子 1 的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的 横波。由于横向振动的几何非线性(弹簧大部分张力在 x 方向,z 方向的有效刚度远小于 1),波的传播速度较慢,且高阶频率成分会在链中产生复杂的非线性动力学行为(类似 FPU 回波现象)。

+
+
+ + + + +
+

二、数值算法

+ +
+

2.1 蛙跳法(Leapfrog / Velocity-Verlet)

+

采用能量守恒特性优异的 蛙跳法(二阶辛积分器),更新公式为:

+
+ v(t + ½Δt) = v(t) + ½ a(t) · Δt
+ r(t + Δt) = r(t) + v(t + ½Δt) · Δt
+ a(t + Δt) = F(r(t + Δt), v(t + ½Δt)) / m
+ v(t + Δt) = v(t + ½Δt) + ½ a(t + Δt) · Δt +
+

蛙跳法在长时间模拟中能量漂移极小(本案例验证 < 0.004%),适合无阻尼的保守系统。

+
+ +
+

2.2 时间步长与采样

+ + + + + + +
参数说明
DT0.01 s积分步长(远小于 1/ω ≈ 0.16 s,满足稳定性条件)
T_total100 s总模拟时间 → NT = 10000 步
NSTEP50每 NSTEP 步取一帧用于动画 → 200 帧
methodleapfrog蛙跳法(Velocity-Verlet)
+
+ +
+

2.3 计算流程

+
+ 读入 coord.txt
connection.txt
bond.txt
+ + 施加驱动力
(驱动原子 1)
+ + 记录轨迹 + + 蛙跳法
更新位置/速度
+ + 固定约束
(x, y 锁定)
+ + 循环
NT 次
+
+

注意:驱动力在 每次积分前 施加,确保受驱原子的位置正确传递给弹簧力计算。

+
+
+ + + + +
+

三、驱动力模型

+ +
+

3.1 定义文件

+

驱动力由 input/driver.txt 定义,格式如下:

+
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
+1     0     0     5      0      0      1     0     0    90    all
+
+ +
+

3.2 数学公式

+

受驱原子的位置由下式决定(完全替换 coord.txt 中的初始坐标和固定约束):

+
+ r(t) = A · cos(2πf · t + φ) +
+

速度由解析导数给出:

+
+ v(t) = −A · 2πf · sin(2πf · t + φ) +
+

其中 A = (amp_x, amp_y, amp_z),f = (freq_x, freq_y, freq_z) 为不同方向的驱动频率,φ = (phi_x, phi_y, phi_z) 为相位(角度制,代码自动转换为弧度)。

+
+ +
+

3.3 本案例驱动参数

+ + + + + + +
参数含义
amp_z5.0z 方向驱动振幅
freq_z1.0 Hz驱动频率(周期 1 s)
phi_z90°驱动相位 → z(0) = 5·cos(90°) = 0
periodall全程驱动,永不停止
+
+ z(t) = 5.0 · cos(2π · 1.0 · t + 90°) +
+
+ +
+

3.4 有限周期驱动

+

period 参数支持三种模式:

+
    +
  • all — 全程驱动
  • +
  • 数值 — 驱动指定周期数后 静止(冻结在最终位置,速度归零)。例如 period: 1 表示驱动 1 个完整周期后停止。
  • +
+
+ +
+

3.5 驱动与固定约束的关系

+

对于受驱原子(driver.txtn 指定的原子),其在 coord.txt 中的初始坐标和 fix_x/fix_y/fix_z 约束被 完全忽略。原子的位置和速度完全由驱动力公式决定。

+
+
+ + + + +
+

四、使用方法

+ +
+

4.1 完整运行(模拟 + 动画)

+
cd examples/case06
+python run_dynamics.py
+

这步会依次执行:物理模拟 → 抽帧 → 打开 VisPy 3D 动画窗口。

+
+ +
+

4.2 仅查看已有结果

+

如果已经跑完模拟且生成了 output/display.txt,可以通过修改 input.txt 跳过计算,只开动画:

+
step_simulate:  0   # 跳过模拟
+step_sample:    0   # 跳过抽帧
+step_animation: 1   # 播放动画
+

然后运行:python run_dynamics.py

+
+ +
+

4.3 手动 3D 动画

+

也可以单独启动 VisPy 窗口:

+
python ../../draw.py output/
+
+ +
+

4.4 强制重新计算

+

修改参数后需要重新运行模拟时,设置:

+
force_calc: 1        # 忽略缓存,强制重新计算
+
+ +
+

4.5 动画交互

+ + + + + + + + + + + + +
操作效果
鼠标拖动旋转视角
滚轮缩放
W / S 键相机沿 Z 轴向前 / 向后移动(靠近/远离场景)
A / D 键视角向右 / 向左平移
E / Q 键视角上升 / 下降(屏幕方向)
C / X 键增大 / 减小步长
V 键切换透视 / 正交投影
左上角 reset 按钮复位视角到初始位置
左上角 info 按钮切换信息面板显示/隐藏
左上角 axes 按钮切换坐标轴显示/隐藏
+
+
+ + + + +
+

五、参数参考

+ +
+

5.1 input.txt 关键参数

+ + + + + + + + + + + + + +
参数默认值说明
gravity_field0均匀重力场(已关闭)
gravity_interaction0原子间万有引力(已关闭)
elastic_force1弹簧键力(已开启)
damping_force0阻尼(已关闭)
driving_force1驱动力开关(1=开启,需 driver.txt)
methodleapfrog数值积分方法
DT0.01积分步长 (s)
T_total100.0总模拟时间 (s)
NSTEP50抽帧步数间隔
enginepython计算引擎(python / c / cpp / fortran)
use_marker1渲染模式(0=Sphere 网格, 1=Marker GPU 实例化)
+
+ +
+

5.2 流程控制参数

+ + + + + + + + +
参数01
step_simulate跳过模拟(加载已有轨迹)运行物理模拟
step_sample跳过抽帧从轨迹抽取显示帧
step_plot不生成图表生成轨迹/能量图
step_plot_wave不生成波形图生成波形能量动画 GIF
step_animation不启动动画自动打开 VisPy 3D 窗口
force_calc自动检测缓存强制重新计算
+
+
+ + + + +
+

六、文件结构

+ +
case06/
+├── input/
+│   ├── input.txt          # 主配置文件(YAML 格式)
+│   ├── coord.txt          # 原子坐标(120 个原子)
+│   ├── connection.txt     # 弹簧连接关系(59 条键)
+│   ├── bond.txt           # 弹簧参数(k=1.0, L₀=1.0)
+│   └── driver.txt       # 驱动力定义(本案例新增)
+├── output/
+│   ├── trajectory.txt     # 全量轨迹数据(50000 步 × 120 原子)
+│   ├── display.txt        # 抽帧后的动画数据(500 帧 × 120 原子)
+│   ├── dynamics.log       # 计算日志
+│   ├── animation.log      # 动画启动日志(闪退时排查用)
+│   └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
+├── doc/
+│   └── index.html         # 本文档
+├── Readme.md              # 案例简介
+└── run_dynamics.py        # 案例运行入口
+
+ + + + +
+

七、常见问题

+ +
+

7.1 动画窗口闪退

+

如果 VisPy 窗口一闪就消失,请检查:

+
    +
  • output/animation.log 中是否有错误信息
  • +
  • output/display.txt 是否存在(需先跑 step_sample: 1
  • +
+
+ +
+

7.2 原子不振动

+

可能原因:

+
    +
  • NSTEP 过大:抽帧间隔大于驱动周期的一半时,动画会丢失振动细节。建议 NSTEP ≤ 1/(freq · DT · 10)
  • +
  • 相位 φ 使采样点落在零值:试试 phi_z: 0 让原子在 t=0 处于振幅峰值
  • +
  • 确认 driving_force: 1driver.txt 中 amp_z 不为 0
  • +
+
+ +
+

7.3 渲染性能慢

+

原子数多时动画卡顿:

+
    +
  • 设置 use_marker: 1(使用 GPU 实例化渲染替代独立网格球体)
  • +
  • 增大 NSTEP 减少动画帧数
  • +
+
+
+ +
+ +
+ Dynamics Simulation Framework  ·  生成于 2026-06-10 +
+ +
+ + diff --git a/examples/case09/input/bond.txt b/examples/case09/input/bond.txt new file mode 100644 index 0000000..30b4324 --- /dev/null +++ b/examples/case09/input/bond.txt @@ -0,0 +1,2 @@ +bond_name k rest_length +k1 300.0 1.0 diff --git a/examples/case09/input/connection.txt b/examples/case09/input/connection.txt new file mode 100644 index 0000000..1ae26e2 --- /dev/null +++ b/examples/case09/input/connection.txt @@ -0,0 +1,40 @@ +n1 n2 bond_name +1 2 k1 +2 3 k1 +3 4 k1 +4 5 k1 +5 6 k1 +6 7 k1 +7 8 k1 +8 9 k1 +9 10 k1 +10 11 k1 +11 12 k1 +12 13 k1 +13 14 k1 +14 15 k1 +15 16 k1 +16 17 k1 +17 18 k1 +18 19 k1 +19 20 k1 +20 21 k1 +21 22 k1 +22 23 k1 +23 24 k1 +24 25 k1 +25 26 k1 +26 27 k1 +27 28 k1 +28 29 k1 +29 30 k1 +30 31 k1 +31 32 k1 +32 33 k1 +33 34 k1 +34 35 k1 +35 36 k1 +36 37 k1 +37 38 k1 +38 39 k1 +39 40 k1 \ No newline at end of file diff --git a/examples/case09/input/coord.txt b/examples/case09/input/coord.txt new file mode 100644 index 0000000..f283167 --- /dev/null +++ b/examples/case09/input/coord.txt @@ -0,0 +1,41 @@ +n mass radius x y z vx vy vz fix_x fix_y fix_z +1 1 0.1 0 0 0 0 0 0 0 1 0 +2 1 0.1 1 0 0 0 0 0 0 1 0 +3 1 0.1 2 0 0 0 0 0 0 1 0 +4 1 0.1 3 0 0 0 0 0 0 1 0 +5 1 0.1 4 0 0 0 0 0 0 1 0 +6 1 0.1 5 0 0 0 0 0 0 1 0 +7 1 0.1 6 0 0 0 0 0 0 1 0 +8 1 0.1 7 0 0 0 0 0 0 1 0 +9 1 0.1 8 0 0 0 0 0 0 1 0 +10 1 0.1 9 0 0 0 0 0 0 1 0 +11 1 0.1 10 0 0 0 0 0 0 1 0 +12 1 0.1 11 0 0 0 0 0 0 1 0 +13 1 0.1 12 0 0 0 0 0 0 1 0 +14 1 0.1 13 0 0 0 0 0 0 1 0 +15 1 0.1 14 0 0 0 0 0 0 1 0 +16 1 0.1 15 0 0 0 0 0 0 1 0 +17 1 0.1 16 0 0 0 0 0 0 1 0 +18 1 0.1 17 0 0 0 0 0 0 1 0 +19 1 0.1 18 0 0 0 0 0 0 1 0 +20 1 0.1 19 0 0 0 0 0 0 1 0 +21 1 0.1 20 0 0 0 0 0 0 1 0 +22 1 0.1 21 0 0 0 0 0 0 1 0 +23 1 0.1 22 0 0 0 0 0 0 1 0 +24 1 0.1 23 0 0 0 0 0 0 1 0 +25 1 0.1 24 0 0 0 0 0 0 1 0 +26 1 0.1 25 0 0 0 0 0 0 1 0 +27 1 0.1 26 0 0 0 0 0 0 1 0 +28 1 0.1 27 0 0 0 0 0 0 1 0 +29 1 0.1 28 0 0 0 0 0 0 1 0 +30 1 0.1 29 0 0 0 0 0 0 1 0 +31 1 0.1 30 0 0 0 0 0 0 1 0 +32 1 0.1 31 0 0 0 0 0 0 1 0 +33 1 0.1 32 0 0 0 0 0 0 1 0 +34 1 0.1 33 0 0 0 0 0 0 1 0 +35 1 0.1 34 0 0 0 0 0 0 1 0 +36 1 0.1 35 0 0 0 0 0 0 1 0 +37 1 0.1 36 0 0 0 0 0 0 1 0 +38 1 0.1 37 0 0 0 0 0 0 1 0 +39 1 0.1 38 0 0 0 0 0 0 1 0 +40 1 0.1 39 0 0 0 0 0 1 1 1 \ No newline at end of file diff --git a/examples/case09/input/driver.txt b/examples/case09/input/driver.txt new file mode 100644 index 0000000..0931cc8 --- /dev/null +++ b/examples/case09/input/driver.txt @@ -0,0 +1,2 @@ + n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period + 1 0 0 0.1 0 0 0.04 0 0 90 all diff --git a/examples/case09/input/input.txt b/examples/case09/input/input.txt new file mode 100644 index 0000000..e34d1a5 --- /dev/null +++ b/examples/case09/input/input.txt @@ -0,0 +1,114 @@ +# 物理模拟参数配置 +# 格式:YAML +# 用法:python run_dynamics.py + +# ── 流程控制 ────────────────────────────────── +# 每步用 0/1 单独开关,1=执行,0=跳过 +# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧 +step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧) +step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行 +step_plot: 0 # 绘制轨迹/能量图 → output/trajectory_plots.png +step_animation: 1 # 自动播放 VisPy 3D 动画窗口(需安装 vispy) +step_plot_wave: 1 # 绘制波形能量动画 +force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出 +plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1) +plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1) + +# ── 文件保存 ────────────────────────────────── +save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧) + +# ── 计算引擎 ────────────────────────────────── +# 可选: python, c, cpp, fortran, java +engine: fortran # 默认使用 python 引擎 + +# ── 盒子 ────────────────────────────────────── +box_a: 300.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 + +# ── 初始构型 ────────────────────────────────── +# 坐标文件格式: +# 第一行:n mass radius x y z vx vy vz fix_x fix_y fix_z +# 后续行:原子序号 质量 半径 x y z vx vy vz fix_x fix_y fix_z +coord_file: input/coord.txt +connection_file: input/connection.txt +bond_file: input/bond.txt +driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效) + +# 绘图/动画展示的原子序号(对应 coord_file 第一列 n) +plot_atom: 1 + +# ── 物理参数 ────────────────────────────────── +# 三个方向分量分别对应 x, y, z +G: [0.000, 0.000, 0.000] # 重力场分量 (m/s²) +B: [0.005, 0.000, 0.005] # 阻尼分量 + +# ── 力开关(0=关闭, 1=开启)────────────────── +gravity_field: 0 # 均匀重力场 (G) +gravity_interaction: 0 # 原子间万有引力 +elastic_force: 1 # 弹簧键力 +damping_force: 0 # 阻尼 (B) +driving_force: 1 # 驱动力(需 driver_file 定义) +# +gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效) + +# ── 数值算法 ────────────────────────────────── +# 可选: +# explicit_euler 显式欧拉法 +# implicit_euler 隐式欧拉法 +# midpoint 中点法 +# leapfrog 蛙跳法 +method: leapfrog + +# ── 步骤控制 ────────────────────────────────── +# 以下参数控制哪些步骤被执行和保存 + +# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态) +warmup_steps: 0 # 默认 0(立即开始记录) + +# 总模拟时间(秒),程序自动计算 NT = T_total / DT +# 如果同时指定了 NT,以 NT 为准 +T_total: 200.0 + +# 抽帧间隔(每 NSTEP 步取一帧用于动画) +NSTEP: 100 + +# ── 时间步长 ────────────────────────────────── +DT: 0.001 # 时间步长 (s) + +# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 +sample_start: null # null 表示从头开始(帧索引从 0 起) +sample_end: null # null 表示到末尾 + + + +# ── 渲染方式 ────────────────────────────────── +# 3D 动画中原子渲染方式: +# 0 = Sphere (网格球体,效果精细,原子数少时推荐) +# 1 = Marker (GPU 实例化点,原子数多时性能更佳) +use_marker: 1 + +# ── 显示参数 ────────────────────────────────── +# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 +alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + +# 小球颜色 +# 小球半径从 coord_file 的 radius 列读取 +ball_color_r: 0.20 # R 分量 (0~1) +ball_color_g: 0.60 # G 分量 +ball_color_b: 0.90 # B 分量 + +# 盒子面颜色 +box_color_r: 0.80 +box_color_g: 0.80 +box_color_b: 0.85 + +# ── 摄像机初始位置 ──────────────────────────── +camera_distance: 60.0 # 摄像机到场景中心的距离 +camera_elevation: 0.0 # 俯仰角(度),负值=俯视 +camera_azimuth: 0.0 # 方位角(度) +camera_center_x: 30.0 # 摄像机注视点 x +camera_center_y: 0.0 # 摄像机注视点 y +camera_center_z: 0.0 # 摄像机注视点 z +move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动 + +# ── 视觉放大 ────────────────────────────────── +display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理) diff --git a/examples/case09/input/move_camera.txt b/examples/case09/input/move_camera.txt new file mode 100644 index 0000000..37d5c88 --- /dev/null +++ b/examples/case09/input/move_camera.txt @@ -0,0 +1,9 @@ +# move_camera.txt — 摄像机速度段驱动 +# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d +# vx/vy/vz: 平移速度(每帧移动单位) +# rx/ry/rz: 旋转速度(每帧度数) +# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留) +# +# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转 +all vx=0.02 +# 30-90 vy=0.02 ry=1 rz=1 diff --git a/examples/case09/run_dynamics.py b/examples/case09/run_dynamics.py new file mode 100644 index 0000000..3a3f2b5 --- /dev/null +++ b/examples/case09/run_dynamics.py @@ -0,0 +1,54 @@ +""" +Case runner for Dynamics case06 — 1D atomic chain (transverse wave). + +This script keeps program and data separated: + - program: ../../dynamics.py + - input: ./input + - output: ./output +""" + +from __future__ import annotations + +import argparse +import importlib.util +from pathlib import Path + + +CASE_DIR = Path(__file__).resolve().parent +DYNAMICS_PATH = Path("..") / ".." / "dynamics.py" +INPUT_DIR = Path("input") +OUTPUT_DIR = Path("output") +CONFIG_FILE = INPUT_DIR / "input.txt" + + +def load_dynamics_module(module_path: Path): + spec = importlib.util.spec_from_file_location("dynamics_module", module_path) + if spec is None or spec.loader is None: + raise ImportError(f"无法加载 dynamics.py: {module_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(): + parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case06") + parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图") + args = parser.parse_args() + + dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve() + input_dir = (CASE_DIR / INPUT_DIR).resolve() + output_dir = (CASE_DIR / OUTPUT_DIR).resolve() + config_path = (CASE_DIR / CONFIG_FILE).resolve() + + module = load_dynamics_module(dynamics_path) + module.run_case( + config_path=config_path, + runtime_base=CASE_DIR, + input_dir=input_dir, + output_dir=output_dir, + no_plot=args.no_plot, + ) + + +if __name__ == "__main__": + main() diff --git a/examples/case10/Readme.md b/examples/case10/Readme.md new file mode 100644 index 0000000..151e74c --- /dev/null +++ b/examples/case10/Readme.md @@ -0,0 +1,40 @@ +# case06: 一维原子链横波模拟 + +60 个原子沿 x 轴排列,相邻原子用弹簧连接。原子 1 受 z 方向驱动力作用,产生沿链传播的横波。 + +## 物理设定 + +| 参数 | 值 | +|---|---| +| 原子数 | 120 | +| 排列 | 沿 x 轴等间距排列,间距为 1 | +| 约束 | 原子**沿 z 方向自由振动**(fix_x=1, fix_y=1, fix_z=0),x, y 锁定 | +| 弹簧 | 劲度系数 k=1.0,原长 L₀=1.0 | +| 重力 | 无 | +| 万有引力 | 无 | +| 阻尼 | 无 | +| 驱动力 | 原子 1(z 方向驱动) | +| 算法 | leapfrog(蛙跳法,能量守恒) | + +## 驱动力 + +原子 1 的位置由 `input/driver.txt` 中的驱动力公式决定: + +```math +z(t) = A_z \cdot \cos(2\pi f_z t + \phi_z) +``` + +当前参数:A_z = 0.5, f_z = 0.1 Hz, φ_z = 90°, period = all(全程驱动)。 + +## 动力学行为 + +原子 1 沿 z 方向的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的**横波**。由于 z 方向的振动是横向的,弹簧大部分张力在 x 方向,z 方向的有效刚度是非线性的——等效于一个三次方恢复力(FPU 型非线性),因此波速较慢。 + +## 使用方法 + +```bash +cd examples/case06 +python run_dynamics.py +``` + +配置参数详见 `input/input.txt`,驱动力定义见 `input/driver.txt`,完整文档见 `doc/index.html`。 diff --git a/examples/case10/doc/index.html b/examples/case10/doc/index.html new file mode 100644 index 0000000..c50710e --- /dev/null +++ b/examples/case10/doc/index.html @@ -0,0 +1,477 @@ + + + + + +case06 — 一维原子链驱动力学模拟 | 物理原理 & 使用文档 + + + + + + + +
+

一维原子链驱动力学模拟

+

120 个原子沿 x 轴排列 · 弹簧连接 · z 方向受迫振动

+ case06 · examples/case06 +
+ +
+ + + + +
+

目录

+
    +
  1. 物理原理
  2. +
  3. 数值算法
  4. +
  5. 驱动力模型
  6. +
  7. 使用方法
  8. +
  9. 参数参考
  10. +
  11. 文件结构
  12. +
  13. 常见问题
  14. +
+
+ + + + +
+

一、物理原理

+ +
+

1.1 一维原子链

+

120 个原子沿 x 轴 等间距排列,原子间距为 1。相邻原子之间用 理想弹簧 连接,弹簧的劲度系数 k = 1.0,原长 L₀ = 1.0(与原子间距一致,初始状态弹簧无拉伸)。

+

每个原子被限制在 z 方向 自由振动,x 和 y 方向锁定(fix_x=1, fix_y=1, fix_z=0)。

+
+ +
+

1.2 弹簧力(胡克定律)

+

当原子 ij 之间有弹簧连接时,原子 i 受到的弹簧力为:

+
+ F = −k · (dL₀) · uij +
+

其中 d = |rjri| 为两原子间距离,uij 为从 i 指向 j 的单位向量。由于原子只在 z 方向振动,弹簧在 z 方向的分量是 几何非线性 的——对于小振幅近似,z 方向等效于一个三次方恢复力(FPU 型非线性)。

+
+ +
+

1.3 运动方程

+

对于第 i 个自由原子(非受驱),牛顿第二定律给出:

+
+ m · ai = Fispring + Fidriving +
+

本案例中 唯一的外力 来自驱动力(仅施加于原子 1)。无重力、无万有引力、无阻尼,系统总能量守恒。

+
+ +
+

1.4 波传播

+

原子 1 的受迫振动通过弹簧逐次传递给相邻原子,形成沿链传播的 横波。由于横向振动的几何非线性(弹簧大部分张力在 x 方向,z 方向的有效刚度远小于 1),波的传播速度较慢,且高阶频率成分会在链中产生复杂的非线性动力学行为(类似 FPU 回波现象)。

+
+
+ + + + +
+

二、数值算法

+ +
+

2.1 蛙跳法(Leapfrog / Velocity-Verlet)

+

采用能量守恒特性优异的 蛙跳法(二阶辛积分器),更新公式为:

+
+ v(t + ½Δt) = v(t) + ½ a(t) · Δt
+ r(t + Δt) = r(t) + v(t + ½Δt) · Δt
+ a(t + Δt) = F(r(t + Δt), v(t + ½Δt)) / m
+ v(t + Δt) = v(t + ½Δt) + ½ a(t + Δt) · Δt +
+

蛙跳法在长时间模拟中能量漂移极小(本案例验证 < 0.004%),适合无阻尼的保守系统。

+
+ +
+

2.2 时间步长与采样

+ + + + + + +
参数说明
DT0.01 s积分步长(远小于 1/ω ≈ 0.16 s,满足稳定性条件)
T_total100 s总模拟时间 → NT = 10000 步
NSTEP50每 NSTEP 步取一帧用于动画 → 200 帧
methodleapfrog蛙跳法(Velocity-Verlet)
+
+ +
+

2.3 计算流程

+
+ 读入 coord.txt
connection.txt
bond.txt
+ + 施加驱动力
(驱动原子 1)
+ + 记录轨迹 + + 蛙跳法
更新位置/速度
+ + 固定约束
(x, y 锁定)
+ + 循环
NT 次
+
+

注意:驱动力在 每次积分前 施加,确保受驱原子的位置正确传递给弹簧力计算。

+
+
+ + + + +
+

三、驱动力模型

+ +
+

3.1 定义文件

+

驱动力由 input/driver.txt 定义,格式如下:

+
n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period
+1     0     0     5      0      0      1     0     0    90    all
+
+ +
+

3.2 数学公式

+

受驱原子的位置由下式决定(完全替换 coord.txt 中的初始坐标和固定约束):

+
+ r(t) = A · cos(2πf · t + φ) +
+

速度由解析导数给出:

+
+ v(t) = −A · 2πf · sin(2πf · t + φ) +
+

其中 A = (amp_x, amp_y, amp_z),f = (freq_x, freq_y, freq_z) 为不同方向的驱动频率,φ = (phi_x, phi_y, phi_z) 为相位(角度制,代码自动转换为弧度)。

+
+ +
+

3.3 本案例驱动参数

+ + + + + + +
参数含义
amp_z5.0z 方向驱动振幅
freq_z1.0 Hz驱动频率(周期 1 s)
phi_z90°驱动相位 → z(0) = 5·cos(90°) = 0
periodall全程驱动,永不停止
+
+ z(t) = 5.0 · cos(2π · 1.0 · t + 90°) +
+
+ +
+

3.4 有限周期驱动

+

period 参数支持三种模式:

+
    +
  • all — 全程驱动
  • +
  • 数值 — 驱动指定周期数后 静止(冻结在最终位置,速度归零)。例如 period: 1 表示驱动 1 个完整周期后停止。
  • +
+
+ +
+

3.5 驱动与固定约束的关系

+

对于受驱原子(driver.txtn 指定的原子),其在 coord.txt 中的初始坐标和 fix_x/fix_y/fix_z 约束被 完全忽略。原子的位置和速度完全由驱动力公式决定。

+
+
+ + + + +
+

四、使用方法

+ +
+

4.1 完整运行(模拟 + 动画)

+
cd examples/case06
+python run_dynamics.py
+

这步会依次执行:物理模拟 → 抽帧 → 打开 VisPy 3D 动画窗口。

+
+ +
+

4.2 仅查看已有结果

+

如果已经跑完模拟且生成了 output/display.txt,可以通过修改 input.txt 跳过计算,只开动画:

+
step_simulate:  0   # 跳过模拟
+step_sample:    0   # 跳过抽帧
+step_animation: 1   # 播放动画
+

然后运行:python run_dynamics.py

+
+ +
+

4.3 手动 3D 动画

+

也可以单独启动 VisPy 窗口:

+
python ../../draw.py output/
+
+ +
+

4.4 强制重新计算

+

修改参数后需要重新运行模拟时,设置:

+
force_calc: 1        # 忽略缓存,强制重新计算
+
+ +
+

4.5 动画交互

+ + + + + + + + + + + + +
操作效果
鼠标拖动旋转视角
滚轮缩放
W / S 键相机沿 Z 轴向前 / 向后移动(靠近/远离场景)
A / D 键视角向右 / 向左平移
E / Q 键视角上升 / 下降(屏幕方向)
C / X 键增大 / 减小步长
V 键切换透视 / 正交投影
左上角 reset 按钮复位视角到初始位置
左上角 info 按钮切换信息面板显示/隐藏
左上角 axes 按钮切换坐标轴显示/隐藏
+
+
+ + + + +
+

五、参数参考

+ +
+

5.1 input.txt 关键参数

+ + + + + + + + + + + + + +
参数默认值说明
gravity_field0均匀重力场(已关闭)
gravity_interaction0原子间万有引力(已关闭)
elastic_force1弹簧键力(已开启)
damping_force0阻尼(已关闭)
driving_force1驱动力开关(1=开启,需 driver.txt)
methodleapfrog数值积分方法
DT0.01积分步长 (s)
T_total100.0总模拟时间 (s)
NSTEP50抽帧步数间隔
enginepython计算引擎(python / c / cpp / fortran)
use_marker1渲染模式(0=Sphere 网格, 1=Marker GPU 实例化)
+
+ +
+

5.2 流程控制参数

+ + + + + + + + +
参数01
step_simulate跳过模拟(加载已有轨迹)运行物理模拟
step_sample跳过抽帧从轨迹抽取显示帧
step_plot不生成图表生成轨迹/能量图
step_plot_wave不生成波形图生成波形能量动画 GIF
step_animation不启动动画自动打开 VisPy 3D 窗口
force_calc自动检测缓存强制重新计算
+
+
+ + + + +
+

六、文件结构

+ +
case06/
+├── input/
+│   ├── input.txt          # 主配置文件(YAML 格式)
+│   ├── coord.txt          # 原子坐标(120 个原子)
+│   ├── connection.txt     # 弹簧连接关系(59 条键)
+│   ├── bond.txt           # 弹簧参数(k=1.0, L₀=1.0)
+│   └── driver.txt       # 驱动力定义(本案例新增)
+├── output/
+│   ├── trajectory.txt     # 全量轨迹数据(50000 步 × 120 原子)
+│   ├── display.txt        # 抽帧后的动画数据(500 帧 × 120 原子)
+│   ├── dynamics.log       # 计算日志
+│   ├── animation.log      # 动画启动日志(闪退时排查用)
+│   └── wave_animation.gif # 波形能量动画(step_plot_wave=1 时生成)
+├── doc/
+│   └── index.html         # 本文档
+├── Readme.md              # 案例简介
+└── run_dynamics.py        # 案例运行入口
+
+ + + + +
+

七、常见问题

+ +
+

7.1 动画窗口闪退

+

如果 VisPy 窗口一闪就消失,请检查:

+
    +
  • output/animation.log 中是否有错误信息
  • +
  • output/display.txt 是否存在(需先跑 step_sample: 1
  • +
+
+ +
+

7.2 原子不振动

+

可能原因:

+
    +
  • NSTEP 过大:抽帧间隔大于驱动周期的一半时,动画会丢失振动细节。建议 NSTEP ≤ 1/(freq · DT · 10)
  • +
  • 相位 φ 使采样点落在零值:试试 phi_z: 0 让原子在 t=0 处于振幅峰值
  • +
  • 确认 driving_force: 1driver.txt 中 amp_z 不为 0
  • +
+
+ +
+

7.3 渲染性能慢

+

原子数多时动画卡顿:

+
    +
  • 设置 use_marker: 1(使用 GPU 实例化渲染替代独立网格球体)
  • +
  • 增大 NSTEP 减少动画帧数
  • +
+
+
+ +
+ + + +
+ + diff --git a/examples/case10/input/bond.txt b/examples/case10/input/bond.txt new file mode 100644 index 0000000..30b4324 --- /dev/null +++ b/examples/case10/input/bond.txt @@ -0,0 +1,2 @@ +bond_name k rest_length +k1 300.0 1.0 diff --git a/examples/case10/input/connection.txt b/examples/case10/input/connection.txt new file mode 100644 index 0000000..1ae26e2 --- /dev/null +++ b/examples/case10/input/connection.txt @@ -0,0 +1,40 @@ +n1 n2 bond_name +1 2 k1 +2 3 k1 +3 4 k1 +4 5 k1 +5 6 k1 +6 7 k1 +7 8 k1 +8 9 k1 +9 10 k1 +10 11 k1 +11 12 k1 +12 13 k1 +13 14 k1 +14 15 k1 +15 16 k1 +16 17 k1 +17 18 k1 +18 19 k1 +19 20 k1 +20 21 k1 +21 22 k1 +22 23 k1 +23 24 k1 +24 25 k1 +25 26 k1 +26 27 k1 +27 28 k1 +28 29 k1 +29 30 k1 +30 31 k1 +31 32 k1 +32 33 k1 +33 34 k1 +34 35 k1 +35 36 k1 +36 37 k1 +37 38 k1 +38 39 k1 +39 40 k1 \ No newline at end of file diff --git a/examples/case10/input/coord.txt b/examples/case10/input/coord.txt new file mode 100644 index 0000000..6b54c89 --- /dev/null +++ b/examples/case10/input/coord.txt @@ -0,0 +1,41 @@ +n mass radius x y z vx vy vz fix_x fix_y fix_z +1 1 0.1 0 0 0 0 0 0 0 1 1 +2 1 0.1 1 0 0 0 0 0 0 1 1 +3 1 0.1 2 0 0 0 0 0 0 1 1 +4 1 0.1 3 0 0 0 0 0 0 1 1 +5 1 0.1 4 0 0 0 0 0 0 1 1 +6 1 0.1 5 0 0 0 0 0 0 1 1 +7 1 0.1 6 0 0 0 0 0 0 1 1 +8 1 0.1 7 0 0 0 0 0 0 1 1 +9 1 0.1 8 0 0 0 0 0 0 1 1 +10 1 0.1 9 0 0 0 0 0 0 1 1 +11 1 0.1 10 0 0 0 0 0 0 1 1 +12 1 0.1 11 0 0 0 0 0 0 1 1 +13 1 0.1 12 0 0 0 0 0 0 1 1 +14 1 0.1 13 0 0 0 0 0 0 1 1 +15 1 0.1 14 0 0 0 0 0 0 1 1 +16 1 0.1 15 0 0 0 0 0 0 1 1 +17 1 0.1 16 0 0 0 0 0 0 1 1 +18 1 0.1 17 0 0 0 0 0 0 1 1 +19 1 0.1 18 0 0 0 0 0 0 1 1 +20 1 0.1 19 0 0 0 0 0 0 1 1 +21 1 0.1 20 0 0 0 0 0 0 1 1 +22 1 0.1 21 0 0 0 0 0 0 1 1 +23 1 0.1 22 0 0 0 0 0 0 1 1 +24 1 0.1 23 0 0 0 0 0 0 1 1 +25 1 0.1 24 0 0 0 0 0 0 1 1 +26 1 0.1 25 0 0 0 0 0 0 1 1 +27 1 0.1 26 0 0 0 0 0 0 1 1 +28 1 0.1 27 0 0 0 0 0 0 1 1 +29 1 0.1 28 0 0 0 0 0 0 1 1 +30 1 0.1 29 0 0 0 0 0 0 1 1 +31 1 0.1 30 0 0 0 0 0 0 1 1 +32 1 0.1 31 0 0 0 0 0 0 1 1 +33 1 0.1 32 0 0 0 0 0 0 1 1 +34 1 0.1 33 0 0 0 0 0 0 1 1 +35 1 0.1 34 0 0 0 0 0 0 1 1 +36 1 0.1 35 0 0 0 0 0 0 1 1 +37 1 0.1 36 0 0 0 0 0 0 1 1 +38 1 0.1 37 0 0 0 0 0 0 1 1 +39 1 0.1 38 0 0 0 0 0 0 1 1 +40 1 0.1 39 0 0 0 0 0 1 1 1 \ No newline at end of file diff --git a/examples/case10/input/driver.txt b/examples/case10/input/driver.txt new file mode 100644 index 0000000..32c0362 --- /dev/null +++ b/examples/case10/input/driver.txt @@ -0,0 +1,2 @@ + n amp_x amp_y amp_z freq_x freq_y freq_z phi_x phi_y phi_z period + 1 0.1 0 0 0.04 0 0 0 0 90 all diff --git a/examples/case10/input/input.txt b/examples/case10/input/input.txt new file mode 100644 index 0000000..d2d0ad1 --- /dev/null +++ b/examples/case10/input/input.txt @@ -0,0 +1,114 @@ +# 物理模拟参数配置 +# 格式:YAML +# 用法:python run_dynamics.py + +# ── 流程控制 ────────────────────────────────── +# 每步用 0/1 单独开关,1=执行,0=跳过 +# 依赖关系:抽帧依赖模拟结果,绘图依赖模拟+抽帧 +step_simulate: 1 # 运行物理模拟 → output/display.txt(引擎直接抽帧) +step_sample: 0 # (旧版)从 trajectory.txt 重新抽帧,默认0=不执行 +step_plot: 1 # 绘制轨迹/能量图 → output/trajectory_plots.png +step_animation: 0 # 自动播放 VisPy 3D 动画窗口(需安装 vispy) +step_plot_wave: 1 # 绘制波形能量动画 +force_calc: 1 # 强制重新计算:1=跳过缓存强算,0=自动使用已有输出 +plot_wave_save_gif: 0 # 输出波形 GIF(需 step_plot_wave=1) +plot_wave_save_mp4: 0 # 输出波形 MP4(需 step_plot_wave=1) + +# ── 文件保存 ────────────────────────────────── +save_trajectory: 0 # 0=不保留完整轨迹文件, 1=保留 trajectory.txt(用于后续单独抽帧) + +# ── 计算引擎 ────────────────────────────────── +# 可选: python, c, cpp, fortran, java +engine: c # 默认使用 python 引擎 + +# ── 盒子 ────────────────────────────────────── +box_a: 300.0 # 立方体半边长,粒子被限制在 [-box_a, box_a]³ 内 + +# ── 初始构型 ────────────────────────────────── +# 坐标文件格式: +# 第一行:n mass radius x y z vx vy vz fix_x fix_y fix_z +# 后续行:原子序号 质量 半径 x y z vx vy vz fix_x fix_y fix_z +coord_file: input/coord.txt +connection_file: input/connection.txt +bond_file: input/bond.txt +driver_file: input/driver.txt # 驱动力定义文件(driving_force=1 时生效) + +# 绘图/动画展示的原子序号(对应 coord_file 第一列 n) +plot_atom: 1 + +# ── 物理参数 ────────────────────────────────── +# 三个方向分量分别对应 x, y, z +G: [0.000, 0.000, 0.000] # 重力场分量 (m/s²) +B: [0.005, 0.000, 0.005] # 阻尼分量 + +# ── 力开关(0=关闭, 1=开启)────────────────── +gravity_field: 0 # 均匀重力场 (G) +gravity_interaction: 0 # 原子间万有引力 +elastic_force: 1 # 弹簧键力 +damping_force: 0 # 阻尼 (B) +driving_force: 1 # 驱动力(需 driver_file 定义) +# +gravity_strength: 1.0 # 万有引力强度(仅 gravity_interaction=1 时有效) + +# ── 数值算法 ────────────────────────────────── +# 可选: +# explicit_euler 显式欧拉法 +# implicit_euler 隐式欧拉法 +# midpoint 中点法 +# leapfrog 蛙跳法 +method: leapfrog + +# ── 步骤控制 ────────────────────────────────── +# 以下参数控制哪些步骤被执行和保存 + +# 预热步数:模拟开始时跳过不保存的步数(用于稳定初始状态) +warmup_steps: 0 # 默认 0(立即开始记录) + +# 总模拟时间(秒),程序自动计算 NT = T_total / DT +# 如果同时指定了 NT,以 NT 为准 +T_total: 10.0 + +# 抽帧间隔(每 NSTEP 步取一帧用于动画) +NSTEP: 20 + +# ── 时间步长 ────────────────────────────────── +DT: 0.001 # 时间步长 (s) + +# 抽帧范围:只保存 [sample_start, sample_end) 区间内的帧 +sample_start: null # null 表示从头开始(帧索引从 0 起) +sample_end: null # null 表示到末尾 + + + +# ── 渲染方式 ────────────────────────────────── +# 3D 动画中原子渲染方式: +# 0 = Sphere (网格球体,效果精细,原子数少时推荐) +# 1 = Marker (GPU 实例化点,原子数多时性能更佳) +use_marker: 1 + +# ── 显示参数 ────────────────────────────────── +# 盒子透明度:单个数值(统一)或 6 个数的数组,按 [-x,+x,-y,+y,-z,+z] 顺序 +alpha: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + +# 小球颜色 +# 小球半径从 coord_file 的 radius 列读取 +ball_color_r: 0.20 # R 分量 (0~1) +ball_color_g: 0.60 # G 分量 +ball_color_b: 0.90 # B 分量 + +# 盒子面颜色 +box_color_r: 0.80 +box_color_g: 0.80 +box_color_b: 0.85 + +# ── 摄像机初始位置 ──────────────────────────── +camera_distance: 120.0 # 摄像机到场景中心的距离 +camera_elevation: 0.0 # 俯仰角(度),负值=俯视 +camera_azimuth: 0.0 # 方位角(度) +camera_center_x: 60.0 # 摄像机注视点 x +camera_center_y: 0.0 # 摄像机注视点 y +camera_center_z: 0.0 # 摄像机注视点 z +move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动 + +# ── 视觉放大 ────────────────────────────────── +display_amp: [1.0, 1.0, 10.0] # x/y/z 方向视觉位移放大倍数(不影响物理) diff --git a/examples/case10/input/move_camera.txt b/examples/case10/input/move_camera.txt new file mode 100644 index 0000000..37d5c88 --- /dev/null +++ b/examples/case10/input/move_camera.txt @@ -0,0 +1,9 @@ +# move_camera.txt — 摄像机速度段驱动 +# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d +# vx/vy/vz: 平移速度(每帧移动单位) +# rx/ry/rz: 旋转速度(每帧度数) +# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留) +# +# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转 +all vx=0.02 +# 30-90 vy=0.02 ry=1 rz=1 diff --git a/examples/case10/run_dynamics.py b/examples/case10/run_dynamics.py new file mode 100644 index 0000000..3a3f2b5 --- /dev/null +++ b/examples/case10/run_dynamics.py @@ -0,0 +1,54 @@ +""" +Case runner for Dynamics case06 — 1D atomic chain (transverse wave). + +This script keeps program and data separated: + - program: ../../dynamics.py + - input: ./input + - output: ./output +""" + +from __future__ import annotations + +import argparse +import importlib.util +from pathlib import Path + + +CASE_DIR = Path(__file__).resolve().parent +DYNAMICS_PATH = Path("..") / ".." / "dynamics.py" +INPUT_DIR = Path("input") +OUTPUT_DIR = Path("output") +CONFIG_FILE = INPUT_DIR / "input.txt" + + +def load_dynamics_module(module_path: Path): + spec = importlib.util.spec_from_file_location("dynamics_module", module_path) + if spec is None or spec.loader is None: + raise ImportError(f"无法加载 dynamics.py: {module_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(): + parser = argparse.ArgumentParser(description="运行 Dynamics 示例案例 case06") + parser.add_argument("--no-plot", action="store_true", help="跳过 matplotlib 绘图") + args = parser.parse_args() + + dynamics_path = (CASE_DIR / DYNAMICS_PATH).resolve() + input_dir = (CASE_DIR / INPUT_DIR).resolve() + output_dir = (CASE_DIR / OUTPUT_DIR).resolve() + config_path = (CASE_DIR / CONFIG_FILE).resolve() + + module = load_dynamics_module(dynamics_path) + module.run_case( + config_path=config_path, + runtime_base=CASE_DIR, + input_dir=input_dir, + output_dir=output_dir, + no_plot=args.no_plot, + ) + + +if __name__ == "__main__": + main() diff --git a/plot_wave.py b/plot_wave.py index 09dd2c9..c023f02 100644 --- a/plot_wave.py +++ b/plot_wave.py @@ -108,9 +108,21 @@ def _load_wave_dataset(output_dir): "gravity_strength": float(header.get("gravity_strength", 1.0)), "G": gravity_vec, "driving_force": int(header.get("driving_force", 0)), + "display_amp": _parse_display_amp(header.get("display_amp", "")), } +def _parse_display_amp(raw): + if not raw or not str(raw).strip(): + return np.ones(3) + try: + import ast as _ast + v = np.array(_ast.literal_eval(str(raw).strip()), dtype=np.float64) + return v if v.shape == (3,) else np.ones(3) + except Exception: + return np.ones(3) + + def compute_energy(x, y, z, vx, vy, vz, masses, mass_arr, bond_pairs, bond_stiffness, bond_rest_lengths, gravity_field, G, gravity_interaction, gravity_strength): @@ -320,6 +332,70 @@ def compute_energy_flux(x, y, z, vx, vy, vz, return flux, bond_xpos +def compute_driver_work_power(x, y, z, vx, vy, vz, + bond_pairs, bond_stiffness, bond_rest_lengths, + atom_ids, driver_info): + """计算每个驱动原子通过键对系统(非驱动原子)做功的功率。 + + 对于驱动原子 d 与系统原子 j 之间的键: + P_{d→j} = F_{d→j} · v_j + 其中 F_{d→j} 是键对系统原子 j 的弹簧力。 + + Returns: + drv_powers: dict {atom_id: (n_frames,)} 每个驱动原子的瞬时功率 + total_power: (n_frames,) 所有驱动原子功率之和 + """ + if bond_pairs is None or len(bond_pairs) == 0: + n_frames = x.shape[0] + return {}, np.zeros(n_frames) + + id_to_idx = {int(aid): i for i, aid in enumerate(atom_ids)} + driven_idx = {id_to_idx[aid] for aid in driver_info if aid in id_to_idx} + n_frames = x.shape[0] + + drv_powers = {} + + for b in range(len(bond_pairs)): + ii, jj = int(bond_pairs[b, 0]), int(bond_pairs[b, 1]) + i_drv = ii in driven_idx + j_drv = jj in driven_idx + if i_drv == j_drv: # 两端同为驱动或同为自由,跳过 + continue + + drv_loc = ii if i_drv else jj # 驱动端 index + sys_loc = jj if i_drv else ii # 系统端 index + drv_aid = int(atom_ids[drv_loc]) + + dx_ = x[:, jj] - x[:, ii] + dy_ = y[:, jj] - y[:, ii] + dz_ = z[:, jj] - z[:, ii] + dist = np.sqrt(dx_**2 + dy_**2 + dz_**2) + dist = np.maximum(dist, 1e-12) + + k = bond_stiffness[b] + r0 = bond_rest_lengths[b] + fac = k * (dist - r0) / dist # 标量弹力因子 + + # 作用于系统原子的弹簧力:指向驱动原子方向 + if i_drv: # drv=i, sys=j: 力方向 j→i,即 -(dx_, dy_, dz_) + fx = -fac * dx_ + fy = -fac * dy_ + fz = -fac * dz_ + else: # drv=j, sys=i: 力方向 i→j,即 +(dx_, dy_, dz_) + fx = fac * dx_ + fy = fac * dy_ + fz = fac * dz_ + + power = fx * vx[:, sys_loc] + fy * vy[:, sys_loc] + fz * vz[:, sys_loc] + + if drv_aid not in drv_powers: + drv_powers[drv_aid] = np.zeros(n_frames) + drv_powers[drv_aid] += power + + total_power = sum(drv_powers.values()) if drv_powers else np.zeros(n_frames) + return drv_powers, total_power + + def plot_wave(output_dir, save_gif=False, save_mp4=False, show=True): """主绘图函数:读取 display.txt 并生成波形+能量动画。 @@ -370,26 +446,106 @@ def plot_wave(output_dir, save_gif=False, save_mp4=False, show=True): dy = y - pos_0[np.newaxis, :, 1] dz = z - pos_0[np.newaxis, :, 2] - # ── 系统总能量(用于右下时间图)── - ek_sys, us_sys, ug_sys, ugr_sys = compute_energy( - x, y, z, vx, vy, vz, masses, masses, - bond_pairs, bond_stiffness, bond_rest_lengths, - gravity_field, G, gravity_interaction, gravity_strength) - e_total = ek_sys + us_sys + ug_sys + ugr_sys - power = np.gradient(e_total, t) - - # ── 每粒子能量 ── + # ── 每粒子能量(图2 与图3 共用同一套计算)── ek_atom, pe_atom, et_atom = compute_per_atom_energy( x, y, z, vx, vy, vz, masses, bond_pairs, bond_stiffness, bond_rest_lengths, atom_ids, driver_info) + # ── 系统总能量 = 各粒子求和(与图2 完全一致)── + ek_sys = np.sum(ek_atom, axis=1) + us_sys = np.sum(pe_atom, axis=1) + e_total = np.sum(et_atom, axis=1) + power = np.gradient(e_total, t) + # 重力势能:仍用原有函数提供(若启用重力场) + _, _, ug_sys, ugr_sys = compute_energy( + x, y, z, vx, vy, vz, masses, masses, + bond_pairs, bond_stiffness, bond_rest_lengths, + gravity_field, G, gravity_interaction, gravity_strength) + if gravity_field or gravity_interaction: + e_total = e_total + ug_sys + ugr_sys + power = np.gradient(e_total, t) + # ── 能流密度 ── flux, bond_xpos = compute_energy_flux( x, y, z, vx, vy, vz, bond_pairs, bond_stiffness, bond_rest_lengths) - # ── y 轴范围 ── + # ── 驱动做功功率 ── + drv_powers, total_drv_power = compute_driver_work_power( + x, y, z, vx, vy, vz, + bond_pairs, bond_stiffness, bond_rest_lengths, + atom_ids, driver_info) + + # ── 原子可视化预计算 ── + display_amp = np.array(data.get("display_amp", [1.0, 1.0, 1.0]), dtype=np.float64) + eq_x_vis = pos_0[:, 0] + eq_z_vis = pos_0[:, 2] + # 视觉坐标 = 平衡位置 + 放大的位移 + x_vis = eq_x_vis + (x - eq_x_vis) * display_amp[0] # (n_frames, n_atoms) + z_vis = eq_z_vis + (z - eq_z_vis) * display_amp[2] + + # 找边界原子(与驱动原子成键的系统原子)及对应键 + id_to_idx_vis = {int(aid): i for i, aid in enumerate(atom_ids)} + driven_set_vis = {id_to_idx_vis[aid] for aid in driver_info if aid in id_to_idx_vis} + bond_boundary_list = [] # (drv_idx, sys_idx, bond_b) + for _b in range(len(bond_pairs)): + _ii, _jj = int(bond_pairs[_b, 0]), int(bond_pairs[_b, 1]) + if (_ii in driven_set_vis) ^ (_jj in driven_set_vis): + _drv = _ii if _ii in driven_set_vis else _jj + _sys = _jj if _ii in driven_set_vis else _ii + bond_boundary_list.append((_drv, _sys, _b)) + + # 唯一边界原子索引列表 + _bnd_set = {} + for _drv, _sys, _b in bond_boundary_list: + if _sys not in _bnd_set: + _bnd_set[_sys] = len(_bnd_set) + boundary_atom_idx = np.array(list(_bnd_set.keys()), dtype=int) + n_boundary = len(boundary_atom_idx) + + _lat = (eq_x_vis[-1] - eq_x_vis[0]) / max(n_atoms - 1, 1) + _z_all = z_vis.reshape(-1) + _z_min, _z_max = np.min(_z_all), np.max(_z_all) + _z_mg = max((_z_max - _z_min) * 0.2, _lat * 2) + _z_range = max((_z_max - _z_min) + 2 * _z_mg, _lat * 4) + _arrow_len = _z_range * 0.50 # 箭头最大显示长度 = 纵坐标范围的 50% + + # 预计算边界原子受到的驱动力 + # 方向:沿显示坐标下的键方向(消除坐标轴比例失真);大小:胡克力模 k|d-r0| + bnd_fx_scaled = np.zeros((n_frames, max(n_boundary, 1))) + bnd_fz_scaled = np.zeros((n_frames, max(n_boundary, 1))) + _f_mag_all = [] + for _drv, _sys, _b in bond_boundary_list: + _bi = _bnd_set[_sys] + # 物理键长 + _dx3 = x[:, _drv] - x[:, _sys] + _dy3 = y[:, _drv] - y[:, _sys] + _dz3 = z[:, _drv] - z[:, _sys] + _dist = np.maximum(np.sqrt(_dx3**2 + _dy3**2 + _dz3**2), 1e-12) + # 有符号力大小(正 = 拉向驱动原子,负 = 推离) + _f_signed = bond_stiffness[_b] * (_dist - bond_rest_lengths[_b]) + # 显示坐标下的键方向(x-z 平面) + _dx_d = x_vis[:, _drv] - x_vis[:, _sys] + _dz_d = z_vis[:, _drv] - z_vis[:, _sys] + _disp_len = np.maximum(np.sqrt(_dx_d**2 + _dz_d**2), 1e-12) + bnd_fx_scaled[:, _bi] += _f_signed * _dx_d / _disp_len + bnd_fz_scaled[:, _bi] += _f_signed * _dz_d / _disp_len + _f_mag_all.append(np.abs(_f_signed)) + _f_max = np.max(_f_mag_all) if _f_mag_all else 1.0 + _f_max = _f_max if _f_max > 1e-20 else 1.0 + bnd_fx_scaled = bnd_fx_scaled / _f_max * _arrow_len + bnd_fz_scaled = bnd_fz_scaled / _f_max * _arrow_len + + # 边界原子速度(方向沿实际速度,大小归一化) + bnd_vx_raw = vx[:, boundary_atom_idx] if n_boundary > 0 else np.zeros((n_frames, 1)) + bnd_vz_raw = vz[:, boundary_atom_idx] if n_boundary > 0 else np.zeros((n_frames, 1)) + _v_max = np.max(np.sqrt(bnd_vx_raw**2 + bnd_vz_raw**2)) if n_boundary > 0 else 1.0 + _v_max = _v_max if _v_max > 1e-20 else 1.0 + bnd_vx_scaled = bnd_vx_raw / _v_max * _arrow_len + bnd_vz_scaled = bnd_vz_raw / _v_max * _arrow_len + + # y 轴范围 ── def get_ylim(arr): vmax = np.max(np.abs(arr)) if vmax < 1e-10: @@ -413,8 +569,9 @@ def plot_wave(output_dir, save_gif=False, save_mp4=False, show=True): energy_vmax = energy_vmax if energy_vmax > 1e-12 else 1.0 energy_ylim = (0.0, energy_vmax * 1.2) - e_max = max(np.max(e_total), 0.01) * 1.3 - p_max = max(np.max(np.abs(power)) * 1.3, 0.01) + e_max = max(np.max(e_total), 1e-12) + e_min = min(np.min(e_total), 0.0) + p_max = max(np.percentile(np.abs(power), 95) if len(power) > 0 else 0.0, 0.0) # 能流 y 轴范围(对称,正负各半) if flux.size > 0: @@ -426,100 +583,282 @@ def plot_wave(output_dir, save_gif=False, save_mp4=False, show=True): atom_idx = np.arange(n_atoms) - # ── 图形布局:4 行 × 1 列,纵向排列 ── + # ── 驱动/非驱动粒子能量(右下图)── + id_to_idx = {int(aid): i for i, aid in enumerate(atom_ids)} + driven_idx = np.array([id_to_idx[aid] for aid in driver_info if aid in id_to_idx], dtype=int) + free_idx = np.setdiff1d(np.arange(n_atoms), driven_idx) + has_driver = len(driven_idx) > 0 + + if has_driver: + ek_drv = np.sum(ek_atom[:, driven_idx], axis=1) + ep_drv = np.sum(pe_atom[:, driven_idx], axis=1) + ek_free = np.sum(ek_atom[:, free_idx], axis=1) + ep_free = np.sum(pe_atom[:, free_idx], axis=1) + + # ── 图形布局:左3行、右3行(subplot_mosaic)── plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False - fig, (ax_wave, ax_energy, ax_flux, ax_ep) = plt.subplots(4, 1, figsize=(12, 18)) - fig.suptitle("波形与能量分析", fontsize=16) - fig.subplots_adjust(hspace=0.42, top=0.95) + from matplotlib.collections import LineCollection as _LC - # ── 图1:x/y/z 位移波形叠加 ── + fig, axes = plt.subplot_mosaic( + [['atoms', 'ep'], + ['wave', 'drv'], + ['energy', 'pwr']], + figsize=(20, 15)) + ax_atoms = axes['atoms'] + ax_wave = axes['wave'] + ax_ep = axes['ep'] + ax_energy = axes['energy'] + ax_drv = axes['drv'] + ax_pwr = axes['pwr'] + fig.subplots_adjust(hspace=0.45, wspace=0.32, top=0.97) + + # ── 左上:原子位置 + 键 + 力/速度箭头 ── + _x_min, _x_max = eq_x_vis[0], eq_x_vis[-1] + ax_atoms.set_xlim(_x_min - _lat, _x_max + _lat) + ax_atoms.set_ylim(_z_min - _z_mg, _z_max + _z_mg) + ax_atoms.set_xlabel("位置 $x$") + ax_atoms.set_ylabel("位移 $z$(放大 {:.0f}×)".format(display_amp[2])) + ax_atoms.set_title("原子运动(红=驱动,箭头:红=驱动力,蓝=边界速度)") + ax_atoms.set_aspect('auto') + ax_atoms.grid(True, alpha=0.2) + + # 键线段(LineCollection,初始帧) + def _make_bond_segs(frame_idx): + segs = [] + for _b in range(len(bond_pairs)): + _ii, _jj = int(bond_pairs[_b, 0]), int(bond_pairs[_b, 1]) + segs.append([(x_vis[frame_idx, _ii], z_vis[frame_idx, _ii]), + (x_vis[frame_idx, _jj], z_vis[frame_idx, _jj])]) + return segs + + _bond_lc = _LC(_make_bond_segs(0), colors='#888888', linewidths=0.8, zorder=1) + ax_atoms.add_collection(_bond_lc) + + # 散点:自由原子(黑) + _free_mask = np.array([i not in driven_set_vis for i in range(n_atoms)]) + _scat_free, = ax_atoms.plot( + x_vis[0, _free_mask], z_vis[0, _free_mask], + 'o', color='black', ms=4, zorder=3) + + # 散点:驱动原子(红) + _drv_mask = ~_free_mask + _scat_drv, = ax_atoms.plot( + x_vis[0, _drv_mask], z_vis[0, _drv_mask], + 'o', color='red', ms=6, zorder=4) + + # 力箭头(红,边界原子) + _q_force = ax_atoms.quiver( + x_vis[0, boundary_atom_idx] if n_boundary > 0 else [], + z_vis[0, boundary_atom_idx] if n_boundary > 0 else [], + bnd_fx_scaled[0] if n_boundary > 0 else [], + bnd_fz_scaled[0] if n_boundary > 0 else [], + color='red', angles='xy', scale_units='xy', scale=1, + width=0.007, headwidth=5, headlength=5, zorder=5) + + # 速度箭头(蓝,边界原子) + _q_vel = ax_atoms.quiver( + x_vis[0, boundary_atom_idx] if n_boundary > 0 else [], + z_vis[0, boundary_atom_idx] if n_boundary > 0 else [], + bnd_vx_scaled[0] if n_boundary > 0 else [], + bnd_vz_scaled[0] if n_boundary > 0 else [], + color='blue', angles='xy', scale_units='xy', scale=1, + width=0.007, headwidth=5, headlength=5, zorder=5) + + # ── 左上:x/y/z 位移波形 ── ax_wave.set_xlim(0, n_atoms - 1) ax_wave.set_ylim(disp_ylim) ax_wave.set_xlabel("原子序号") - ax_wave.set_ylabel("位移") - ax_wave.set_title("粒子位移(x / y / z 方向)") + ax_wave.set_ylabel("位移 $u$") + ax_wave.set_title("粒子位移($x$ / $y$ / $z$ 方向)") ax_wave.grid(True, alpha=0.3) wave_disps = [dx, dy, dz] - wave_labels = ["x 方向(纵波)", "y 方向(横波)", "z 方向(横波)"] + wave_labels = ["$u_x$(纵波)", "$u_y$(横波)", "$u_z$(横波)"] wave_colors = ["#2563eb", "#ea580c", "#16a34a"] wave_lines = [] for label, color in zip(wave_labels, wave_colors): ln, = ax_wave.plot([], [], color=color, linewidth=1.5, label=label) wave_lines.append(ln) ax_wave.legend(loc="upper right", fontsize=9) - time_text = ax_wave.text(0.02, 0.95, "", transform=ax_wave.transAxes, - fontsize=10, verticalalignment="top") + _dt_frame = (t[1] - t[0]) if len(t) > 1 else 0.0 + _t_total_str = f"{t[-1] + _dt_frame:.2f} s" + _time_axes = [ax_atoms, ax_wave, ax_energy, ax_ep, ax_drv, ax_pwr] + time_texts = [ + ax.text(0.02, 0.97, "", transform=ax.transAxes, + fontsize=9, verticalalignment="top", + bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.7)) + for ax in _time_axes + ] + time_text = time_texts[1] # 保留旧名兼容下面的代码 - # ── 图2:每粒子动能、势能、总能叠加 ── + # ── 左下:每粒子能量(左轴)+ 能流密度(右轴)── ax_energy.set_xlim(0, n_atoms - 1) ax_energy.set_ylim(energy_ylim) - ax_energy.set_xlabel("原子序号") - ax_energy.set_ylabel("能量") - ax_energy.set_title("每粒子能量(动能 / 势能 / 总能)") + ax_energy.set_xlabel("原子序号 / 键位置") + ax_energy.set_ylabel("能量 $E$") + ax_energy.set_title( + r"每粒子能量($E_k$/$E_p$/$E_{tot}$)与能流密度 $J$" + ) ax_energy.grid(True, alpha=0.3) energy_arrays = [ek_atom, pe_atom, et_atom] - energy_labels = ["动能", "势能", "总能"] - energy_colors = ["#1d4ed8", "#b45309", "#7c3aed"] + energy_labels = ["$E_k$(动能)", "$E_p$(势能)", "$E_{tot}$(总能)"] + energy_colors = ["#16a34a", "#b45309", "#7c3aed"] energy_lines = [] for label, color in zip(energy_labels, energy_colors): ln, = ax_energy.plot([], [], color=color, linewidth=1.5, label=label) energy_lines.append(ln) - ax_energy.legend(loc="upper right", fontsize=9) - # ── 图3:能流密度 J(Hardy 公式)── - xmin_flux = bond_xpos[0] if len(bond_xpos) > 0 else 0 - xmax_flux = bond_xpos[-1] if len(bond_xpos) > 0 else n_atoms - 1 - ax_flux.set_xlim(xmin_flux, xmax_flux) + ax_flux = ax_energy.twinx() ax_flux.set_ylim(flux_ylim) + ax_flux.set_ylabel("能流密度 $J$", color="#dc2626") + ax_flux.tick_params(axis='y', labelcolor="#dc2626") ax_flux.axhline(0, color="gray", linewidth=0.8, linestyle="--") - ax_flux.set_xlabel("位置(键中点 x 坐标)") - ax_flux.set_ylabel("能流密度 J") - ax_flux.set_title("键能流密度 J = ½ F·(vᵢ+vⱼ) (J>0 向右传播,J<0 向左传播)") - ax_flux.grid(True, alpha=0.3) - flux_line, = ax_flux.plot([], [], color="#dc2626", linewidth=1.5) + flux_line, = ax_flux.plot([], [], color="#dc2626", linewidth=1.5, + label="$J$(能流密度)") + handles_e, labels_e = ax_energy.get_legend_handles_labels() + handles_f, labels_f = ax_flux.get_legend_handles_labels() + ax_energy.legend(handles_e + handles_f, labels_e + labels_f, + loc="upper right", fontsize=9) - # ── 图4:系统总能量随时间 ── + # ── 右上:系统总能量随时间 ── ax_ep.set_xlim(t[0], t[-1]) - ep_yhigh = max(e_max, p_max) - ep_ylow = min(-p_max * 0.1, 0.0) - ax_ep.set_ylim(ep_ylow, ep_yhigh) - ax_ep.set_xlabel("时间 (s)") - ax_ep.set_ylabel("能量 / 功率") + ep_margin = (e_max - e_min) * 0.15 if e_max > e_min else e_max * 0.15 + ax_ep.set_ylim(e_min - ep_margin, e_max + ep_margin) + ax_ep.set_clip_on(True) + ax_ep.set_xlabel("时间 $t$ (s)") + ax_ep.set_ylabel("能量 $E$ / 功率 $P$") ax_ep.set_title("系统能量与输入功率") ax_ep.grid(True, alpha=0.3) - ln_ek, = ax_ep.plot([], [], "b-", lw=1.5, label="动能") - ln_us, = ax_ep.plot([], [], "orange", lw=1.5, label="弹性势能") - ln_et, = ax_ep.plot([], [], "r--", lw=1.5, label="总能量") - ln_pw, = ax_ep.plot([], [], "g-", lw=1.5, alpha=0.7, label="输入功率 (dE/dt)") + ln_ek, = ax_ep.plot([], [], "b-", lw=1.5, label="$E_k$(动能)") + ln_us, = ax_ep.plot([], [], "orange", lw=1.5, label="$E_s$(弹性势能)") + ln_et, = ax_ep.plot([], [], "r--", lw=1.5, label="$E_{tot}$(总能量)") + ln_pw, = ax_ep.plot([], [], "g-", lw=1.5, alpha=0.7, label=r"$P_{in}=dE/dt$") ln_ug = None ln_ugr = None if gravity_field: - ln_ug, = ax_ep.plot([], [], "purple", lw=1.0, alpha=0.5, label="重力势能") + ln_ug, = ax_ep.plot([], [], "purple", lw=1.0, alpha=0.5, label="$E_g$(重力势能)") if gravity_interaction and n_atoms <= 200: - ln_ugr, = ax_ep.plot([], [], "brown", lw=1.0, alpha=0.5, label="万有引力势能") - ax_ep.legend(loc="upper left", fontsize=9) + ln_ugr, = ax_ep.plot([], [], "brown", lw=1.0, alpha=0.5, label="$E_{gr}$(万有引力势能)") + ax_ep.legend(loc="upper right", fontsize=9) + + # ── 右下:驱动/非驱动粒子能量随时间 ── + ax_drv.set_xlim(t[0], t[-1]) + ax_drv.set_xlabel("时间 $t$ (s)") + ax_drv.set_ylabel("能量 $E$") + ax_drv.grid(True, alpha=0.3) + ln_ek_drv = ln_ep_drv = ln_ek_free = ln_ep_free = None + ln_et_drv = ln_et_free = None + if has_driver: + et_drv = ek_drv + ep_drv + et_free = ek_free + ep_free + drv_ids = sorted(driver_info.keys()) + ax_drv.set_title(f"驱动粒子(序号 {drv_ids})向系统做功") + ln_ek_drv, = ax_drv.plot([], [], color="#dc2626", lw=1.2, linestyle="--", + label=r"$E_k^{drv}$(驱动动能)") + ln_ep_drv, = ax_drv.plot([], [], color="#f97316", lw=1.2, linestyle="--", + label=r"$E_p^{drv}$(驱动势能)") + ln_et_drv, = ax_drv.plot([], [], color="#7f1d1d", lw=2.0, + label=r"$E_{tot}^{drv}$(驱动总能)") + ln_ek_free, = ax_drv.plot([], [], color="#2563eb", lw=1.2, linestyle="--", + label=r"$E_k^{sys}$(系统动能)") + ln_ep_free, = ax_drv.plot([], [], color="#16a34a", lw=1.2, linestyle="--", + label=r"$E_p^{sys}$(系统势能)") + ln_et_free, = ax_drv.plot([], [], color="#1e3a5f", lw=2.0, + label=r"$E_{tot}^{sys}$(系统总能)") + ax_drv.legend(loc="upper right", fontsize=8) + # y 轴一次定好 + _drv_all = np.concatenate([ek_drv, ep_drv, et_drv, ek_free, ep_free, et_free]) + _dy_max = np.max(_drv_all) + _dy_min = np.min(_drv_all) + _dy_mg = (_dy_max - _dy_min) * 0.15 if _dy_max > _dy_min else abs(_dy_max) * 0.15 + 1e-12 + ax_drv.set_ylim(_dy_min - _dy_mg, _dy_max + _dy_mg) + else: + ax_drv.set_title("驱动粒子能量(无驱动力)") + ax_drv.text(0.5, 0.5, "无驱动力", transform=ax_drv.transAxes, + ha="center", va="center", fontsize=12, color="gray") + + # ── 右下:驱动做功功率 ── + ax_pwr.set_xlim(t[0], t[-1]) + ax_pwr.set_xlabel("时间 $t$ (s)") + ax_pwr.set_ylabel("功率 $P$") + ax_pwr.set_title("驱动原子对系统做功的功率 $P = \\mathbf{F}_{bond}\\cdot\\mathbf{v}_{sys}$") + ax_pwr.axhline(0, color="gray", linewidth=0.8, linestyle="--") + ax_pwr.grid(True, alpha=0.3) + + pwr_colors = ["#dc2626", "#2563eb", "#16a34a", "#f97316", "#7c3aed"] + ln_pwr_each = {} # aid -> Line2D + if drv_powers: + for idx_d, (aid, _) in enumerate(sorted(drv_powers.items())): + color = pwr_colors[idx_d % len(pwr_colors)] + ln, = ax_pwr.plot([], [], color=color, lw=1.2, linestyle="--", + label=f"$P_{{drv,{aid}}}$(原子 {aid})") + ln_pwr_each[aid] = ln + ln_pwr_total, = ax_pwr.plot([], [], color="black", lw=2.0, + label=r"$P_{total}$(总功率)") + ax_pwr.legend(loc="upper right", fontsize=9) + + # y 轴一次定好 + if drv_powers: + _pw_all = np.concatenate(list(drv_powers.values()) + [total_drv_power]) + _pw_max = np.max(_pw_all) + _pw_min = np.min(_pw_all) + _pw_mg = (_pw_max - _pw_min) * 0.15 if _pw_max > _pw_min else abs(_pw_max) * 0.15 + 1e-12 + ax_pwr.set_ylim(_pw_min - _pw_mg, _pw_max + _pw_mg) # ── 动画更新 ── def update(frame): - # 图1:位移波形 + # 每轮开始时清屏 + # 左上:原子位置动画 + _bond_lc.set_segments(_make_bond_segs(frame)) + _scat_free.set_xdata(x_vis[frame, _free_mask]) + _scat_free.set_ydata(z_vis[frame, _free_mask]) + _scat_drv.set_xdata(x_vis[frame, _drv_mask]) + _scat_drv.set_ydata(z_vis[frame, _drv_mask]) + if n_boundary > 0: + _q_force.set_offsets( + np.column_stack([x_vis[frame, boundary_atom_idx], + z_vis[frame, boundary_atom_idx]])) + _q_force.set_UVC(bnd_fx_scaled[frame], bnd_fz_scaled[frame]) + _q_vel.set_offsets( + np.column_stack([x_vis[frame, boundary_atom_idx], + z_vis[frame, boundary_atom_idx]])) + _q_vel.set_UVC(bnd_vx_scaled[frame], bnd_vz_scaled[frame]) + + if frame == 0: + all_clear = list(wave_lines) + list(energy_lines) + [flux_line] + all_clear += [ln for ln in [ln_ek, ln_us, ln_et, ln_pw, ln_ug, ln_ugr] + if ln is not None] + if has_driver: + all_clear += [ln for ln in [ln_ek_drv, ln_ep_drv, ln_et_drv, + ln_ek_free, ln_ep_free, ln_et_free] + if ln is not None] + all_clear += list(ln_pwr_each.values()) + [ln_pwr_total] + for ln in all_clear: + ln.set_data([], []) + _tstr0 = f"t = {t[0]:.2f} s / {_t_total_str} | 帧 1/{n_frames}" + for _tt in time_texts: + _tt.set_text(_tstr0) + return all_clear + time_texts + [_bond_lc, _scat_free, _scat_drv, + _q_force, _q_vel] + + # 左中:位移波形 for i, ln in enumerate(wave_lines): ln.set_data(atom_idx, wave_disps[i][frame]) - time_text.set_text(f"t = {t[frame]:.2f} s | 帧 {frame+1}/{n_frames}") + _tstr = f"t = {t[frame]:.2f} s / {_t_total_str} | 帧 {frame+1}/{n_frames}" + for _tt in time_texts: + _tt.set_text(_tstr) - # 图2:每粒子能量 + # 左下:每粒子能量 + 能流密度 for i, ln in enumerate(energy_lines): ln.set_data(atom_idx, energy_arrays[i][frame]) - - # 图3:能流密度 if flux.shape[1] > 0: flux_line.set_data(bond_xpos, flux[frame]) - # 图4:系统能量(累计到当前帧) + # 右上:系统能量(累计) cur_t = t[:frame + 1] ln_ek.set_data(cur_t, ek_sys[:frame + 1]) ln_us.set_data(cur_t, us_sys[:frame + 1]) @@ -527,15 +866,33 @@ def plot_wave(output_dir, save_gif=False, save_mp4=False, show=True): ln_pw.set_data(cur_t, power[:frame + 1]) if ln_ug: ln_ug.set_data(cur_t, ug_sys[:frame + 1]) if ln_ugr: ln_ugr.set_data(cur_t, ugr_sys[:frame + 1]) - ax_ep.set_xlim(t[0], max(t[frame] + max(t[-1] * 0.05, 1), t[-1])) - artists = wave_lines + [time_text] + energy_lines + \ - [flux_line, ln_ek, ln_us, ln_et, ln_pw] - if ln_ug: artists.append(ln_ug) - if ln_ugr: artists.append(ln_ugr) + # 右中:驱动/系统粒子能量(累计) + if has_driver: + ln_ek_drv.set_data( cur_t, ek_drv[:frame + 1]) + ln_ep_drv.set_data( cur_t, ep_drv[:frame + 1]) + ln_et_drv.set_data( cur_t, et_drv[:frame + 1]) + ln_ek_free.set_data(cur_t, ek_free[:frame + 1]) + ln_ep_free.set_data(cur_t, ep_free[:frame + 1]) + ln_et_free.set_data(cur_t, et_free[:frame + 1]) + + # 右下:驱动做功功率(累计) + for aid, ln in ln_pwr_each.items(): + ln.set_data(cur_t, drv_powers[aid][:frame + 1]) + ln_pwr_total.set_data(cur_t, total_drv_power[:frame + 1]) + + artists = (wave_lines + time_texts + energy_lines + + [flux_line, ln_ek, ln_us, ln_et, ln_pw]) + if ln_ug: artists.append(ln_ug) + if ln_ugr: artists.append(ln_ugr) + if has_driver: + artists += [ln_ek_drv, ln_ep_drv, ln_et_drv, + ln_ek_free, ln_ep_free, ln_et_free] + artists += list(ln_pwr_each.values()) + [ln_pwr_total] + artists += [_bond_lc, _scat_free, _scat_drv, _q_force, _q_vel] return artists - ani = FuncAnimation(fig, update, frames=n_frames, interval=50, blit=True) + ani = FuncAnimation(fig, update, frames=n_frames, interval=50, blit=True, repeat=True) # ── 输出文件 ── gif_path = None