К основному контенту

Повышение быстродействия

 

В заметке Тепловое взаимодействие ячеек в соте было промоделировано изменение температуры для двустороннего кластера сот размером 11 на 11 ячеек при помещении туда четырех пчел и исходной температуре 12°С. Для каждой пустой ячейки решалась система из 10 уравнений - по количеству граней плюс уравнение для воздуха, а для ячейки с пчелой - еще дополнительно 3 уравнения: для торакса, головы и брюшка. Решение находилось методом fsolve библиотеки scipy - это численный метод (обычно алгоритм Минпака), который внутри себя подбирает корни системы нелинейных уравнений. Моделирование производилось с небольшим шагом по времени - 0.01 с, т.к. увеличение шага приводило к расхождению решения (примерно с шага 0.03 с). Поскольку для достижения требуемой точности на каждом шаге проводилось множество итераций, решение находилось медленно. Десять минут (шестьсот секунд) реального времени моделировалось примерно за 300 секунд. Это значит, что рамка Дадана считалась бы около часа, а весь улей более десяти часов. Десять минут - за 10 часов! Это, конечно же, неприемлемо, если иметь в виду цель - создание цифрового двойника семьи в улье.

Я попытался где можно векторизовать уравнения и применить известные методы ускорения в Python, в частности надстройку numba, но это ускорило вычисления не более, чем в полтора раза. Тогда я опять обратился за помощью к ИИ, и на этот раз он справился (мой первый опыт, как вы помните, был не очень удачным).

Суть предложений ИИ сводилась к двум: изменению алгоритма решения задачи (системы уравнений) на метод Эйлера и жесткой структуре данных в виде массивов: массива температур, массива топологии (связей), массива теплоемкостей и т.д. Оба эти предложения нацелены на то, чтобы дать надстройке numba проявить свою максимальную эффективность

По алгоритму

Сопоставим теплоемкости:

  • Теплоемкость воска ячейки \(C_{wax}\) : 124 мДж/°C.
  • Теплоемкость пчелы \(C_{bee}\): 349 мДж/°C.
  • Теплоемкость воздуха в ячейке с пчелой \(C_{air\_bee}\): 0.145 мДж/°C.
  • Теплоемкость воздуха в пустой ячейке \(C_{air\_cell}\): около 0.367 мДж/°C.
    Разница между воздухом и воском — в сотни раз. Воздух внутри микро-объема ячейки является малоинерционной средой. Поэтому, можно считать воздух в ячейке безынерционным - пренебречь его теплоемкостью. В этом случае температура воздуха в ячейке на текущем шаге — это просто средневзвешенная температура окружения:

$$T_{ai}=\frac{\frac{T_{th}}{R_{th\_ai}}+\frac{T_{hd}}{R_{hd\_ai}}+\frac{T_{ab}}{R_{ab\_ai}}+\sum _{k=0}^{8}\frac{T_{wall}[k]}{R_{ai\_wall}[k]}+\frac{T_{ambient}}{R_{ab\_}}}{\frac{1}{R_{th\_ai}}+\frac{1}{R_{hd\_ai}}+\frac{1}{R_{ab\_ai}}+\sum _{k=0}^{8}\frac{1}{R_{ai\_wall}[k]}+\frac{1}{R_{ab\_}}}$$

Прямой пересчет температур (явная схема Эйлера). 

Раз у нас есть \(T_{ai}\), новые температуры для следующего шага времени (t + dt) вычисляются за 1 такт безо всяких итераций, например, для температуры головы: 

$$T_{hd}=T_{hd\_prev}+\frac{dt}{C_{hd}}\cdot \left(p_{s\_hd\_prev}+\frac{T_{th\_prev}-T_{hd\_prev}}{R_{th\_hd}}-\frac{T_{hd\_prev}-T_{ai\_prev}}{R_{hd\_ai}}-q_{evap\_hd\_prev}\right)$$ 

Аналогично для \(T_{hd}, T_{th}\) и всех 9 граней \(T_{wall}\).

По массивам

Основные массивы и их формы:

  1. Массив теплоемкостей C_grid (N_cells×13)
  2. Массив температур ячеек T_grid (N_cells×13)
  3. Массив топологии Topology (N_cells×9)
  4. Массив соседей по граням Neighbor_Faces(N_cells×9)
  5. Массив статусов States(N_cells)
    Такое структурирование данных позволяет при моделировании одного шага во времени убрать цикл по ячейкам, не перебирать составляющие (13 - девять стенок, три части тела пчелы и воздух). Т.е. в программе остается один цикл - по времени.

