Анимация: равномерное время (ничего не вырезается) + результат эксперимента в кадре

- кадры на равномерной сетке по ФИЗИЧЕСКОМУ времени вместо индексов массива
  (раньше плотные точки разряда съедали все кадры, подлёт вырезался)
- множитель замедления написан в кадре, событие показано целиком
- шапка: выходная скорость, КПД, ступени, энергия банок, масса снаряда
- панель тока каждой катушки во времени + панель скорости снаряда,
  бегущий курсор синхронен с положением в трубе, окна разрядов подкрашены
- датчики каждой ступени отмечены на трубе

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-08 01:21:18 +05:00
parent d4affa9574
commit a0f02e246a

View File

@@ -1,10 +1,15 @@
"""Анимация одного прогона: снаряд летит по трубе, катушки светятся по току. """Анимация одного прогона: снаряд в трубе + ток катушек + скорость, всё во времени.
Строит СПЛОШНУЮ траекторию (баллистический подлёт к датчику + разряд по Честность по времени: кадры берутся на РАВНОМЕРНОЙ сетке по физическому
каждой ступени, сшитые по глобальному времени/координате) и рендерит кадр времени (раньше — по индексам массива, из-за чего плотные точки разряда
за кадром: положение снаряда в трубе + "свечение" каждой катушки съедали все кадры, а подлёт между катушками «вырезался»). Всё событие
пропорционально её текущему току (яркая -- сильное поле, тусклая -- нет). показывается целиком в замедлении, множитель замедления написан в кадре.
Сохраняется как GIF через встроенный в matplotlib PillowWriter.
Результат эксперимента — в самом кадре:
- шапка: выходная скорость, КПД, энергия банок, масса снаряда, ступени;
- средняя панель: ток каждой катушки во времени (окна разрядов подкрашены);
- нижняя панель: скорость снаряда во времени;
- бегущий курсор на обеих панелях синхронен с положением снаряда в трубе.
""" """
from pathlib import Path from pathlib import Path
@@ -19,19 +24,21 @@ import numpy as np
from gausse.sim.coilgun import CoilgunConfig, CoilgunResult from gausse.sim.coilgun import CoilgunConfig, CoilgunResult
_STAGE_COLORS = ["#d4a017", "#c0392b", "#2980b9", "#27ae60", "#8e44ad",
"#d35400", "#16a085", "#7f8c8d", "#2c3e50", "#e91e63"]
def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig, fps: int):
"""Непрерывная траектория снаряда: коаст (по инерции) + разряды по ступеням.
Строится ПОСЛЕДОВАТЕЛЬНО (без сортировки по времени, которая рвала def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig):
траекторию): между разрядами снаряд летит по инерции от конца прошлого """Непрерывные t/x/v + ток по каждой ступени + окна разрядов.
разряда до начала следующего; во время разряда берём точно посчитанные
discharge_x/v/i. Глобальная координата = центр катушки ступени + локальный Строится последовательно: подлёт (скорость почти постоянна на сантиметрах
discharge_x. Ток каждой катушки задан только в окне её собственного разряда. межкатушечного зазора) + точные траектории разрядов из решателя.
Глобальная координата = центр катушки ступени + локальный discharge_x.
""" """
n_stages = len(config.stages) n_stages = len(config.stages)
seg_t, seg_x = [], [] seg_t, seg_x, seg_v = [], [], []
seg_i = [[] for _ in range(n_stages)] seg_i = [[] for _ in range(n_stages)]
discharge_windows = [] # (t_start, t_end, stage_idx)
t_cursor = 0.0 t_cursor = 0.0
prev_global_x = None prev_global_x = None
@@ -44,14 +51,15 @@ def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig, fps: in
disch_global_x = outcome.global_coil_center_m + r.discharge_x disch_global_x = outcome.global_coil_center_m + r.discharge_x
start_global_x = float(disch_global_x[0]) start_global_x = float(disch_global_x[0])
# коаст по инерции от конца прошлого разряда до начала этого # подлёт от конца прошлого разряда до момента поджига этого
if prev_global_x is not None and prev_v and prev_v > 1e-6: if prev_global_x is not None and prev_v and prev_v > 1e-6:
coast_dist = start_global_x - prev_global_x coast_dist = max(start_global_x - prev_global_x, 0.0)
coast_dur = max(coast_dist / prev_v, 0.0) coast_dur = coast_dist / prev_v
n_coast = max(int(coast_dur * fps) + 2, 2) n_coast = 16
ct = np.linspace(0.0, coast_dur, n_coast) ct = np.linspace(0.0, coast_dur, n_coast)
seg_t.append(t_cursor + ct) seg_t.append(t_cursor + ct)
seg_x.append(np.linspace(prev_global_x, start_global_x, n_coast)) seg_x.append(np.linspace(prev_global_x, start_global_x, n_coast))
seg_v.append(np.full(n_coast, prev_v))
for s in range(n_stages): for s in range(n_stages):
seg_i[s].append(np.zeros(n_coast)) seg_i[s].append(np.zeros(n_coast))
t_cursor += coast_dur t_cursor += coast_dur
@@ -59,8 +67,10 @@ def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig, fps: in
# окно разряда: точная траектория + ток именно этой катушки # окно разряда: точная траектория + ток именно этой катушки
seg_t.append(t_cursor + r.discharge_t) seg_t.append(t_cursor + r.discharge_t)
seg_x.append(disch_global_x) seg_x.append(disch_global_x)
seg_v.append(r.discharge_v)
for s in range(n_stages): for s in range(n_stages):
seg_i[s].append(r.discharge_i if s == outcome.stage_index else np.zeros_like(r.discharge_t)) seg_i[s].append(r.discharge_i if s == outcome.stage_index else np.zeros_like(r.discharge_t))
discharge_windows.append((t_cursor, t_cursor + float(r.discharge_t[-1]), outcome.stage_index))
t_cursor += float(r.discharge_t[-1]) t_cursor += float(r.discharge_t[-1])
prev_global_x = float(disch_global_x[-1]) prev_global_x = float(disch_global_x[-1])
@@ -71,38 +81,102 @@ def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig, fps: in
times = np.concatenate(seg_t) times = np.concatenate(seg_t)
positions = np.concatenate(seg_x) positions = np.concatenate(seg_x)
velocities = np.concatenate(seg_v)
currents = [np.concatenate(seg_i[s]) for s in range(n_stages)] currents = [np.concatenate(seg_i[s]) for s in range(n_stages)]
return times, positions, currents # страховка от немонотонности на стыках сегментов (float-складки)
order = np.argsort(times, kind="stable")
return times[order], positions[order], velocities[order], currents, order, discharge_windows
def animate_run(result: CoilgunResult, config: CoilgunConfig, out_path: str, fps: int = 30) -> str: def animate_run(result: CoilgunResult, config: CoilgunConfig, out_path: str, fps: int = 30) -> str:
if not result.stage_outcomes: if not result.stage_outcomes:
raise ValueError("нечего анимировать: нет ни одной ступени в результате") raise ValueError("нечего анимировать: нет ни одной ступени в результате")
times, positions, currents = _build_global_timeline(result, config, fps) times, positions, velocities, currents_raw, order, windows = _build_global_timeline(result, config)
n_frames = min(len(times), fps * 10) # ограничение на длину анимации currents = [c[order] for c in currents_raw]
frame_idx = np.linspace(0, len(times) - 1, n_frames).astype(int) total_t = float(times[-1])
# РАВНОМЕРНАЯ сетка по физическому времени: ничего не вырезается,
# всё событие целиком в замедлении (множитель — в кадре)
wall_duration_s = 8.0
n_frames = int(wall_duration_s * fps)
t_grid = np.linspace(0.0, total_t, n_frames)
x_grid = np.interp(t_grid, times, positions)
v_grid = np.interp(t_grid, times, velocities)
i_grid = [np.interp(t_grid, times, c) for c in currents]
slowmo = wall_duration_s / max(total_t, 1e-9)
coil_centers = [o.global_coil_center_m for o in result.stage_outcomes] coil_centers = [o.global_coil_center_m for o in result.stage_outcomes]
max_current_per_stage = [max(np.max(np.abs(c)), 1e-9) for c in currents] t_ms = times * 1e3
fig, ax = plt.subplots(figsize=(8, 3)) fig, (ax_tube, ax_i, ax_v) = plt.subplots(
3, 1, figsize=(9, 7.5), gridspec_kw={"height_ratios": [1.1, 1.4, 1.4], "hspace": 0.45}
)
# --- шапка: РЕЗУЛЬТАТ эксперимента ---
eff = result.efficiency
header = (
f"выход {result.exit_v_mps:.1f} м/с | КПД {eff * 100:.1f}% | "
f"{len(config.stages)} ступ. | E_банок {result.total_energy_in_j:.0f} Дж | "
f"снаряд {config.projectile.mass_kg * 1e3:.0f} г"
if result.feasible and eff is not None
else f"НЕРЕАЛИЗУЕМО: {result.reason}"
)
fig.suptitle(header, fontsize=12, fontweight="bold")
# --- панель 1: труба ---
tube_min, tube_max = positions.min(), positions.max() tube_min, tube_max = positions.min(), positions.max()
ax.set_xlim(tube_min * 1e3 - 5, tube_max * 1e3 + 5) pad = max((tube_max - tube_min) * 0.05, 0.005)
ax.set_ylim(-1, 1) ax_tube.set_xlim((tube_min - pad) * 1e3, (tube_max + pad) * 1e3)
ax.set_xlabel("положение, мм") ax_tube.set_ylim(-1, 1)
ax.set_yticks([]) ax_tube.set_xlabel("положение, мм")
ax.set_title("Пролёт снаряда через ступени") ax_tube.set_yticks([])
ax_tube.axhline(0, color="#888", linewidth=3, zorder=1) # труба
ax.axhline(0, color="#888", linewidth=3, zorder=1) # труба
coil_dots = [] coil_dots = []
for k, c in enumerate(coil_centers): for k, c in enumerate(coil_centers):
dot = ax.scatter([c * 1e3], [0], s=600, c="lightgray", edgecolors="#333", linewidths=1.5, zorder=2) color = _STAGE_COLORS[k % len(_STAGE_COLORS)]
ax.annotate(f"катушка {k}", (c * 1e3, 0), textcoords="offset points", xytext=(0, 22), dot = ax_tube.scatter([c * 1e3], [0], s=500, c="lightgray",
ha="center", fontsize=8, color="#555") edgecolors=color, linewidths=2, zorder=2)
ax_tube.annotate(f"катушка {k}", (c * 1e3, 0), textcoords="offset points",
xytext=(0, 20), ha="center", fontsize=8, color=color)
# позиция датчика этой ступени (у каждой катушки свой)
sensor_x = (c - config.stages[k].sensor_to_coil_distance_m) * 1e3
ax_tube.plot([sensor_x], [-0.35], marker="v", color=color, markersize=6, zorder=2)
coil_dots.append(dot) coil_dots.append(dot)
slug_dot = ax.scatter([], [], s=180, c="#1f4e8c", edgecolors="white", linewidths=1, zorder=3) ax_tube.plot([], [], marker="v", linestyle="", color="#555", label="датчики")
time_text = ax.text(0.02, 0.88, "", transform=ax.transAxes, fontsize=10) ax_tube.legend(loc="upper left", fontsize=7, frameon=False)
slug_dot = ax_tube.scatter([], [], s=160, c="#1f4e8c", edgecolors="white", linewidths=1, zorder=3)
info_text = ax_tube.text(0.99, 1.25, "", transform=ax_tube.transAxes,
fontsize=9, ha="right", va="top")
# --- панель 2: ток каждой катушки во времени ---
for k in range(len(config.stages)):
color = _STAGE_COLORS[k % len(_STAGE_COLORS)]
ax_i.plot(t_ms, currents[k], color=color, linewidth=1.2, label=f"катушка {k}")
for (w0, w1, k) in windows:
ax_i.axvspan(w0 * 1e3, w1 * 1e3, color=_STAGE_COLORS[k % len(_STAGE_COLORS)], alpha=0.08)
ax_i.set_ylabel("ток, А")
ax_i.set_xlabel("время, мс")
ax_i.legend(loc="upper right", fontsize=7, frameon=False, ncols=2)
ax_i.grid(alpha=0.25)
cursor_i = ax_i.axvline(0.0, color="#333", linewidth=1)
# --- панель 3: скорость снаряда во времени ---
ax_v.plot(t_ms, velocities, color="#1f4e8c", linewidth=1.6)
for (w0, w1, k) in windows:
ax_v.axvspan(w0 * 1e3, w1 * 1e3, color=_STAGE_COLORS[k % len(_STAGE_COLORS)], alpha=0.08)
ax_v.set_ylabel("скорость, м/с")
ax_v.set_xlabel("время, мс")
ax_v.grid(alpha=0.25)
if result.feasible and result.exit_v_mps is not None:
ax_v.axhline(result.exit_v_mps, color="#27ae60", linewidth=0.8, linestyle="--")
ax_v.annotate(f"выход {result.exit_v_mps:.1f} м/с",
(t_ms[-1], result.exit_v_mps), ha="right", va="bottom",
fontsize=8, color="#27ae60")
cursor_v = ax_v.axvline(0.0, color="#333", linewidth=1)
v_dot = ax_v.scatter([], [], s=30, c="#1f4e8c", zorder=3)
max_i_per_stage = [max(float(np.max(np.abs(c))), 1e-9) for c in currents]
def _coil_color(intensity): def _coil_color(intensity):
# выкл -> светло-серый; максимум тока -> яркое золото (свечение поля) # выкл -> светло-серый; максимум тока -> яркое золото (свечение поля)
@@ -111,15 +185,18 @@ def animate_run(result: CoilgunResult, config: CoilgunConfig, out_path: str, fps
on = np.array([1.0, 0.78, 0.0]) on = np.array([1.0, 0.78, 0.0])
return tuple(off + (on - off) * t) return tuple(off + (on - off) * t)
def update(frame_i): def update(f):
idx = frame_idx[frame_i] slug_dot.set_offsets([[x_grid[f] * 1e3, 0]])
slug_dot.set_offsets([[positions[idx] * 1e3, 0]]) for k, dots in enumerate(coil_dots):
for stage_idx, dots in enumerate(coil_dots): intensity = abs(i_grid[k][f]) / max_i_per_stage[k]
intensity = abs(currents[stage_idx][idx]) / max_current_per_stage[stage_idx]
dots.set_color(_coil_color(intensity)) dots.set_color(_coil_color(intensity))
dots.set_sizes([600 + 400 * min(intensity, 1.0)]) # чуть раздувается при разряде dots.set_edgecolor(_STAGE_COLORS[k % len(_STAGE_COLORS)])
time_text.set_text(f"t = {times[idx] * 1e3:.3f} мс") dots.set_sizes([500 + 350 * min(intensity, 1.0)])
return [slug_dot, time_text, *coil_dots] info_text.set_text(f"t = {t_grid[f] * 1e3:.2f} мс | v = {v_grid[f]:.1f} м/с | замедление ×{slowmo:.0f}")
cursor_i.set_xdata([t_grid[f] * 1e3])
cursor_v.set_xdata([t_grid[f] * 1e3])
v_dot.set_offsets([[t_grid[f] * 1e3, v_grid[f]]])
return [slug_dot, info_text, cursor_i, cursor_v, v_dot, *coil_dots]
anim = animation.FuncAnimation(fig, update, frames=n_frames, interval=1000 / fps, blit=False) anim = animation.FuncAnimation(fig, update, frames=n_frames, interval=1000 / fps, blit=False)
out_path = str(out_path) out_path = str(out_path)