Zirok05's picture
Update app/simulation/visualization/animation.py
e191b9b verified
Raw
History Blame Contribute Delete
5.31 kB
from matplotlib.animation import FFMpegWriter
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import tempfile
import numpy as np
import os
def minutes_to_time(minutes, start_time="00:00"):
start_hour, start_min = map(int, start_time.split(':'))
total_minutes = start_hour * 60 + start_min + minutes
hour = (total_minutes // 60) % 24
minute = total_minutes % 60
return f"{hour:02d}:{minute:02d}"
def create_simulation_video(frames, specialists_count, second_model_name, fps=24):
if not frames:
return None
# Настройка стиля
plt.style.use('seaborn-v0_8-whitegrid')
fig, axes = plt.subplots(2, 2, figsize=(16, 10), facecolor='#f8f9fa')
plt.subplots_adjust(hspace=0.4, wspace=0.25)
plt.close()
def update(i):
data = frames[i]
for ax in axes.flatten():
ax.clear()
ax.set_facecolor('white')
# 1. ДИНАМИКА ПОТОКА (Локализация)
y_inflow = data['inflow_history']
axes[0, 0].fill_between(range(len(y_inflow)), y_inflow, color='#4361ee', alpha=0.3)
axes[0, 0].plot(range(len(y_inflow)), y_inflow, color='#4361ee', linewidth=2)
axes[0, 0].set_xlim(0, 1440) # Фиксация оси времени
axes[0, 0].set_title("ДИНАМИКА ПОТОКА (заявок/мин)", fontsize=12, fontweight='bold')
axes[0, 0].set_xlabel("Минуты симуляции")
# 2. ЗАГРУЗКА СИСТЕМЫ
y_load = [v * 100 for v in data['load_history']]
axes[0, 1].fill_between(range(len(y_load)), y_load, color='#4cc9f0', alpha=0.3)
axes[0, 1].plot(range(len(y_load)), y_load, color='#4cc9f0', linewidth=2)
axes[0, 1].axhline(y=80, color='#f72585', linestyle='--', alpha=0.6)
axes[0, 1].set_xlim(0, 1440)
axes[0, 1].set_ylim(0, 110)
axes[0, 1].set_title(f"ЗАГРУЖЕННОСТЬ СПЕЦИАЛИСТОВ %: {y_load[-1]:.1f}%", fontsize=12, fontweight='bold')
# 3. HEATMAP И ЛЕГЕНДА
states = np.array(data['specialist_states'])
cols = 20
rows = int(np.ceil(specialists_count / cols))
z = np.zeros((rows, cols))
for idx, val in enumerate(states[:rows * cols]):
z[idx // cols, idx % cols] = val
im = axes[1, 0].imshow(z, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=10)
axes[1, 0].set_title(f"МОНИТОРИНГ: {specialists_count} СПЕЦИАЛИСТОВ", fontsize=12, fontweight='bold')
axes[1, 0].axis('off')
# Добавляем текстовую легенду под хитмапом
legend_text = "Цвета: Зеленый (Свободен) → Желтый (3-5 мин) → Красный (8+ мин)"
axes[1, 0].text(0.5, -0.1, legend_text, ha='center', transform=axes[1, 0].transAxes, fontsize=10)
# --- 4. РАЗДЕЛЕННЫЕ ОЧЕРЕДИ И СТАТИСТИКА ---
ax_stat = axes[1, 1]
ax_stat.clear()
ax_stat.axis('off')
# Цвета для очередей (краснеют, если очередь > 50)
q_mod_color = '#991b1b' if data['queue'] > 50 else '#166534'
q_biz_color = '#991b1b' if data.get('business_queue', 0) > 50 else '#1e293b'
ax_stat.text(0.25, 0.9, "ОЧЕРЕДЬ\n(МОДЕЛИ)", fontsize=10, ha='center', fontweight='bold')
ax_stat.text(0.25, 0.78, f"{data['queue']}", fontsize=26, ha='center', fontweight='bold', color=q_mod_color)
ax_stat.text(0.75, 0.9, "ОЧЕРЕДЬ\n(БИЗНЕС ПРАВИЛА)", fontsize=10, ha='center', fontweight='bold')
ax_stat.text(0.75, 0.78, f"{data.get('business_queue', 0)}", fontsize=26, ha='center', fontweight='bold',
color=q_biz_color)
# Сводная таблица
cum = data['cumulative']
stats_text = (
f"Итоговые показатели к {data['time_str']}\n"
f"--------------------------------------\n"
f"ОБРАБОТАНО ВСЕГО: {cum['total_processed']}\n"
f"Авто-одобрено: {cum['auto_approved']}\n"
f"Авто-отказы: {cum['auto_declined']}\n"
f"Ручной разбор (модель): {cum['manual_processed']}\n"
f"Ручной разбор (бизнес правила): {cum['business_manual_processed']}\n"
f"--------------------------------------\n"
f"Используемая модель: {second_model_name}"
)
ax_stat.text(0.5, 0.3, stats_text, fontsize=10, fontfamily='monospace',
ha='center', va='center', transform=ax_stat.transAxes,
bbox=dict(facecolor='#f8f9fa', alpha=1, boxstyle='round,pad=1', edgecolor='#dee2e6'))
return axes.flatten()
ani = animation.FuncAnimation(fig, update, frames=len(frames), interval=1000 / fps)
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
writer = animation.FFMpegWriter(fps=fps, bitrate=2000, extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p'])
ani.save(tmp_file.name, writer=writer)
return tmp_file.name