Эти предложения сам же ИИ и оформил в скрипт.
Задав такой же кластер 11×11 ячеек и поместив там 4 пчелы, можно произвести сравнение результатов нового алгоритма и старого. Напомню, ранее такой расчет для 600 секунд реального времени выполнялся примерно за 300 секунд при шаге моделирования по времени в 0.01 с, т.е. число шагов составляло 60000,а время выполнения одного шага - 0.05 с. При таком же шаге моделирования новая программа выполнила расчет этих 600 с примерно за 0.8 секунды, т.е. в 375 раз быстрее!
Этот результат уже может позволить моделировать улей быстрее реального времени, и вновь появляется перспектива создания цифрового двойника. Однако, что насчет устойчивости? Мы отмечали, что при увеличении шага примерно с 0.03 секунд устойчивость теряется.
Постепенное увеличение шага в новом алгоритме показало, что устойчивость теряется примерно с шага в 0.3 секунды. Это значит, что можно с запасом поставить шаг моделирования 0.1 секунды, т.е. увеличить его в 10 раз по сравнению со старым алгоритмом. Общее время моделирования 600 секунд для нашего кластера 11×11 ячеек в этом случае составило 0.08 секунды, что в 3750 раз быстрее исходного.

В этот момент чат с ИИ практически переполнился контекстом (это особенность современного этапа развития всех ИИ-чатов), а текст программы в ходе доработки стал не очень читабельным. Поэтому я попросил другой ИИ-чат подготовить текст программы к публикации. "Свежий" ИИ творчески отнесся к делу и предложил еще две возможности поднять быстродействие:

  1. добавил fastmath=True в обертку @njit (ускоряет математические операции).
  2. убрал .copy() внутри шага симуляции; вместо этого используется классический трюк "двойного буфера" (double-buffering): мы передаем два массива (T_current и T_next), пишем в T_next, а затем просто меняем их местами: T_cur, T_next = T_next, T_cur. Это убирает аллокацию памяти на каждом шаге и снимает нагрузку с Garbage Collector. 
  3. В результате время моделирования уменьшилось до 0.055 с, т.е. более, чем в 5000 раз по сравнению с исходным.

Быстродействие, таким образом, увеличили. Но известно, что метод Эйлера сам по себе вносит погрешность в решение для нелинейных систем (а она у нас существенно нелинейная). Кроме того, в новом алгоритме мы пренебрегли теплоемкостью воздуха при расчете температуры воздуха в ячейке, что тоже могло внести дополнительную погрешность. Сравним алгоритмы по точности.







В левой колонке результаты старого алгоритма, в правой - нового. Видим, что качественно картина не поменялась - пчелы греют сот вокруг себя. Но расхождения действительно есть: в новом алгоритме мощность, отдаваемая пчелами несколько больше, поэтому и температуры в ячейках с пчелами и окрестностях - выше. При этом разница составляет менее 0.2°С. Это является платой за повышение быстродействия. На данном этапе полагаю такую погрешность допустимой для моделирования. Если в дальнейшем окажется, что такой точности недостаточно - займемся повышением точности, будем решать проблемы по мере их поступления.
P.S. Кстати рамка Дадана, которая ранее считалась бы более часа, сейчас считается менее двух секунд:



🐝 Python-скрипт

  """  Программа к заметке "Повышение производительности" """  

import time  
import math  
import numpy as np  
from numba import njit  
from scipy.optimize import fsolve  
import matplotlib.pyplot as plt  
from mpl_toolkits.axes_grid1.inset_locator import inset_axes  
  
# ==========================================================  
# 1. ФИЗИЧЕСКИЕ ПАРАМЕТРЫ И ГЕОМЕТРИЯ (Бывший config_11x11.py)  
# ==========================================================  
# Сетка и условия  
cols, rows = 11, 11  
variant = 2          # 1: Тонкий сот, 2: Коричневый сот, 3: Толстый сот  
T_ambient = 12.0     # Начальная температура воздуха (°C)  
  
# Пчела  
d_bee = 0.004  
s_r = math.pi * (d_bee / 2) ** 2  
h_head, h_thorax, h_ab = 0.0015, 0.0045, 0.006  
h_total = h_head + h_thorax + h_ab  
A_hd = math.pi * d_bee * h_head + s_r  
A_th = math.pi * d_bee * h_thorax  
A_ab = math.pi * d_bee * h_ab + s_r  
V_bee = h_total * s_r  
  
