modified: CMakeLists.txt

modified:   INSTALL.md
	modified:   README.md
	modified:   build_release_zip.py
	modified:   compute.py
	new file:   doc/index.html
	modified:   dynamics.py
	modified:   engines/c/main.c
	modified:   engines/cpp/main.cpp
	modified:   engines/fortran/main.f90
	modified:   examples/case01/input/coord.txt
	renamed:    examples/case01/input/parameters.yaml -> examples/case01/input/input.txt
	modified:   examples/case01/run_dynamics.py
	new file:   examples/case02/input/bond.txt
	new file:   examples/case02/input/connection.txt
	new file:   examples/case02/input/coord.txt
	new file:   examples/case02/input/input.txt
	new file:   examples/case02/run_dynamics.py
This commit is contained in:
2026-05-20 16:03:59 +08:00
parent 45513fe334
commit 5de80d4f7e
18 changed files with 3058 additions and 233 deletions
+95 -36
View File
@@ -4,9 +4,9 @@ dynamics.py
统一入口:读取 YAML 配置文件 → 运行模拟 → 抽帧 → 绘图(可选)
用法:
python dynamics.py # 使用 input/parameters.yaml
python dynamics.py input/parameters.yaml # 指定配置文件
python dynamics.py --config input/parameters.yaml --no-plot
python dynamics.py # 使用 input/input.txt
python dynamics.py input/input.txt # 指定配置文件YAML 格式)
python dynamics.py --config input/input.txt --no-plot
"""
import os
@@ -115,6 +115,14 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
config = load_yaml_config(config_path)
print(f"[run] 已加载配置: {config_path}")
# ── T_total → NT 自动转换 ──────────────────────
if "T_total" in config and "NT" not in config:
dt = float(config["DT"])
config["NT"] = int(float(config["T_total"]) / dt)
print(f"[run] T_total={config['T_total']} → NT={config['NT']} (DT={dt})")
elif "T_total" in config and "NT" in config:
print(f"[run] 同时指定了 T_total 和 NT,使用 NT={config['NT']}")
# 显示步骤控制信息
steps_info = {k: config.get(k, 1) for k in ["step_simulate", "step_sample", "step_plot", "step_animation"]}
step_flags = ", ".join(f"{k}={v}" for k, v in steps_info.items())
@@ -136,27 +144,42 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
disp_path = os.path.join(output_dir_abs, "display.txt")
# ── 自动缓存检测 ───────────────────────────────────────
# 若 output/ 中 trajectory.txt 和 display.txt 均已存在,
# 自动跳过模拟和抽帧,直接使用已有结果。
# 如需强制重新计算,删除 output/ 目录或设 step_simulate: 1 即可。
output_exists = (
os.path.isdir(output_dir_abs)
and os.path.exists(traj_path)
and os.path.exists(disp_path)
)
if output_exists:
if config.get("step_simulate", 1):
print(f"[run] 检测到已有输出({traj_path}),自动跳过模拟与抽帧,直接进入后续步骤")
config["step_simulate"] = 0
config["step_sample"] = 0
else:
print(f"[run] 已有输出,步骤已被跳过")
# force_calc=1: 强制重新计算,忽略缓存
# force_calc=0: 尊重 step_simulate 设置,不自动覆盖
force_calc = int(config.get("force_calc", 0))
if force_calc:
print(f"[run] force_calc=1,跳过缓存,强制重新计算")
config["step_simulate"] = 1
config["step_sample"] = 1
elif config.get("step_simulate", 1):
# step_simulate=1 且 force_calc=0 → 按用户要求执行计算
# 但检测一下参数是否已变更(NT),如果变了则自动更新 step_sample
if os.path.isdir(output_dir_abs) and os.path.exists(traj_path) and os.path.exists(disp_path):
cached_nt = None
try:
with open(traj_path, 'rb') as _f:
_f.seek(-4096, 2)
_tail = _f.read().decode('utf-8', errors='replace')
import re as _re
_m = _re.search(r'"NT":\s*(\d+)', _tail)
if _m:
cached_nt = int(_m.group(1))
except Exception:
pass
config_nt = int(config.get("NT", 0))
if cached_nt is not None and cached_nt != config_nt:
print(f"[run] 参数已变更(缓存 NT={cached_nt},配置 NT={config_nt}),"
f"将重新计算")
config["step_sample"] = 1
else:
# 参数一致,按 step_simulate=1 执行,step_sample 由用户设置决定
pass
else:
# 目录存在但文件不全 → 强制重新计算
if os.path.isdir(output_dir_abs):
print(f"[run] output/ 目录存在但文件不完整,将重新计算")
# step_simulate=0 → 检测缓存是否存在并提示
if os.path.isdir(output_dir_abs) and os.path.exists(traj_path) and os.path.exists(disp_path):
print(f"[run] 已有输出,步骤已被跳过")
else:
print(f"[run] output/ 目录不存在,将执行完整流程")
print(f"[run] 没有可用的缓存输出,但 step_simulate=0,将跳过模拟")
# 2. 运行物理模拟 → output/trajectory.txt
if config.get("step_simulate", 1):
@@ -164,9 +187,12 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
total_steps = config["NT"]
record_steps = total_steps - (config.get("warmup_steps") or 0)
print(f"[run] 开始计算 总步数={total_steps} 记录步数={record_steps} DT={config['DT']}")
import time as _time
_t0 = _time.time()
if engine == "python":
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_from_config(config, str(runtime_base))
print(f"[run] 计算完成,记录 {record_steps}")
compute.save_trajectory_txt(traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz, str(runtime_base))
else:
# 外部引擎:先加载配置到全局变量,再运行引擎,再用 save_trajectory_txt 补全 metadata
@@ -178,6 +204,9 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
traj_x, traj_y, traj_z, traj_vx, traj_vy, traj_vz = compute.run_engine(
engine, input_dir_abs, output_dir_abs, config)
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")
else:
print("[run] 步骤 [模拟] 已跳过,直接加载已有轨迹")
if not os.path.exists(traj_path):
@@ -282,6 +311,11 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
"box_color_r": float(data["box_color_r"]),
"box_color_g": float(data["box_color_g"]),
"box_color_b": float(data["box_color_b"]),
"gravity_field": int(data.get("gravity_field", 1)),
"gravity_interaction": int(data.get("gravity_interaction", 0)),
"elastic_force": int(data.get("elastic_force", 1)),
"damping_force": int(data.get("damping_force", 0)),
"gravity_strength": float(data.get("gravity_strength", 1.0)),
}
save_display_txt(disp_data, str(runtime_base))
print(f"[run] 抽帧完成: {sample_end - sample_start} 步 -> {n_frames}")
@@ -328,21 +362,28 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
# ── 能量计算 ─────────────────────────────────────
masses = np.array(data["atom_masses"]) # (n_atoms,)
G_vec = np.array(data.get("G", [0.0, 0.0, -9.8])) # [gx, gy, gz]
gravity_field_enabled = int(data.get("gravity_field", 1))
gravity_interaction_enabled = int(data.get("gravity_interaction", 0))
gravity_strength = float(data.get("gravity_strength", 1.0))
elastic_force_enabled = int(data.get("elastic_force", 1))
damping_force_enabled = int(data.get("damping_force", 0))
# 动能 Ek = ½ m v²
ek = 0.5 * masses[np.newaxis, :] * (all_vx**2 + all_vy**2 + all_vz**2)
# 重力势能 Ug = -m G·r
ug = -masses[np.newaxis, :] * (
G_vec[0] * all_x + G_vec[1] * all_y + G_vec[2] * all_z
)
# 均匀重力势能 Ug = -m G·r
ug = np.zeros_like(ek)
if gravity_field_enabled:
ug = -masses[np.newaxis, :] * (
G_vec[0] * all_x + G_vec[1] * all_y + G_vec[2] * all_z
)
# 弹性势能 Us = ½ k (d - d₀)²
us = np.zeros_like(ek)
bond_pairs = data.get("bond_pairs")
bond_stiffness = data.get("bond_stiffness")
bond_rest_lengths = data.get("bond_rest_lengths")
if bond_pairs is not None and len(bond_pairs) > 0:
if elastic_force_enabled and bond_pairs is not None and len(bond_pairs) > 0:
for b_idx in range(len(bond_pairs)):
i, j = bond_pairs[b_idx]
dx = all_x[:, j] - all_x[:, i]
@@ -350,17 +391,33 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
dz = all_z[:, j] - all_z[:, i]
dist = np.sqrt(dx**2 + dy**2 + dz**2)
stretch = dist - bond_rest_lengths[b_idx]
us[:, i] += 0.5 * bond_stiffness[b_idx] * stretch**2
us[:, j] += 0.5 * bond_stiffness[b_idx] * stretch**2
us_each = 0.5 * bond_stiffness[b_idx] * stretch**2
us[:, i] += us_each # 将整根键的势能记给 i
# 万有引力势能 Ug_grav = -G_grav * m_i * m_j / r
ug_grav = np.zeros_like(ek)
if gravity_interaction_enabled:
n_atoms_en = len(masses)
for i in range(n_atoms_en):
for j in range(i + 1, n_atoms_en):
dx = all_x[:, j] - all_x[:, i]
dy = all_y[:, j] - all_y[:, i]
dz = all_z[:, j] - all_z[:, i]
dist = np.sqrt(dx**2 + dy**2 + dz**2)
dist = np.maximum(dist, 1e-12)
pair_pe = -gravity_strength * masses[i] * masses[j] / dist
ug_grav[:, i] += 0.5 * pair_pe
ug_grav[:, j] += 0.5 * pair_pe
# 各原子总能量
e_total = ek + ug + us # (NT, n_atoms)
e_total = ek + ug + us + ug_grav # (NT, n_atoms)
# 系统能量分量
ek_sys = np.sum(ek, axis=1)
ug_sys = np.sum(ug, axis=1)
us_sys = np.sum(us, axis=1)
e_sys = ek_sys + ug_sys + us_sys
ug_grav_sys = np.sum(ug_grav, axis=1)
e_sys = ek_sys + ug_sys + us_sys + ug_grav_sys
# ── 第 3 行左:各原子总能量 ──
ax_e = axes[2, 0]
@@ -376,9 +433,11 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
# ── 第 3 行右:系统总能量 ──
ax_sys = axes[2, 1]
ax_sys.plot(time, ek_sys, 'b-', linewidth=1.5, label="系统动能")
ax_sys.plot(time, ug_sys, 'g-', linewidth=1.5, label="系统重力势能")
if bond_pairs is not None and len(bond_pairs) > 0:
ax_sys.plot(time, ug_sys, 'g-', linewidth=1.5, label="均匀重力势能")
if elastic_force_enabled and bond_pairs is not None and len(bond_pairs) > 0:
ax_sys.plot(time, us_sys, color='orange', linewidth=1.5, label="系统弹性势能")
if gravity_interaction_enabled:
ax_sys.plot(time, ug_grav_sys, color='purple', linewidth=1.5, label="万有引力势能")
ax_sys.plot(time, e_sys, 'r--', linewidth=1.5, label="系统总能量")
ax_sys.set_title("系统总能量")
ax_sys.set_xlabel("时间 (s)")
@@ -421,8 +480,8 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output",
def main():
parser = argparse.ArgumentParser(description="物理模拟统一入口")
parser.add_argument("config_file", nargs="?", default=os.path.join("input", "parameters.yaml"),
help="YAML 配置文件路径(默认: input/parameters.yaml")
parser.add_argument("config_file", nargs="?", default=os.path.join("input", "input.txt"),
help="YAML 配置文件路径(默认: input/input.txt,虽然是 .txt 后缀但使用 YAML 格式")
parser.add_argument("--config", dest="config_override",
help="YAML 配置文件路径(可选,优先于位置参数)")
parser.add_argument("--input-dir", default="input",