feat: add display_amp parameter for visual displacement amplification

Supports display_amp: [ax, ay, az] in input.txt. On rendering, each atom's
displacement from its frame-0 equilibrium is multiplied by the corresponding
factor. Physics is unchanged; only the rendered positions are scaled.

Useful for visualizing small-amplitude waves that would otherwise be invisible.
Example: display_amp: [1.0, 1.0, 5.0] exaggerates z-direction motion 5x.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 07:28:56 +08:00
parent d489222eaf
commit d371b28acc
2 changed files with 65 additions and 16 deletions
+50 -13
View File
@@ -67,6 +67,20 @@ N_FRAMES = DISP_ALL_X.shape[0]
NT = int(disp_data["n_total_frames"])
N_ATOMS = int(disp_data["n_total_particles"])
DT = float(h.get("DT", 0.001))
# 视觉位移放大:display_amp: [ax, ay, az],对偏离第0帧的位移乘以倍数
_damp_raw = h.get("display_amp", "")
if _damp_raw.strip():
import ast as _ast
_damp_vals = _ast.literal_eval(_damp_raw.strip())
_damp = np.array(_damp_vals, dtype=np.float64)
if _damp.shape == (3,) and not np.allclose(_damp, 1.0):
_eq_x = DISP_ALL_X[0:1, :] # 第0帧作为平衡位置参考
_eq_y = DISP_ALL_Y[0:1, :]
_eq_z = DISP_ALL_Z[0:1, :]
DISP_ALL_X = _eq_x + (DISP_ALL_X - _eq_x) * _damp[0]
DISP_ALL_Y = _eq_y + (DISP_ALL_Y - _eq_y) * _damp[1]
DISP_ALL_Z = _eq_z + (DISP_ALL_Z - _eq_z) * _damp[2]
NSTEP = int(h.get("NSTEP", 1))
DISP_STEP = np.arange(N_FRAMES) * NSTEP
DISP_T = DISP_STEP * DT
@@ -119,11 +133,31 @@ box_color_b = float(h.get("box_color_b", 0.85))
info_margin = 8
axis_length = 10.0
import math as _math_cam
_cx = float(h.get("camera_center_x", 0.0))
_cy = float(h.get("camera_center_y", 0.0))
_cz = float(h.get("camera_center_z", 0.0))
# 若 input.txt 指定了摄像机自身坐标,则由坐标反推 distance/elevation/azimuth
if h.get("camera_pos_x") is not None:
_px = float(h["camera_pos_x"])
_py = float(h["camera_pos_y"])
_pz = float(h["camera_pos_z"])
_dx, _dy, _dz = _px - _cx, _py - _cy, _pz - _cz
_dist = _math_cam.sqrt(_dx*_dx + _dy*_dy + _dz*_dz) or 1.0
_elev = _math_cam.degrees(_math_cam.asin(max(-1.0, min(1.0, _dy / _dist))))
_azim = _math_cam.degrees(_math_cam.atan2(_dx, _dz))
else:
_dist = float(h.get("camera_distance", 40.0))
_elev = float(h.get("camera_elevation", 0))
_azim = float(h.get("camera_azimuth", 0))
initial_camera = {
"distance": float(h.get("camera_distance", 40.0)),
"elevation": float(h.get("camera_elevation", 0)),
"azimuth": float(h.get("camera_azimuth", 0)),
"center": (0, 0, 0),
"distance": _dist,
"elevation": _elev,
"azimuth": _azim,
"center": (_cx, _cy, _cz),
}
@@ -595,7 +629,9 @@ def _load_move_camera_txt():
return segs if segs else None
# 先试 move_camera.txt 直读,没有则用 display.txt 缓存
_CAM_MOTION = _load_move_camera_txt()
# header 中 camera_keyframes 为空字符串表示 move_camera=0(开关关闭),跳过文件加载
_camera_motion_enabled = bool(h.get("camera_keyframes", ""))
_CAM_MOTION = _load_move_camera_txt() if _camera_motion_enabled else None
if not _CAM_MOTION:
_CAM_MOTION = json.loads(h.get("camera_keyframes", "null")) if h.get("camera_keyframes") else None
if _CAM_MOTION:
@@ -609,14 +645,14 @@ if _CAM_MOTION:
def _update_motion_camera(f_idx):
"""速度段驱动:每帧累加平移/旋转。
时间交叠时所有段同时生效,按文件中出现的顺序依次作用。
矩阵操作不具有对易性,排在前面的段优先作用于相机位置
只有当前帧存在活动段时才覆写相机,否则保留用户键盘/鼠标操作的结果
"""
if not _CAM_MOTION:
return
global _cam_center, _cam_elev, _cam_azim, _cam_dist
# 找当前帧所有活动的段(时间交叠=同时作用),按文件顺序依次应用
active = False
for seg in _CAM_MOTION:
if seg["start"] <= f_idx < seg["end"]:
_cam_center[0] += seg["v"][0]
@@ -624,12 +660,13 @@ def _update_motion_camera(f_idx):
_cam_center[2] += seg["v"][2]
_cam_elev += seg["r"][0]
_cam_azim += seg["r"][1]
# rz 预留
active = True
view.camera.center = tuple(_cam_center)
view.camera.distance = _cam_dist
view.camera.elevation = _cam_elev
view.camera.azimuth = _cam_azim
if active:
view.camera.center = tuple(_cam_center)
view.camera.distance = _cam_dist
view.camera.elevation = _cam_elev
view.camera.azimuth = _cam_azim
def update(event):