ke0 = 3e-5  
Pmin = 0.2e-3  
k_hd, k_ab = 0.065, 0.042  
m_head, m_thorax, m_ab = 10.25e-6, 32.5e-6, 57e-6  
c = 3500  
C_hd, C_th, C_ab = m_head * c, m_thorax * c, m_ab * c  
  
# Воздух  
ρ_air, k_air, c_air = 1.205, 0.025, 1005  
  
# Воск и ячейки  
L_w = [0.0001, 0.00014, 0.0002]  
L_b = [0.00013, 0.00055, 0.0012]  
c_w = [3300., 3000., 2700.]  
λ_w = [0.235, 0.1608, 0.055]  
eps_w = [0.22, 0.5, 0.9]  
  
L_wall = L_w[variant - 1]  
L_bottom = L_b[variant - 1]  
c_wax = c_w[variant - 1]  
λ_wax = λ_w[variant - 1]  
ρ_wax = 960  
  
thread = 5.4e-3  
d_cell = thread - L_wall  
h_cell = 12e-3  
dh = 1e-3  
a = d_cell / 2. / np.cos(np.radians(30))  
A_lateral = a * (h_cell - dh / 2.)  
d1, d2 = d_cell, np.sqrt(a ** 2 + dh ** 2)  
A_bottom = d1 * d2 / 2.  
  
C_wax_lateral = ρ_wax * A_lateral * L_wall * c_wax  
C_wax_bottom = ρ_wax * A_bottom * L_bottom * c_wax  
A_wall_total = 6 * A_lateral + 3 * A_bottom  
area_hex = (3 * math.sqrt(3) / 2) * a ** 2  
A_entrance = area_hex  
V_cell = area_hex * h_total  
  
C_air_cell = ρ_air * V_cell * c_air  
C_air_bee = ρ_air * (V_cell - V_bee) * c_air  
  
# Излучение и тепловые сопротивления  
sigma = 5.67e-8  
t0 = -273.15  
eps = 0.22  
eps_wall = eps_w[variant - 1]  
se = sigma * eps_wall  
se_th, se_hd, se_ab_ = se * A_th, se * (A_hd - s_r), se * (A_ab - s_r)  
  
h_in = k_air / ((d_cell - d_bee) / 2)  
h_entrance = 11.0  
R_th_ai = 1 / (h_in * A_th)  
R_hd_ai = 1 / (h_in * A_hd)  
R_ab_ai = 1 / (h_in * (A_ab - s_r))  
R_ab_ = 1 / (h_entrance * s_r)  
hA_ = h_entrance * (A_entrance - s_r)  
hA = h_entrance * A_entrance  
R_head_air_open = 1 / (h_entrance * A_hd)  
R_thorax_air_open = 1 / (h_entrance * A_th)  
R_ab_air_open = 1 / (h_entrance * A_ab)  
  
# Режимы работы (0.0 - обычный, 45e-3 - режим печки)  
P_warm_up = 0.0  
  
# ==========================================================  
# 2. ГЕНЕРАЦИЯ ТОПОЛОГИИ И СЕТКИ  
# ==========================================================  
CELL_EMPTY, CELL_BEE = 0, 5  
I_HD, I_TH, I_AB, I_AI = 0, 1, 2, 3  
  
# Массивы сопротивлений и теплоемкостей для Numba  
R_ai_wall_arr = np.array([L_wall / (k_air * A) for A in [A_lateral] * 6 + [A_bottom] * 3], dtype=np.float64)  
R_wall_self = np.array([L_wall / (λ_wax * A_lateral)] * 6 + [L_bottom / (λ_wax * A_bottom)] * 3, dtype=np.float64)  
C_wall_base = np.array([C_wax_lateral] * 6 + [C_wax_bottom] * 3, dtype=np.float64)  
  
# Генерация координат сот  
cells = []  
for side in ["Y", "^"]:  
    for col in range(-int(cols / 2), int(cols / 2) + 1):  
        for row in range(-int(rows / 2), int(rows / 2) + 1):  
            cells.append((0, side, col, row))  
  
cell_to_index = {cell: i for i, cell in enumerate(cells)}  
N_CELLS = len(cells)  
  
EVEN_ROW_DELTAS = [(-1, 0), (-1, -1), (0, -1), (1, 0), (0, 1), (-1, 1)]  
ODD_ROW_DELTAS = [(1, 0), (1, -1), (0, -1), (-1, 0), (0, 1), (1, 1)]  
  
