From a0f02e246a3321522bb88f997cd2c5d7e604092b Mon Sep 17 00:00:00 2001 From: jze9 Date: Wed, 8 Jul 2026 01:21:18 +0500 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F:=20=D1=80=D0=B0=D0=B2=D0=BD=D0=BE=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D0=BE=D0=B5=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20(=D0=BD?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D0=B3=D0=BE=20=D0=BD=D0=B5=20=D0=B2=D1=8B?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=D0=B0=D0=B5=D1=82=D1=81=D1=8F)=20+=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=81=D0=BF=D0=B5=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B2=20=D0=BA=D0=B0=D0=B4=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - кадры на равномерной сетке по ФИЗИЧЕСКОМУ времени вместо индексов массива (раньше плотные точки разряда съедали все кадры, подлёт вырезался) - множитель замедления написан в кадре, событие показано целиком - шапка: выходная скорость, КПД, ступени, энергия банок, масса снаряда - панель тока каждой катушки во времени + панель скорости снаряда, бегущий курсор синхронен с положением в трубе, окна разрядов подкрашены - датчики каждой ступени отмечены на трубе Co-Authored-By: Claude Fable 5 --- src/gausse/report/animate.py | 165 +++++++++++++++++++++++++---------- 1 file changed, 121 insertions(+), 44 deletions(-) diff --git a/src/gausse/report/animate.py b/src/gausse/report/animate.py index 014fac0..37fcc93 100644 --- a/src/gausse/report/animate.py +++ b/src/gausse/report/animate.py @@ -1,10 +1,15 @@ -"""Анимация одного прогона: снаряд летит по трубе, катушки светятся по току. +"""Анимация одного прогона: снаряд в трубе + ток катушек + скорость, всё во времени. -Строит СПЛОШНУЮ траекторию (баллистический подлёт к датчику + разряд по -каждой ступени, сшитые по глобальному времени/координате) и рендерит кадр -за кадром: положение снаряда в трубе + "свечение" каждой катушки -пропорционально её текущему току (яркая -- сильное поле, тусклая -- нет). -Сохраняется как GIF через встроенный в matplotlib PillowWriter. +Честность по времени: кадры берутся на РАВНОМЕРНОЙ сетке по физическому +времени (раньше — по индексам массива, из-за чего плотные точки разряда +съедали все кадры, а подлёт между катушками «вырезался»). Всё событие +показывается целиком в замедлении, множитель замедления написан в кадре. + +Результат эксперимента — в самом кадре: +- шапка: выходная скорость, КПД, энергия банок, масса снаряда, ступени; +- средняя панель: ток каждой катушки во времени (окна разрядов подкрашены); +- нижняя панель: скорость снаряда во времени; +- бегущий курсор на обеих панелях синхронен с положением снаряда в трубе. """ from pathlib import Path @@ -19,19 +24,21 @@ import numpy as np 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): - """Непрерывная траектория снаряда: коаст (по инерции) + разряды по ступеням. - Строится ПОСЛЕДОВАТЕЛЬНО (без сортировки по времени, которая рвала - траекторию): между разрядами снаряд летит по инерции от конца прошлого - разряда до начала следующего; во время разряда берём точно посчитанные - discharge_x/v/i. Глобальная координата = центр катушки ступени + локальный - discharge_x. Ток каждой катушки задан только в окне её собственного разряда. +def _build_global_timeline(result: CoilgunResult, config: CoilgunConfig): + """Непрерывные t/x/v + ток по каждой ступени + окна разрядов. + + Строится последовательно: подлёт (скорость почти постоянна на сантиметрах + межкатушечного зазора) + точные траектории разрядов из решателя. + Глобальная координата = центр катушки ступени + локальный discharge_x. """ n_stages = len(config.stages) - seg_t, seg_x = [], [] + seg_t, seg_x, seg_v = [], [], [] seg_i = [[] for _ in range(n_stages)] + discharge_windows = [] # (t_start, t_end, stage_idx) t_cursor = 0.0 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 start_global_x = float(disch_global_x[0]) - # коаст по инерции от конца прошлого разряда до начала этого + # подлёт от конца прошлого разряда до момента поджига этого if prev_global_x is not None and prev_v and prev_v > 1e-6: - coast_dist = start_global_x - prev_global_x - coast_dur = max(coast_dist / prev_v, 0.0) - n_coast = max(int(coast_dur * fps) + 2, 2) + coast_dist = max(start_global_x - prev_global_x, 0.0) + coast_dur = coast_dist / prev_v + n_coast = 16 ct = np.linspace(0.0, coast_dur, n_coast) seg_t.append(t_cursor + ct) 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): seg_i[s].append(np.zeros(n_coast)) 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_x.append(disch_global_x) + seg_v.append(r.discharge_v) 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)) + discharge_windows.append((t_cursor, t_cursor + float(r.discharge_t[-1]), outcome.stage_index)) t_cursor += float(r.discharge_t[-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) positions = np.concatenate(seg_x) + velocities = np.concatenate(seg_v) 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: if not result.stage_outcomes: raise ValueError("нечего анимировать: нет ни одной ступени в результате") - times, positions, currents = _build_global_timeline(result, config, fps) - n_frames = min(len(times), fps * 10) # ограничение на длину анимации - frame_idx = np.linspace(0, len(times) - 1, n_frames).astype(int) + times, positions, velocities, currents_raw, order, windows = _build_global_timeline(result, config) + currents = [c[order] for c in currents_raw] + 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] - 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() - ax.set_xlim(tube_min * 1e3 - 5, tube_max * 1e3 + 5) - ax.set_ylim(-1, 1) - ax.set_xlabel("положение, мм") - ax.set_yticks([]) - ax.set_title("Пролёт снаряда через ступени") - - ax.axhline(0, color="#888", linewidth=3, zorder=1) # труба + pad = max((tube_max - tube_min) * 0.05, 0.005) + ax_tube.set_xlim((tube_min - pad) * 1e3, (tube_max + pad) * 1e3) + ax_tube.set_ylim(-1, 1) + ax_tube.set_xlabel("положение, мм") + ax_tube.set_yticks([]) + ax_tube.axhline(0, color="#888", linewidth=3, zorder=1) # труба coil_dots = [] for k, c in enumerate(coil_centers): - dot = ax.scatter([c * 1e3], [0], s=600, c="lightgray", edgecolors="#333", linewidths=1.5, zorder=2) - ax.annotate(f"катушка {k}", (c * 1e3, 0), textcoords="offset points", xytext=(0, 22), - ha="center", fontsize=8, color="#555") + color = _STAGE_COLORS[k % len(_STAGE_COLORS)] + dot = ax_tube.scatter([c * 1e3], [0], s=500, c="lightgray", + 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) - slug_dot = ax.scatter([], [], s=180, c="#1f4e8c", edgecolors="white", linewidths=1, zorder=3) - time_text = ax.text(0.02, 0.88, "", transform=ax.transAxes, fontsize=10) + ax_tube.plot([], [], marker="v", linestyle="", color="#555", label="датчики") + 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): # выкл -> светло-серый; максимум тока -> яркое золото (свечение поля) @@ -111,15 +185,18 @@ def animate_run(result: CoilgunResult, config: CoilgunConfig, out_path: str, fps on = np.array([1.0, 0.78, 0.0]) return tuple(off + (on - off) * t) - def update(frame_i): - idx = frame_idx[frame_i] - slug_dot.set_offsets([[positions[idx] * 1e3, 0]]) - for stage_idx, dots in enumerate(coil_dots): - intensity = abs(currents[stage_idx][idx]) / max_current_per_stage[stage_idx] + def update(f): + slug_dot.set_offsets([[x_grid[f] * 1e3, 0]]) + for k, dots in enumerate(coil_dots): + intensity = abs(i_grid[k][f]) / max_i_per_stage[k] dots.set_color(_coil_color(intensity)) - dots.set_sizes([600 + 400 * min(intensity, 1.0)]) # чуть раздувается при разряде - time_text.set_text(f"t = {times[idx] * 1e3:.3f} мс") - return [slug_dot, time_text, *coil_dots] + dots.set_edgecolor(_STAGE_COLORS[k % len(_STAGE_COLORS)]) + dots.set_sizes([500 + 350 * min(intensity, 1.0)]) + 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) out_path = str(out_path)