Анимация: равномерное время (ничего не вырезается) + результат эксперимента в кадре
- кадры на равномерной сетке по ФИЗИЧЕСКОМУ времени вместо индексов массива (раньше плотные точки разряда съедали все кадры, подлёт вырезался) - множитель замедления написан в кадре, событие показано целиком - шапка: выходная скорость, КПД, ступени, энергия банок, масса снаряда - панель тока каждой катушки во времени + панель скорости снаряда, бегущий курсор синхронен с положением в трубе, окна разрядов подкрашены - датчики каждой ступени отмечены на трубе Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user