def get_neighbor_for_face(side, col, row, face_id):  
    if face_id < 6:  
        deltas = ODD_ROW_DELTAS if (row % 2 == 1) else EVEN_ROW_DELTAS  
        if side == "^":  
            deltas = EVEN_ROW_DELTAS if (row % 2 == 1) else ODD_ROW_DELTAS  
        dc, dr = deltas[face_id]  
        return (0, side, int(col + dc), int(row + dr))  
    else:  
        k = face_id - 6  
        if side == "Y":  
            bottom_list = [(col, row - 1), (col, row), (col - 1, row)] if row % 2 == 0 else [(col, row - 1), (col, row), (col + 1, row)]  
            n_side = "^"  
        else:  
            bottom_list = [(col, row), (col, row + 1), (col + 1, row)] if row % 2 == 0 else [(col, row), (col, row + 1), (col - 1, row)]  
            n_side = "Y"  
        n_col, n_row = bottom_list[k]  
        return (0, n_side, n_col, n_row)  
  
# Построение матриц топологии  
Topology = np.full((N_CELLS, 9), -1, dtype=np.int32)  
Neighbor_Faces = np.full((N_CELLS, 9), -1, dtype=np.int32)  
all_cells_set = set(cells)  
  
for i, cell in enumerate(cells):  
    _, side, col, row = cell  
    for face_id in range(9):  
        n_addr = get_neighbor_for_face(side, col, row, face_id)  
        if n_addr in all_cells_set:  
            Topology[i, face_id] = cell_to_index[n_addr]  
            _, n_side, n_col, n_row = n_addr  
            for rev_f in range(9):  
                if get_neighbor_for_face(n_side, n_col, n_row, rev_f) == (0, side, col, row):  
                    Neighbor_Faces[i, face_id] = rev_f  
                    break  
  
# Инициализация состояний  
States = np.full(N_CELLS, CELL_EMPTY, dtype=np.int32)  
# В оригинале было 4 пчелы, но для теста оставлена 1 центральная.  
# Если нужно 4, раскомментируйте строку ниже.  
# bee_cells = [(0, 'Y', 0, 0)]  
bee_cells = [(0, 'Y', 0, 0), (0, 'Y', 0, 1), (0, 'Y', 0, 2), (0, 'Y', 2, -1)]  
bee_idx = [cell_to_index[cell] for cell in bee_cells]  
for idx in bee_idx:  
    States[idx] = CELL_BEE  
  
# ==========================================================  
# 3. РАСЧЕТ НАЧАЛЬНЫХ ТЕМПЕРАТУР (Тепловой баланс отдельной пчелы)  
# ==========================================================  
def metabolic_power(T):  
    return max((29.133 - 0.739 * T) * 1e-3, Pmin)  
  
def calculate_initial_temperatures(T_air_val):  
    l_val = (1.3493 * np.exp(-0.058 * T_air_val)) * 1e-3  
    R_th_hd_val = l_val / (k_hd * s_r)  
    R_th_ab_val = l_val / (k_ab * s_r)  
  
    def equations(vars_val):  
        Thd_v, Tth_v, Tab_v = vars_val  
        q_evap = ke0 * Tth_v  
        P_rest = metabolic_power(T_air_val)  
  
        p_s_hd = -eps * sigma * ((Thd_v - t0)**4 - (T_air_val - t0)**4) * A_hd  
        p_s_th = -eps * sigma * ((Tth_v - t0)**4 - (T_air_val - t0)**4) * A_th  
        p_s_ab = -eps * sigma * ((Tab_v - t0)**4 - (T_air_val - t0)**4) * A_ab  
  
        eq1 = p_s_hd + (Tth_v - Thd_v)/R_th_hd_val - (Thd_v - T_air_val)/R_head_air_open - q_evap  
        eq2 = P_rest + p_s_th - (Tth_v - Thd_v)/R_th_hd_val - (Tth_v - Tab_v)/R_th_ab_val - (Tth_v - T_air_val)/R_thorax_air_open  
        eq3 = p_s_ab + (Tth_v - Tab_v)/R_th_ab_val - (Tab_v - T_air_val)/R_ab_air_open - q_evap  
        return [eq1, eq2, eq3]  
  
    return fsolve(equations, [T_air_val + 10, T_air_val + 15, T_air_val + 10], xtol=1e-8)  
  
Thd_init, Tth_init, Tab_init = calculate_initial_temperatures(T_ambient)  
T_bee_avg_init = (m_head * Thd_init + m_thorax * Tth_init + m_ab * Tab_init) / (m_head + m_thorax + m_ab)  
  
