docs: 更新 examples/Readme.md 并新增 Readme.html

- 覆盖全部 10 个案例(原 Readme 只到 case06)
- 新增案例选择指南表格
- Readme.html 为深色主题独立 HTML 页面
  (含卡片布局、标签分类、代码高亮、响应式设计)
- 各案例详情对齐最新配置参数
This commit is contained in:
2026-06-17 15:33:49 +08:00
parent ea99f09f9b
commit 0e636e275d
59 changed files with 7302 additions and 418 deletions
+209 -22
View File
@@ -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 = []