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
+15 -3
View File
@@ -75,6 +75,7 @@ camera_center_z = 0.0
camera_pos_x = None
camera_pos_y = None
camera_pos_z = None
display_amp_str = ""
FIXED_MASK_X = None
FIXED_MASK_Y = None
FIXED_MASK_Z = None
@@ -774,7 +775,7 @@ def run_from_config(config, out_dir=None):
global box_color_r, box_color_g, box_color_b
global use_marker, camera_keyframes_raw, camera_distance, camera_elevation, camera_azimuth
global camera_center_x, camera_center_y, camera_center_z
global camera_pos_x, camera_pos_y, camera_pos_z
global camera_pos_x, camera_pos_y, camera_pos_z, display_amp_str
global FIXED_MASK_X, FIXED_MASK_Y, FIXED_MASK_Z
global warmup_steps, sample_start, sample_end
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
@@ -849,6 +850,13 @@ def run_from_config(config, out_dir=None):
camera_pos_x = config.get("camera_pos_x", None)
camera_pos_y = config.get("camera_pos_y", None)
camera_pos_z = config.get("camera_pos_z", None)
_damp_cfg = config.get("display_amp", None)
if _damp_cfg is not None:
import ast as _ast_cfg
_dv = np.array(_ast_cfg.literal_eval(str(_damp_cfg).strip()), dtype=np.float64)
display_amp_str = f"[{_dv[0]},{_dv[1]},{_dv[2]}]"
else:
display_amp_str = ""
# 力开关
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
@@ -1179,6 +1187,8 @@ def run_engine(engine, input_dir, output_dir, config):
_cam_extra["camera_pos_x"] = str(camera_pos_x)
_cam_extra["camera_pos_y"] = str(camera_pos_y)
_cam_extra["camera_pos_z"] = str(camera_pos_z)
if display_amp_str:
_cam_extra["display_amp"] = display_amp_str
save_display_npz(
_npz_path,
_d["frames_x"], _d["frames_y"], _d["frames_z"],
@@ -1779,7 +1789,8 @@ def run_simulation(save_trajectory=0):
"camera_pos_y": str(camera_pos_y),
"camera_pos_z": str(camera_pos_z),
} if camera_pos_x is not None else {}),
"camera_keyframes": str(camera_keyframes_raw)}
"camera_keyframes": str(camera_keyframes_raw),
**({"display_amp": display_amp_str} if display_amp_str else {})}
)
print(f"[compute] display.txt 已保存至: {disp_path} ({n_frames_actual} 帧)")
@@ -1822,7 +1833,8 @@ def run_simulation(save_trajectory=0):
"camera_center_z": str(camera_center_z),
"camera_keyframes": str(camera_keyframes_raw),
"number_of_frames": str(record_steps),
"number_of_particles": str(n_atoms)}
"number_of_particles": str(n_atoms),
**({"display_amp": display_amp_str} if display_amp_str else {})}
save_display_npz(_npz_path,
sampled_x[:n_frames_actual], sampled_y[:n_frames_actual],
sampled_z[:n_frames_actual],
+45 -8
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:
@@ -611,12 +647,12 @@ 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,8 +660,9 @@ 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
if active:
view.camera.center = tuple(_cam_center)
view.camera.distance = _cam_dist
view.camera.elevation = _cam_elev