# ==========================================================  
# 4. ПОДГОТОВКА МАТРИЦ ДЛЯ NUMBA  
# ==========================================================  
C_grid = np.zeros((N_CELLS, 13), dtype=np.float64)  
for i in range(N_CELLS):  
    C_grid[i, 4:13] = C_wall_base  
    if States[i] == CELL_BEE:  
        C_grid[i, I_HD] = C_hd  
        C_grid[i, I_TH] = C_th  
        C_grid[i, I_AB] = C_ab  
        C_grid[i, I_AI] = C_air_bee  
    else:  
        C_grid[i, I_AI] = C_air_cell  
  
# Двойной буфер для температур (избегаем аллокаций внутри цикла!)  
T_grid_A = np.full((N_CELLS, 13), T_ambient, dtype=np.float64)  
T_grid_B = np.full((N_CELLS, 13), T_ambient, dtype=np.float64)  
  
for t_idx in bee_idx:  
    T_grid_A[t_idx, I_HD] = Thd_init  
    T_grid_A[t_idx, I_TH] = Tth_init  
    T_grid_A[t_idx, I_AB] = Tab_init  
    T_grid_B[t_idx, I_HD] = Thd_init  
    T_grid_B[t_idx, I_TH] = Tth_init  
    T_grid_B[t_idx, I_AB] = Tab_init  
  
# ==========================================================  
# 5. ЯДРО СИМУЛЯЦИИ (NUMBA JIT)  
# ==========================================================  
# БЫЛО:  
# @njit(fastmath=True, cache=True)  
# def do_cluster_step(T_cur, T_next, Topology, Neighbor_Faces, States, C_grid, dt, T_ambient):  
  
# СТАЛО:  
@njit(fastmath=True, cache=True)  
def do_cluster_step(T_cur, T_next, Topology, Neighbor_Faces, States, C_grid, dt, T_ambient, heater_idx):  
    Q_entrance_Y = 0.0  
    Q_entrance_X = 0.0  
    half_cells = len(T_cur) // 2  
  
    for i in range(len(T_cur)):  
        state = States[i]  
        Thd = T_cur[i, I_HD]  
        Tth = T_cur[i, I_TH]  
        Tab = T_cur[i, I_AB]  
  
        # Расчет температуры воздуха в ячейке (Tai)  
        if state == CELL_BEE:  
            num = Tth/R_th_ai + Thd/R_hd_ai + Tab/R_ab_ai + T_ambient/R_ab_  
            den = 1.0/R_th_ai + 1.0/R_hd_ai + 1.0/R_ab_ai + 1.0/R_ab_  
        else:  
            R_ent = 1.0 / (h_entrance * A_entrance)  
            num = T_ambient / R_ent  
            den = 1.0 / R_ent  
  
        for f in range(9):  
            num += T_cur[i, 4 + f] / R_ai_wall_arr[f]  
            den += 1.0 / R_ai_wall_arr[f]  
  
        Tai = num / den  
        T_next[i, I_AI] = Tai  
  
        is_side_Y = (i < half_cells)  
  
        # Расчет теплопотерь  
        if state == CELL_BEE:  
            Q_cell = hA_ * (Tai - T_ambient)  
            Q_ab = (Tab - T_ambient) / R_ab_  
            if is_side_Y: Q_entrance_Y += (Q_cell + Q_ab)  
            else:         Q_entrance_X += (Q_cell + Q_ab)  
        else:  
            Q_cell = hA * (Tai - T_ambient)  
            if is_side_Y: Q_entrance_Y += Q_cell  
            else:         Q_entrance_X += Q_cell  
  
        # Расчет температур стенок  
        T_wall_avg = 0.0  
        for f in range(9):  
            T_wall_avg += T_cur[i, 4 + f]  
        T_wall_avg /= 9.0  
  
        p_s_th = -se_th * ((Tth - t0)**4 - (T_wall_avg - t0)**4)  
        p_s_hd = -se_hd * ((Thd - t0)**4 - (T_wall_avg - t0)**4)  
        p_s_ab = -se_ab_ * ((Tab - t0)**4 - (T_wall_avg - t0)**4)  
        p_s_total = p_s_th + p_s_hd + p_s_ab  
  
        for f in range(9):  
            Q_from_air = (Tai - T_cur[i, 4 + f]) / R_ai_wall_arr[f]  
            neighbor_idx = Topology[i, f]  
  
            if neighbor_idx != -1:  
                n_face = Neighbor_Faces[i, f]  
                Q_from_neighbor = (T_cur[neighbor_idx, 4 + n_face] - T_cur[i, 4 + f]) / R_wall_self[f]  
            else:  
                Q_from_neighbor = (T_ambient - T_cur[i, 4 + f]) / R_wall_self[f]  
  
            p_s_to_wall_k = ((A_lateral if f < 6 else A_bottom) / A_wall_total) * p_s_total if state == CELL_BEE else 0.0  
  
            T_next[i, 4 + f] = T_cur[i, 4 + f] + (dt / C_grid[i, 4 + f]) * (Q_from_air + Q_from_neighbor + p_s_to_wall_k)  
  
        # Расчет температур органов пчелы  
        if state == CELL_BEE:  
            P_rest = (29.133 - 0.739 * Tai) * 1e-3  
            if P_rest < Pmin: P_rest = Pmin  
  
            # Добавляем мощность "печки" только для центральной пчелы (индекс 0 в bee_idx)  
            # БЫЛО:            # P_local = P_warm_up if i == bee_idx[0] else 0.0  
            # СТАЛО:            P_local = P_warm_up if i == heater_idx else 0.0  
  
            l_dyn = (1.3493 * np.exp(-0.058 * Tai)) * 1e-3  
            R_th_hd_dyn = l_dyn / (k_hd * s_r)  
            R_th_ab_dyn = l_dyn / (k_ab * s_r)  
  
            T_next[i, I_TH] = Tth + (dt / C_grid[i, I_TH]) * (  
                P_rest + P_local + p_s_th - (Tth - Thd)/R_th_hd_dyn - (Tth - Tab)/R_th_ab_dyn - (Tth - Tai)/R_th_ai)  
  
            T_next[i, I_HD] = Thd + (dt / C_grid[i, I_HD]) * (  
                (Tth - Thd)/R_th_hd_dyn - (Thd - Tai)/R_hd_ai - ke0 * Tth)  
  
            T_next[i, I_AB] = Tab + (dt / C_grid[i, I_AB]) * (  
                p_s_ab + (Tth - Tab)/R_th_ab_dyn - (Tab - Tai)/R_ab_ai - (Tab - T_ambient)/R_ab_ - ke0 * Tth)  
  
    return Q_entrance_Y, Q_entrance_X  
  
# ==========================================================  
# 6. ЦИКЛ СИМУЛЯЦИИ И СБОР ДАННЫХ  
# ==========================================================  
dt_sim = 0.1  
time_total = 600.0  
steps = int(time_total / dt_sim)  
  
# Предаллокация списков для логов  
log_time = []  
log_T_hd, log_T_center, log_T_ab, log_T_ai_bee, log_T_wall_bee, log_T_bee_avg = [], [], [], [], [], []  
log_Power, log_Q_Y, log_Q_X = [], [], []  
  
print(f"Задана сетка {cols}x{rows}. Запуск скоростной симуляции...")  
t_idx = bee_idx[0]  
  
# Прогрев Numba (первый вызов всегда медленный из-за компиляции)  
do_cluster_step(T_grid_A, T_grid_B, Topology, Neighbor_Faces, States, C_grid, dt_sim, T_ambient, bee_idx[0])  
  
start_time = time.time()  
T_cur, T_next = T_grid_A, T_grid_B  
  
for step in range(steps):  
    # Считаем шаг, записывая результаты из T_cur в T_next  
    Q_Y, Q_X = do_cluster_step(T_cur, T_next, Topology, Neighbor_Faces, States, C_grid, dt_sim, T_ambient, bee_idx[0])  
  
    # Меняем буферы местами (ссылки, а не копирование данных!)  
    T_cur, T_next = T_next, T_cur  
  
    if step % 10 == 0:  
        t = step * dt_sim  
        log_time.append(t)  
        log_T_hd.append(T_cur[t_idx, I_HD])  
        log_T_center.append(T_cur[t_idx, I_TH])  
        log_T_ab.append(T_cur[t_idx, I_AB])  
        log_T_ai_bee.append(T_cur[t_idx, I_AI])  
        log_T_wall_bee.append(T_cur[t_idx, 4])  
  
        T_bee_avg = (m_head * T_cur[t_idx, I_HD] + m_thorax * T_cur[t_idx, I_TH] + m_ab * T_cur[t_idx, I_AB]) / (m_head + m_thorax + m_ab)  
        log_T_bee_avg.append(T_bee_avg)  
  
        P_rest = max((29.133 - 0.739 * T_cur[t_idx, I_AI]) * 1e-3, Pmin)  
        log_Power.append((P_rest + P_warm_up) * 1000)  
        log_Q_Y.append(Q_Y * 1000)  
        log_Q_X.append(Q_X * 1000)  
  
time_calc = time.time() - start_time  
print(f"Общее время: {time_calc:.3f} c ({steps/time_calc:.0f} шагов/сек)")  
  
# ==========================================================  
# 7. ВИЗУАЛИЗАЦИЯ  
# ==========================================================  
# График 1: Динамика температур  
plt.figure(figsize=(8, 5))  
plt.scatter(log_time, log_T_center, s=2, c='r', label='Торакс', zorder=5)  
plt.plot(log_time, log_T_hd, 'b-', label='Голова', linewidth=1.5)  
plt.plot(log_time, log_T_ab, 'g-', label='Брюшко', linewidth=1.5)  
plt.plot(log_time, log_T_ai_bee, 'c--', label='Воздух в ячейке', linewidth=1.5)  
plt.plot(log_time, log_T_wall_bee, 'm--', label='Стенка ячейки', linewidth=1.5)  
plt.plot(log_time, log_T_bee_avg, 'k:', label='Средняя T пчелы', linewidth=2)  
plt.axhline(T_ambient, color='gray', linestyle=':', label=f'Наружный воздух ({T_ambient}°C)')  
plt.xlabel('Время (с)'); plt.ylabel('Температура (°C)')  
plt.title(f'Тепловой режим пчелы в кластере. Вариант {variant}')  
plt.legend(); plt.grid(True, linestyle=':'); plt.tight_layout(); plt.show()  
  
# График 2: Баланс мощности  
fig, ax1 = plt.subplots(figsize=(8, 5))  
ax1.plot(log_time, log_Power, 'orange', linewidth=2, label='Мощность термогенеза')  
ax1.set_xlabel('Время (с)'); ax1.set_ylabel('Мощность (мВт)', color='orange')  
ax2 = ax1.twinx()  
ax2.plot(log_time, log_Q_Y, 'steelblue', linewidth=2, label='Потери в улочку Y')  
ax2.plot(log_time, log_Q_X, 'crimson', linewidth=2, label='Потери в улочку ^')  
ax2.set_ylabel('Теплопотери (мВт)', color='k')  
lines1, labels1 = ax1.get_legend_handles_labels()  
lines2, labels2 = ax2.get_legend_handles_labels()  
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper right')  
ax1.grid(True, linestyle=':'); ax1.set_title('Баланс мощности: генерация vs потери'); plt.show()  
  
# График 3: 2D-карта температур сот  
temp_Y_clean, temp_X_clean = {}, {}  
for i, cell in enumerate(cells):  
    _, side, col, row = cell  
    T_air_final = T_cur[i, I_AI]  
    if side == 'Y': temp_Y_clean[(col, row)] = T_air_final  
    else: temp_X_clean[(col, row)] = T_air_final  
  
size = 1.0  
width = np.sqrt(3) * size  
dy = 1.5 * size  
  
def hex_corner(center, i):  
    angle_rad = np.pi / 180 * (60 * i - 30)  
    return (center[0] + size * np.cos(angle_rad), center[1] + size * np.sin(angle_rad))  
  
def primal_to_pixel(col, row): return (width * (col + 0.5 * (row & 1)), -dy * row)  
def dual_to_pixel(col, row): return (width * (col + 0.5 * (1 - (row & 1))), -dy * row - size / 2)  
  
fig, ax = plt.subplots(figsize=(8, 6))  
ax.set_aspect('equal')  
all_temps = list(temp_Y_clean.values()) + list(temp_X_clean.values())  
vmin, vmax = min(all_temps), max(all_temps)  
  
for col in range(-int(cols/2), int(cols/2)+1):  
    for row in range(-int(rows/2), int(rows/2)+1):  
        if (col, row) in temp_Y_clean:  
            x, y = primal_to_pixel(col, row)  
            corners = [hex_corner((x, y), i) for i in range(6)]  
            hex_x, hex_y = zip(*corners)  
            color = plt.cm.viridis((temp_Y_clean[(col, row)] - vmin) / max(vmax - vmin, 1e-5))  
            ax.fill(hex_x, hex_y, color=color, edgecolor='steelblue', linewidth=1.2, alpha=0.8)  
            if (col, row) == (0, 0): ax.fill(hex_x, hex_y, facecolor='none', edgecolor='black', linewidth=3)  
            ax.text(x, y, f"{temp_Y_clean[(col, row)]:.2f}", ha='center', va='center', fontsize=7, color='b')  
  
for col in range(-int(cols/2), int(cols/2)+1):  
    for row in range(-int(rows/2), int(rows/2)+1):  
        if (col, row) in temp_X_clean:  
            x, y = dual_to_pixel(col, row)  
            corners = [hex_corner((x, y), i) for i in range(6)]  
            hex_x, hex_y = zip(*corners)  
            ax.plot(hex_x + (hex_x[0],), hex_y + (hex_y[0],), color='crimson', linewidth=1.2)  
            ax.text(x, y, f"{temp_X_clean[(col, row)]:.2f}", ha='center', va='center', fontsize=7, color='crimson')  
  
ax.axis('off'); ax.autoscale_view()  
sm = plt.cm.ScalarMappable(cmap=plt.cm.viridis, norm=plt.Normalize(vmin=vmin, vmax=vmax))  
sm.set_array([])  
plt.colorbar(sm, ax=ax, shrink=0.8).set_label('Температура воздуха (°C)', fontsize=10)  
plt.title(f'Температурное поле сот. Вариант {variant}\n(Синяя — сторона "Y", Красная — сторона "^")')  
plt.tight_layout(); plt.show()  
  
# Финальный вывод в консоль  
print(f"\nФинальные результаты для центральной пчелы:")  
print(f"Торакс: \t\t{log_T_center[-1]:.2f}°C (было {Tth_init:.2f}°C)")  
print(f"Средняя пчела: \t{log_T_bee_avg[-1]:.2f}°C (было {T_bee_avg_init:.2f}°C)")  
print(f"Воздух в ячейке: \t{log_T_ai_bee[-1]:.2f}°C")  
print(f"Мощность: \t\t{log_Power[-1]:.2f} мВт")
    

Комментарии

Популярные сообщения из этого блога

Температура и мощность термогенеза пчелы

Введение Помню, в школе на уроках биологии рассказывали про теплокровных и холоднокровных животных; насекомых, а, значит, и пчелу относили к холоднокровным. Поэтому когда первый раз прочитал, что пчела - пойкилотермное животное, слегка насторожился. Оказалось - зря, это тоже, что холоднокровное, но "по-научному", ещё встречается "эктотермное". А теплокровные - гомойтермные или эндотермные; они способны сохранять постоянную температуру тела, независимо от температуры окружающей среды - это птицы и млекопитающие, остальные - холоднокровные. Итак, согласимся: пчела - пойкилотермное животное. Однако, "всё не так однозначно". Например голый землекоп - холоднокровное млекопитающее. А в мае 2015 года нашли  "полностью теплокровную рыбу"  . Оказалось, правда, на мой взгляд, не полностью - она способна держать температуру всего на 5°С выше окружающей. Выделяют отдельную группу гетеротермных животных, куда относят как некоторых холоднокровных, так и н...

Режим печки

Пчеле для полёта необходима температура торакса не ниже 27°C. Однако, если мы посмотрим на график температуры пчелы в покое в заметке Температура и мощность термогенеза пчелы , то увидим, что это условие обеспечивается в пасмурную погоду только при температуре воздуха выше 17°C, а на солнце - выше 10°С. Но первые очистительные облёты пчёлы делают и в пасмурную погоду уже при температуре 10-12°C. Для того, чтобы взлететь пчела разогревает торакс до рабочей температуры путем изометрического сокращения летательных мышц. При этом махания крыльями не происходит. Такой режим можно назвать режимом печки. Работа этих мышц осуществляется с КПД 4.4%, остальное идёт на нагрев, т.е. КПД такой печки составляет 95.6%!  В заметке про термогенез  была сделана попытка оценить какая дополнительная мощность нужна пчеле, чтобы поднимать свою температуру со скоростью 2°C в минуту, получилось - нужно 6.3 мВт. Сделано это было ещё до создания модели (по крайней мере без её применения). Но теперь-то...

Выступил на конференции АЕП-2025

  22.11.2025 состоялась ежегодная конференция Ассоциации естественного пчеловодства.  Она заняла весь день. Было много интересных докладов. Иван Пигарёв подвел итоги работы ассоциации за год и планах на будущий - в центре внимания новый проект в Окском государственном заповеднике по сохранению и восстановлению естественного ареала обитания (реинтродукции) темной лесной пчелы.  Александр Новик рассказал, как он занимается бортничеством в США - титаническая работа по сохранению гнезд в дуплах деревьев. Яна Тыжнова поведала о некоторых подробностях перевода очередной книги Томаса Сили "Пчелы. Апиология и жизнь. 20 раскрытых загадок поведения медоносных пчёл" - глубочайший анализ смыслов слов на разных языках. Здесь она превзошла самою себя, переводя уже, кажется,  практически с пчелиного языка. Андрей Богданов рассказал и показал, как он содержит пасеку из нескольких точков в лесах Псковской области на протяжении многих лет безо всякого лечения. Были и другие интересные...