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

Как пчелы греют расплод

Введение

Жизнь пчелиной семьи - сложный процесс, меняющий свое содержание от сезона к сезону: она категорически отличается, например, зимой от лета. Поэтому и модели теплового обмена будут разные для каждого времени года. Один из важнейших периодов и, пожалуй, сложнейший с точки зрения тепловой модели - это период выращивания расплода, который продолжается практически полгода. В этой заметке фиксирую текущее состояние модели: как устроен обогрев расплода, пока без учета улочки, т.е. на одном соте.

Расплод семья располагает в центре сота - так эффективней обеспечить его теплом и пищей. Именно целый сот, а не его небольшой фрагмент нужен для того, чтобы промоделировать и понять, как происходит обогрев расплода.

Повышение быстродействия модели позволяет перейти от моделирования небольших фрагментов сота к любому стандартному или нестандартному размеру реальных сотов и рамок. Для определенности возьмем наиболее распространенный размер - Дадана. Рамка Дадана 435×300 мм (за вычетом толщины реек 415×268 мм) моделируется гексагональной сеткой 76×57 ячеек (шаг 5.4 мм).

1. Распределение расплода

Расплод разместим эллиптическим пятном в центре рамки. Полуоси эллипса: 30% ширины и 35% высоты рамки. Внутри эллипса каждая ячейка с вероятностью 92% содержит расплод; остальные 8% — пустые (пропуски, как в реальном соте).

Возраст. Для данной конкретной стороны рамки положим средний возраст расплода 300 часов (~12.5 суток), разброс ±18 ч. По радиусу пятна задан градиент: к центру расплод старше, к краю - моложе (матка движется по спирали от центра). Случайный шум ±6 ч имитирует нерегулярность засева.

Запечатывание. При возрасте 203 ч (~8.5 суток) ячейка запечатывается восковой крышечкой. В модели крышечка:

  • добавляет теплоёмкость C_cap = 10.35 мДж/К к ячейке с расплодом;
  • открывает канал прямого контакта «торакс пчелы → ячейка с расплодом».

Масса, теплоемкость и термогенез эмбриона описаны в заметке Статус ячейки с расплодом 2
Напомню.

  • До запечатывания масса растёт экспоненциально:
    m(t)=m_egg⋅exp(t/τ), m_egg=0.000289 мг, τ=15.5 ч
    К моменту запечатывания (t = 203 ч): m = 132 мг. После запечатывания масса линейно уменьшается (куколка расходует запасы):
    m(t)=132−0.12⋅(t−203) мг
    К выходу взрослой пчелы (t ≈ 502 ч): m ≈ 96 мг.

  • Для ячейки с расплодом возраста 300 ч:
    P_brood(300) = 10⁻⁵·300² − 0.005019·300 + 0.877 ≈ 0.27 мВт
    На всю сторону рамки (в пятне получилось 1313 ячеек расплода): суммарная мощность ≈ 0.35 Вт. Это существенный внутренний источник (350 мВт - это примерно 7 пчел-грелок) - расплод сам себя греет, но, как увидим далее, этого недостаточно.

  • Теплоёмкость. В модели ячейка с расплодом имеет три тепловых узла:
    Расплод (узел I_TH): C = m_embryo · c_brood + C_cap = 0.42 + 0.01 = 0.43 Дж/К
    Стенки (9 узлов): C_wall = 6·C_lat + 3·C_bot = 0.125 Дж/К
    Воздух: C_air = 0.00035 Дж/К (пренебрежимо мала)
    Расплод и стенки - разные тела: расплод греет стенки конвекцией и излучением, стенки передают тепло соседям теплопроводностью. Крышечка (10 мДж/К) добавляется к расплоду, поскольку прижата к нему и имеет с ним хороший тепловой контакт.

    2. Пчёлы-грелки в ячейках

    Пчёлы-грелки размещаются в пустых ячейках среди расплода - тех самых 8% пропусков. Для моделирования из пропусков отбирается 30% (минимум 2), равномерно по азимуту вокруг пятна расплода. В нашем случае: 33 грелки на 1313 ячеек расплода.
    Пчела в ячейке моделируется, как и ранее, тремя соосными цилиндрами: голова (Ø4×1.5 мм), торакс (Ø4×4.5 мм), брюшко (Ø4×6 мм). Зазор между пчелой и стенками ячейки (~0.65 мм на сторону) заполнен воздухом; конвекция в зазоре описывается коэффициентом
    h_in = k_air / ((d_cell − d_bee)/2) ≈ 40 Вт/(м²·К). Брюшко "смотрит" из ячейки и обменивается теплом с воздухом улочки.
    Метаболизм. Базовая мощность определяется температурой, которую ощущают усики:
    P_met = max(29.133−0.739⋅T_sense, 0.2), мВт
    Для грелки в ячейке T_sense = T_air этой ячейки (усики направлены вперёд, вглубь ячейки). При T_air = 34°C: P_met ≈ 4 мВт. Этого достаточно, чтобы поддерживать торакс около ~35°C - на 1° выше среды. Но для обогрева расплода этого мало.
    Режим печки. Пчела-грелка способна кратковременно увеличить тепловыделение, сокращая летательные мышцы торакса без движения крыльев. В модели это добавка P_heater = 45 мВт к метаболизму торакса. Итого: ~49 мВт. По данным литературы (Stabentheiner, 1987; UMT thesis), торакс грелки при этом разогревается до 43–44°C, максимально до 47°C.
    Термостат: один датчик - усики. Ключевое биологическое уточнение: пчела регулирует нагрев не по температуре собственного тела и не по температуре расплода, а по температуре воздуха, которую ощущают усики, т.е. обратная связь - косвенная.
    Предположим, что логика термостатирования у пчелы такая:
    измеренная температура ниже 35°С - включить режим печки;
    измеренная температура выше 36°С - выключить режим печки;
    температура головы выше 47°С - аварийное выключение режима печки.
    Гистерезис 1°C предотвращает дребезг, аварийный потолок 47°C- денатурацию белков.
    Длительность нагрева. Пчёлы-грелки остаются в пустых ячейках неподвижно до 30 минут [20, с.176 и др.]. В модели максимальную продолжительность включения режима печки зададим 1800 с (30 мин).

    Результат (без обсидчиков, T_ambient = 34°C):

    • Воздух в ячейке грелки: 35–36°C (термостат держит в этом диапазоне)
    • Брюшко грелки: 36–37.5°C (на рисунке — цвет кружка)
    • Торакс грелки: 37°C (печка ВЫКЛ) → 37–43°C (печка ВКЛ), на рисунке не показан, т.к. экранируется брюшком
    • Расплод в центре пятна: ~34.7°C (без обсидчиков до цели 35.5 не дотягивает)
    • Грелки работают циклически: печка ВКЛ ~28% времени (в среднем по 33 грелкам). Период цикла определяется тепловой инерцией ячейки: нагрев воздуха до 36°C (выкл) → остывание до 35°C (вкл).
      На рисунке пчелы показаны кружками Ø4 мм (диаметр пчелы), цвет - температура брюшка; обводка: красная - печка ВКЛ, оранжевая - ВЫКЛ.

      3. "Обсидчики" - так для краткости будем называть пчёл на поверхности сота

      сидят сверху на соте, частично перекрывая входы ячеек своим телом. В модели 351 обсидчик, размещены эллиптическим пятном (чуть шире пятна расплода: полуоси 32%×37%).

      Ориентация и упаковка. Каждая пчела задаётся парой (thorax_cell, orientation), где orientation ∈ {0..5} — направление на голову (к одному из 6 боковых соседей, см. Пчела на соте). Брюшко смотрит в противоположного соседа. Три ячейки на пчелу (голова, торакс, брюшко) не должны пересекаться с ячейками других пчёл - генератор выборки перебирает кандидатов и проверяет уникальность.

      Дорсально-вентральная асимметрия. Среда сверху и снизу разная, поэтому теплообмен разный:

      Направление Среда Механизм Коэффициент
      Дорсально (спина, вверх) Воздух улочки Свободная конвекция + излучение h_amb = 8 Вт/(м²·К)
      Вентрально (вниз) Ячейки сота Через перекрытую часть входа h_entrance = 11 Вт/(м²·К)

      Доли перекрытия входа. Проекция части тела на площадь входа (A_entrance ≈ 24 мм²):

      Часть тела   Экран Физический смысл
      Голова
      25% 75% входа открыто
      Торакс
      75% 25% входа открыто
      Брюшко
      100% вход полностью закрыт

      Закрытая часть входа «видит» температуру тела пчелы (конвекция + излучение к стенкам ячейки). Открытая часть — воздух улочки.

      Контакт через крышечку. Если торакс обсидчика находится над запечатанной ячейкой расплода, тепло идёт не через воздух, а напрямую через восковую крышечку.
      Контакт в ~14 раз эффективнее конвекции. В режиме печки пчела прижимает торакс к крышечке (boost=1.5), что ещё улучшает теплоотдачу; g_contact = 150 Вт/(м²·К) — оценочное значение для прижима хитинового торакса к восковой мембране толщиной 0.15 мм. Реальное значение зависит от силы прижима и площади фактического контакта; требует экспериментальной проверки.

      Термостат обсидчика. Датчик - усики, направленные вперёд-вниз. T_sense - температура воздуха в ячейке под головой (не под тораксом!) или температура крышечки. Это «разнесённая обратная связь»: источник тепла (торакс) и датчик (усики) разделены на несколько миллиметров. Пчела может иметь горячий торакс (43°C) и отдавать тепло в ячейку под тораксом, но измеряемая температура в соседней ячейке (крышечки соседней ячейки) будет, например, 35–36°C - и пчела будет продолжать греть.

      Логика та же, что для грелок в ячейках:

      Условие Действие
      T_sense < 35°C ВКЛЮЧИТЬ печку
      T_sense ≥ 36°C ВЫКЛЮЧИТЬ
      T_hd ≥ 47°C АВАРИЙНОЕ ВЫКЛ
      Таймер ≥ 1800 с ВЫКЛЮЧИТЬ (пора отдохнуть)

      Баланс мощности обсидчика:

      P_met + P_heater = Q_street + Q_cells + Q_evap + dU/dt

      где Q_street — дорсальные потери (конвекция + излучение в улочку), Q_cells — вентральный поток (конвекция + контакт + излучение в ячейки), Q_evap — испарение, dU/dt - мощность нагрева самой пчелы (до установившегося режима). Все потоки считаются напрямую из физики; баланс - проверка. Невязка: ~10⁻¹⁵ мВт (машинная точность).

      Численный пример (обсидчик в режиме печки):

      Поток Знач. Куда
      P_met + P_heater ~49 мВт источник (торакс)
      Контакт торакс → крышечка ~30 мВт в расплод (основной канал)
      Конвекция + излучение дорсально ~3.5 мВт в улочку
      Конвекция головы + брюшка вентрально ~2 мВт в воздух ячеек
      Излучение вентрально (к стенкам) ~1 мВт в стенки
      Испарение ~1.5 мВт потеря

      Примерно две трети мощности идёт в расплод. Поэтому обсидчики на запечатанном расплоде - главный инструмент обогрева, т.к. их гораздо больше, чем пчел-грелок в пустых ячейках и, как увидим дальше, они чаще включают режим печки.


      Видим, что расплод прогревается неравномерно, некоторые ячейки перегреты, но, во-первых, это не постоянно и не смертельно, а, во-вторых, мы пока не учитываем рост температуры в улочке и поэтому не видим эффект усреднения температуры в улье (увидим это в следующей заметке).
      На следующем рисунке показано, как могла бы выглядеть картина в тепловизоре:

      Визуально заметно, что плотность "включенных" обсидчиков больше на периферии расплода.

      4. Итоговая температура расплода

      Для нормального развития расплода требуется 33–36°C [32]. Ниже 33°C - гибель, выше 36°C - дефекты/отклонения развития. Модель должна удерживать расплод в этом коридоре при T_ambient = 34°C (воздух в гнезде рядом с расплодной зоной).

      Иерархия нагрева. Три сценария:

      Сценарий T расплода (средн/мин/макс), °C ΔT от T_amb, °C Ячеек с Т>34.5°С
      Без пчёл (1313) 34.40/34.22/34.51 +0.40 56
      Только грелки (1313+33) 34.72/34.29/35.11 +0.72 1160
      Грелки + обсидчики (1313+33+351) 35.29/35.01/38.94 +1.29°C 1313

      Расплод без пчёл греет себя недостаточно: 0.27 мВт на ячейку при тепловом сопротивлении дают в среднем ΔT ≈ 0.40°C.
      Грелки в ячейках добавляют еще 0.32°С (+0.72).

      Обсидчики также включают режим печки по алгоритму локального термостатирования и доводят общий нагрев до 1.29°С, расплод нагревается в среднем до 35.29°С.

      Как видим, основная роль в поддержании температуры расплода принадлежит обсидке.

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


      Видим, что в установившемся режиме 33 пчелы в ячейках (грелки) способны поддерживать среднюю температуру всего расплода, но для этого им приходится держать среднюю температуру торакса около 38°С, при этом в режиме печки должны находиться около четверти всех пчел грелок. При наличии обсидки (351 пчела на соте) грелки в ячейках вообще не включают режим печки, и средняя температура торакса у них составляет около 35.5°С. Около 10% обсидчиков обеспечивают поддержание температуры расплода, включая режим печки, остальные - стабилизируют температуру просто своим присутствием. В этом тоже есть своя логика природы: откуда-то же надо брать эти T_ambient = 34°C.

      Выводы

      Главный результат этой заметки - не конкретные температуры, а демонстрация эмерджентного эффекта. В модели нет ни пчелы-регулятора, ни глобального плана обогрева. Каждая пчела следует одному простому правилу - локальному термостатированию по температуре воздуха под усиками, без перемещения по соту, обмена сигналами с соседями или любого другого поведения. Рассмотрена всего одна сторона одного сота.

      Тем не менее из множества независимых локальных решений самоорганизуется глобальный результат: температура расплода стабилизируется в узком коридоре, необходимом для его развития. Ни одна пчела «не знает» о состоянии расплода в целом, каждая реагирует лишь на воздух в своей ячейке. Согласованность возникает через общую среду: расплод греет стенки, стенки греют воздух, воздух включает и выключает печки соседних пчёл. Это косвенная, стигмергическая координация - тот же принцип, что лежит в основе постройки сота и терморегуляции живой семьи.

      Именно поэтому модель при всей её простоте воспроизводит ключевое свойство семьи как сверхорганизма: надёжный гомеостаз расплода без какого-либо централизованного управления.

 

🐝 Python-скрипт

  """
Один сот (сторона Y), расплод + грелки + обсидчики.
"""
import time, math
import numpy as np
from numba import njit
from scipy.optimize import fsolve
import matplotlib.pyplot as plt
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize
from matplotlib.patches import Patch, Polygon, Circle
import matplotlib
matplotlib.rcParams['font.size'] = 11

# ==========================================================
# ПАРАМЕТРЫ (копия из рабочего скрипта)
# ==========================================================
cols, rows = 76, 57
variant = 2
T_ambient = 34.0
CELL_EMPTY, CELL_BEE, CELL_BROOD = 0, 5, 6
I_HD, I_TH, I_AB, I_AI = 0, 1, 2, 3

LF5 = 203.0; tau_h = 1.0/0.064506; m_egg_mg = 0.000289; p_spec = 8.0; c_brood_spec = 3500.0
kf_mg_h = 0.12
_hours1 = np.arange(1, LF5); _hours2 = np.arange(LF5, 503)
_m1 = m_egg_mg * np.exp(_hours1 / tau_h)
_m2 = _m1[-1] - kf_mg_h * (_hours2 - LF5)
_hours_all = np.hstack((_hours1, _hours2)); _mass_all_mg = np.hstack((_m1, _m2))

def brood_power_W(age_h):
    if age_h <= LF5 - 1: return m_egg_mg*np.exp(age_h/tau_h)*p_spec*1e-6
    return (1e-5*age_h**2 - 0.005019*age_h + 0.877249)*1e-3

def brood_mass_kg(age_h):
    return np.interp(np.clip(age_h,1.0,502.0), _hours_all, _mass_all_mg)*1e-6

cap_thickness = 0.15e-3; cap_density = 960.0
P_heater = 45e-3; T_sense_off = 36.0; T_sense_on = 35.0; T_hd_emerg = 47.0
pulse_surf = 1800.0; contact_boost = 1.5; g_contact = 150.0

brood_params = dict(center_col_frac=0.5, center_row_frac=0.5, radius_col_frac=0.30,
                    radius_row_frac=0.35, fill_rate=0.92, heater_fraction=0.30,
                    min_heaters=2, age_mean=300.0, age_span=36.0, age_noise=6.0, seed=42)
surf_params = dict(center_col_frac=0.5, center_row_frac=0.5, radius_col_frac=0.32,
                   radius_row_frac=0.37, density_max=0.6, seed=7)

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_bee=3500
C_hd,C_th,C_ab=m_head*c_bee,m_thorax*c_bee,m_ab*c_bee
rho_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.]; lam_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]; lam_wax=lam_w[variant-1]; rho_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=rho_wax*A_lateral*L_wall*c_wax; C_wax_bottom=rho_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=rho_air*V_cell*c_air; C_air_bee=rho_air*(V_cell-V_bee)*c_air
C_cap=(A_entrance*cap_thickness*cap_density)*c_wax
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)
A_brood=math.pi*d_cell*h_total+math.pi*(d_cell/2)**2
h_in_brood=k_air/0.3e-3; R_br_ai=1.0/(h_in_brood*A_brood); G_conv_brood=h_in_brood*A_brood
se_brood=sigma*eps_wall*A_brood
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)
h_amb_surface=8.0
A_dorsal_th=0.5*A_th+s_r; A_dorsal_hd=0.5*(A_hd-s_r)+s_r; A_dorsal_ab=0.5*(A_ab-s_r)+s_r
R_th_amb_surf=1.0/(h_amb_surface*A_dorsal_th); R_hd_amb_surf=1.0/(h_amb_surface*A_dorsal_hd); R_ab_amb_surf=1.0/(h_amb_surface*A_dorsal_ab)
cover_head=min((d_bee*h_head)/A_entrance,1.0); cover_thorax=min((d_bee*h_thorax)/A_entrance,1.0); cover_ab=min((d_bee*h_ab)/A_entrance,1.0)
G_cell_hd=h_entrance*cover_head*A_entrance; G_cell_th=h_entrance*cover_thorax*A_entrance; G_cell_ab=h_entrance*cover_ab*A_entrance
se_cell_hd=sigma*eps_wall*cover_head*A_entrance; se_cell_th=sigma*eps_wall*cover_thorax*A_entrance; se_cell_ab=sigma*eps_wall*cover_ab*A_entrance
G_contact_th=g_contact*cover_thorax*A_entrance

# ==========================================================
# ГЕНЕРАЦИЯ + ТОПОЛОГИЯ + ЯДРО (копия из рабочего скрипта)
# ==========================================================
def generate_brood_and_heaters(cols,rows,params):
    rng=np.random.RandomState(params['seed'])
    cmin=-(cols//2); cmax=cmin+cols-1; rmin=-(rows//2); rmax=rmin+rows-1
    cx=cmin+params['center_col_frac']*cols; cy=rmin+params['center_row_frac']*rows
    rx=params['radius_col_frac']*cols; ry=params['radius_row_frac']*rows
    brood,gap=[],[]
    for col in range(cmin,cmax+1):
        for row in range(rmin,rmax+1):
            ds=((col-cx)/rx)**2+((row-cy)/ry)**2
            if ds<=1.0:
                if rng.rand()0 and len(gap)0:
        ang=[math.atan2(r-cy,c-cx) for (c,r) in gap]; order=np.argsort(ang); step=len(order)/nh
        heaters=[gap[order[int(k*step)%len(order)]] for k in range(nh)]
    else: heaters=[]
    return brood,heaters,gap

brood_cells,heater_cells,gap_cells=generate_brood_and_heaters(cols,rows,brood_params)
N_heater = len(heater_cells)
print('Количество грелок ', N_heater)
R_ai_wall_arr=np.array([L_wall/(k_air*A) for A in [A_lateral]*6+[A_bottom]*3])
R_wall_self=np.array([L_wall/(lam_wax*A_lateral)]*6+[L_bottom/(lam_wax*A_bottom)]*3)
C_wall_base=np.array([C_wax_lateral]*6+[C_wax_bottom]*3)
cmin=-(cols//2); cmax=cmin+cols-1; rmin=-(rows//2); rmax=rmin+rows-1
cells=[]
for side in ["Y","^"]:
    for col in range(cmin,cmax+1):
        for row in range(rmin,rmax+1): cells.append((0,side,col,row))
cell_to_index={c:i for i,c 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: _="" age_arr="" age_h="" allset:="" allset="set(cells)" bl="" break="" brood_cells:="" brood_idx="[]" brood_power_w="" c_brood_arr="np.zeros(N_CELLS)" capped="1" cell="" cell_to_index="" cells="" col-1="" col="" dc="" deltas="EVEN_ROW_DELTAS" dr="" else:="" else="" enumerate="" even_row_deltas="" fid="" for="" get_neighbor_for_face="" heater_on="np.zeros(N_CELLS,np.int32);" i="" idx="" if="" in="" int="" is_capped="np.zeros(N_CELLS,np.int32)" k="" na="" nc="" neighbor_faces="" nr="" ns="" odd_row_deltas="" p_brood_arr="" p_extra="np.zeros(N_CELLS);" range="" return="" rf="" row-1="" row="" side="" states="" topology="">=LF5 else 0; is_capped[idx]=capped
    C_brood_arr[idx]=brood_mass_kg(age_h)*c_brood_spec+(C_cap if capped else 0.0)
    brood_idx.append(idx)
heater_idx=[]
for (col,row) in heater_cells:
    idx=cell_to_index[(0,'Y',col,row)]; States[idx]=CELL_BEE; P_extra[idx]=P_heater; heater_on[idx]=1; heater_idx.append(idx)

def generate_surface_bees(cols,rows,side,params,States,cell_to_index):
    rng=np.random.RandomState(params['seed'])
    cmin=-(cols//2); cmax=cmin+cols-1; rmin=-(rows//2); rmax=rmin+rows-1
    cx=cmin+params['center_col_frac']*cols; cy=rmin+params['center_row_frac']*rows
    rx=params['radius_col_frac']*cols; ry=params['radius_row_frac']*rows
    occupied=set(); bees=[]; bid=0
    for col in range(cmin,cmax+1):
        for row in range(rmin,rmax+1):
            ds=((col-cx)/rx)**2+((row-cy)/ry)**2
            # if ds<=1.0 and rng.rand()0.0: G=h_entrance*of*A_entrance; num+=Te*G; den+=G
            if cover>0.0:
                part=Cover_part[i]; is_contact=(part==1 and state==CELL_BROOD and is_capped[i]==1)
                if not is_contact:
                    T_part=T_surf_bees[Cover_bee[i],part]; G=h_entrance*cover*A_entrance; num+=T_part*G; den+=G
            if state==CELL_BROOD: num+=Tth/R_br_ai; den+=1.0/R_br_ai
        Tai=num/den; T_next[i,I_AI]=Tai
        isY=(i0.0:
            part=Cover_part[i]; is_contact=(part==1 and state==CELL_BROOD and is_capped[i]==1)
            if not is_contact:
                T_part=T_surf_bees[Cover_bee[i],part]
                p_rad+=sigma*eps_wall*(cover*A_entrance)*((T_part-t0)**4-(Tw-t0)**4)
        for f in range(9):
            Qa=(Tai-T_cur[i,4+f])/R_ai_wall_arr[f]; ni=Topology[i,f]
            if ni!=-1: Qn=(T_cur[ni,4+Neighbor_Faces[i,f]]-T_cur[i,4+f])/R_wall_self[f]
            else: Qn=(T_amb-T_cur[i,4+f])/R_wall_self[f]
            psk=((A_lateral if f<6 a="" a_bottom="" cur="" dt="" else="" f="" grid="" i="" if="" min:="" n="" p_extra="" p_rad="" pr="Pmin" psk="" state="=CELL_BEE:" t_next="" wall_total="">0.0:
                Tsense=Tai; on=heater_on[i]==1
                woff=(Tsense>=T_sense_off) or (Thd>=T_hd_emerg)
                won=(Tsense0.0 and Cover_part[i]==1 and is_capped[i]==1:
                bee=Cover_bee[i]; Gc=G_contact_th*(contact_boost if surf_heater_on[bee]==1 else 1.0); Tc=T_surf_bees[bee,1]
            Tnew=((Cbr/dt)*Tth+P_brood_arr[i]+G_conv_brood*Tai+Gc*Tc-Qrad)/(Cbr/dt+G_conv_brood+Gc)
            T_next[i,I_TH]=Tnew; T_next[i,I_HD]=Tnew; T_next[i,I_AB]=Tnew
    return Q_Y,Q_X

@njit(fastmath=True,cache=True)
def do_surface_bees_step(T_surf,T_cur,dt,T_amb,head_idx,th_idx,ab_idx,N_surf,States,is_capped,surf_heater_on,surf_heater_timer):
    Qst=0.0; Qce=0.0; Qev=0.0; dU=0.0; maxres=0.0; Qst_arr=np.zeros(N_surf)
    for k in range(N_surf):
        hi=head_idx[k]; ti=th_idx[k]; ai=ab_idx[k]
        Thd=T_surf[k,0]; Tth=T_surf[k,1]; Tab=T_surf[k,2]
        Tsense=T_cur[hi,I_AI]
        Twh=0.0; Twt=0.0; Twb=0.0
        for f in range(9): Twh+=T_cur[hi,4+f]; Twt+=T_cur[ti,4+f]; Twb+=T_cur[ai,4+f]
        Twh/=9.0; Twt/=9.0; Twb/=9.0
        Tah=T_cur[hi,I_AI]; Tat=T_cur[ti,I_AI]; Tab_a=T_cur[ai,I_AI]
        Pmet=(29.133-0.739*Tsense)*1e-3
        if Pmet=T_sense_off) or (Thd>=T_hd_emerg)
        won=(Tsensemaxres: maxres=res
        T_surf[k,0]=Thd+(dt/C_hd)*dThd; T_surf[k,1]=Tth+(dt/C_th)*dTth; T_surf[k,2]=Tab+(dt/C_ab)*dTab
        Qst+=Qst_c; Qce+=Qce_c; Qev+=Qev_c; dU+=dUk; Qst_arr[k]=Qst_c
    return Qst,Qce,Qev,dU,maxres,Qst_arr

# ==========================================================
# ПРОГОН
# ==========================================================
dt_sim=0.1; time_total=3600.0

def make_buffers():
    TA=np.full((N_CELLS,13),T_ambient); TB=np.full((N_CELLS,13),T_ambient)
    for idx in heater_idx:
        for buf in (TA,TB): buf[idx,I_HD]=Thd_i; buf[idx,I_TH]=Tth_i; buf[idx,I_AB]=Tab_i
    hon=heater_on.copy()
    Ts=np.tile(np.array([Thd_i,Tth_i,Tab_i]),(max(N_surf,1),1)).astype(np.float64)
    sho=surf_heater_on.copy(); sht=surf_heater_timer.copy()
    return TA,TB,hon,Ts,sho,sht

def run_simulation(use_surf,time_total,log=False):
    TA,TB,hon,Ts,sho,sht=make_buffers()
    Cf,Cb,Cp=(Cover_frac,Cover_bee,Cover_part) if use_surf else (Cover_frac0,Cover_bee0,Cover_part0)
    steps=int(time_total/dt_sim); T_cur,T_next=TA,TB
    log_t,log_Tbr,log_Tht,log_hon,log_TthS,log_QceS,log_surf_on=[],[],[],[],[],[],[]
    maxres=0.0
    for step in range(steps):
        Q_Y,Q_X=do_cluster_step_surf(T_cur,T_next,Topology,Neighbor_Faces,States,C_grid,P_extra,
                P_brood_arr,C_brood_arr,hon,is_capped,Cf,Cb,Cp,Ts,sho,T_entrance,dt_sim,T_ambient)
        T_cur,T_next=T_next,T_cur
        if use_surf and N_surf:
            Qst,Qce,Qev,dU,mr,_=do_surface_bees_step(Ts,T_cur,dt_sim,T_ambient,surf_head_idx,surf_thorax_idx,surf_ab_idx,N_surf,States,is_capped,sho,sht)
            Q_Y+=Qst
            if mr>maxres: maxres=mr
        if log and step%60==0:
            log_t.append(step*dt_sim)
            log_Tbr.append(T_cur[brood_idx, I_TH].mean() if brood_idx else T_ambient)
            log_Tht.append(T_cur[heater_idx, I_TH].mean() if heater_idx else T_ambient)
            log_hon.append(hon[heater_idx].mean() if heater_idx else 0.0)
            log_TthS.append(Ts[:,1].mean() if N_surf else T_ambient)
            log_QceS.append((Qce/N_surf*1000) if (use_surf and N_surf) else 0.0)
            log_surf_on.append(sho[:N_surf].mean() if N_surf else 0.0)
    T_air=np.full((cols,rows),T_ambient); T_br=np.full((cols,rows),np.nan)
    for i,cell in enumerate(cells):
        _,side,col,row=cell
        if side!="Y": continue
        ci,ri=col-cmin,row-rmin; T_air[ci,ri]=T_cur[i,I_AI]
        if States[i]==CELL_BROOD: T_br[ci,ri]=T_cur[i,I_TH]
    return dict(T_cur=T_cur,Ts=Ts,hon=hon,sho=sho,T_air=T_air,T_br=T_br,
                log_t=log_t,log_Tbr=log_Tbr,log_Tht=log_Tht,log_hon=log_hon,
                log_TthS=log_TthS,log_QceS=log_QceS,log_surf_on=log_surf_on,maxres=maxres)

# компиляция + прогоны
TA0,TB0,hon0,Ts0,sho0,sht0=make_buffers()
do_cluster_step_surf(TA0,TB0,Topology,Neighbor_Faces,States,C_grid,P_extra,P_brood_arr,C_brood_arr,
                     hon0,is_capped,Cover_frac,Cover_bee,Cover_part,Ts0,sho0,T_entrance,dt_sim,T_ambient)
if N_surf: do_surface_bees_step(Ts0,TA0,dt_sim,T_ambient,surf_head_idx,surf_thorax_idx,surf_ab_idx,N_surf,States,is_capped,sho0,sht0)

print("Baseline..."); res_base = run_simulation(use_surf=False, time_total=time_total, log=True)
duty_cycle = np.mean(res_base['log_hon']) * 100
print(f"  Доля времени грелок в печке: {duty_cycle:.0f}%")
print("Основной..."); res_surf=run_simulation(True,time_total,True)
T_cur=res_surf['T_cur']; T_surf_bees=res_surf['Ts']; heater_on=res_surf['hon']; surf_heater_on=res_surf['sho']
# --- Прогон: только расплод (без грелок, без обсидчиков) ---
print("Только расплод (без пчёл)...")
States_save2=States.copy(); P_extra_save2=P_extra.copy(); heater_on_save2=heater_on.copy()
for idx in heater_idx:
    States[idx]=CELL_EMPTY; P_extra[idx]=0; heater_on[idx]=0
res_brood_only=run_simulation(use_surf=False, time_total=time_total, log=False)
States[:]=States_save2; P_extra[:]=P_extra_save2; heater_on[:]=heater_on_save2
T_br_only=res_brood_only['T_br']
print(f"  T расплода без пчёл: {np.nanmin(T_br_only):.3f}..{np.nanmax(T_br_only):.3f}°C")
# ==========================================================
# ВИЗУАЛИЗАЦИЯ (в столбик, для блога)
# ==========================================================
FS=13; size=1.0; width_hex=math.sqrt(3)*size; dy_hex=1.5*size
n_cols,n_rows=cols,rows

def hex_xy(ci,ri):
    col=ci+cmin; row=ri+rmin; return width_hex*(col+0.5*(row&1)), -dy_hex*row

def extent():
    xs=[]; ys=[]
    for ci in range(n_cols):
        for ri in range(n_rows): x,y=hex_xy(ci,ri); xs.append(x); ys.append(y)
    return min(xs)-size,max(xs)+size,min(ys)-size,max(ys)+size

def cbar_inset(ax,fig,cmap,vmin,vmax,x0,x1,y0,y1,lab):
    sx=x1-x0; gap=sx*0.02; cw=sx*0.035
    cax=ax.inset_axes([x1+gap,y0,cw,y1-y0],transform=ax.transData)
    sm=ScalarMappable(cmap=cmap,norm=Normalize(vmin,vmax)); sm.set_array([])
    cb=fig.colorbar(sm,cax=cax); cb.ax.tick_params(labelsize=FS-1); cb.set_label(lab,fontsize=FS); return cw,gap

def draw_hex(ax,fig,field,cmap,vmin,vmax,title='',cb='',brood_field=None):
    co=plt.get_cmap(cmap); ax.set_aspect('equal')
    for ci in range(n_cols):
        for ri in range(n_rows):
            x,y=hex_xy(ci,ri); cx_,cy_=[],[]
            for kk in range(6):
                ang=math.pi/180*(60*kk-30); cx_.append(x+size*math.cos(ang)); cy_.append(y+size*math.sin(ang))
            cx_.append(cx_[0]); cy_.append(cy_[0]); val=field[ci,ri]
            if np.isnan(val):
                ax.fill(cx_,cy_,color='#f0f0f0',edgecolor='#cccccc',lw=0.3); continue
            norm=np.clip((val-vmin)/max(vmax-vmin,1e-5),0,1); ec,ew='#888888',0.3
            if brood_field is not None and not np.isnan(brood_field[ci,ri]): ec,ew='#228B22',1.2
            ax.fill(cx_,cy_,color=co(norm),edgecolor=ec,lw=ew)
    x0,x1,y0,y1=extent(); cw,gap=cbar_inset(ax,fig,co,vmin,vmax,x0,x1,y0,y1,cb)
    ax.set_xlim(x0-0.3,x1+gap+cw+0.3); ax.set_ylim(y0-0.3,y1+0.3)
    ax.set_title(title,fontsize=FS,pad=8); ax.axis('off')

def draw_heater_circles(ax,cmap_obj,norm_obj):
    r_circle=(d_bee/thread)*size
    for idx in heater_idx:
        _,_,col,row=cells[idx]; ci,ri=col-cmin,row-rmin; x,y=hex_xy(ci,ri)
        Tab_val=T_cur[idx,I_AB]; on=heater_on[idx]==1
        circ=Circle((x,y),radius=r_circle,facecolor=cmap_obj(norm_obj(Tab_val)),
                    edgecolor='red' if on else 'orange',linewidth=1.2,zorder=8,alpha=0.95)
        ax.add_patch(circ)

def draw_bee_segments(ax,cmap_obj,norm_obj):
    s_map=width_hex/5.4; L_hd,L_th,L_ab=1.5*s_map,4.5*s_map,6.0*s_map
    W=d_bee*1000*s_map; gap=1.3*s_map
    for k in range(N_surf):
        _,_,tc,tr=cells[surf_thorax_idx[k]]; _,_,hc,hr=cells[surf_head_idx[k]]; _,_,ac,ar=cells[surf_ab_idx[k]]
        px_th=hex_xy(tc-cmin,tr-rmin); px_hd=hex_xy(hc-cmin,hr-rmin); px_ab=hex_xy(ac-cmin,ar-rmin)
        dxu=px_hd[0]-px_ab[0]; dyu=px_hd[1]-px_ab[1]; ln=math.hypot(dxu,dyu)
        if ln<1e-9: ---="" -0.01="" 0.04="" 0.5="" 0="" 1.2:="" 1.3:="" 100="" 12="" 1:="" 1="" 35.5="" 3:="" 3="" 4:="" 5:="" 9="" _="" a1.axhline="" a1.grid="" a1.legend="" a1.plot="" a1.set_title="" a1.set_ylabel="" a1="" a2.axhline="" a2.grid="" a2.legend="" a2.plot="" a2.set_title="" a2.set_ylabel="" a2="" a3.grid="" a3.legend="" a3.plot="" a3.set_title="" a3.set_xlabel="" a3.set_ylabel="" a3="" ab="" age_arr="" age_y="" air="" allv="np.concatenate(vals_all);" allv_base.max="" allv_base="np.concatenate(vals_base)" alpha="0.7," ambient="" ax.add_patch="" ax.legend="" ax1="" ax2="" ax3="" ax="" bbox_inches="tight" bbox_to_anchor="(0.5,-0.01)," bmax="" bmin="" br="" br_max="" br_min="" br_y="" brood_arr="" brood_field="T_br_Y)" brood_idx="" cb="°C" cell="" cells="" ci="" circ="" cmap_g="" cmap_obj="" col="" color="gray" continue="" cur="" cx="" cy="" def="" dmax="" dpi="150," draw_bee_segments="" draw_heater_circles="" draw_hex="" dt="" dt_br="" duty_heater_base="np.mean(res_base[" duty_heater_surf="np.mean(res_surf[" duty_surf="np.mean(res_surf[" edgecolor="black" else="" enumerate="" ext="extent();" f="" facecolor="none" fh2="figsize_for(*ext," fh="" fig1_brood_age_power_t.png="" fig3_brood_dt.png="" fig4_dynamics.png="" fig5_dynamics.png="" fig="" fig_heaters_and_surface.png="" fig_heaters_only.png="" figsize="(9," float="" fontsize="FS-1);" for="" frameon="False)" from="" fw2="" fw="10;" g--="" g-="" gap="" gmax="" gmin="" handles="legend_full()," hd="" hdr="f" heater_idx:="" heater_idx="" heater_on="" hon="" hot="" i="" i_ab="" i_th="" idx="" if="" import="" in="" inferno="" k="" label="обсидчики" legend_full="" legend_heaters="" len="" line2d="" linestyle="none" linewidth="1.2," loc="upper center" log="" log_b="" log_hon="" log_s="" log_surf_on="" log_t="" log_tbr="" log_tht="" log_tths="" lorrd="" ls=":" lw="2," m-="" magma="" marker="o" markeredgecolor="orange" markeredgewidth="1.5," markerfacecolor="#bdbdbd" markersize="9," matplotlib.lines="" min="" n="" n_surf:="" ncol="4," norm_b="" norm_g="" norm_obj="" np.any="" np.array="" np.isnan="" np.nanmax="" np.nanmin="" on="" orange="" p1="(cx+ux*L/2+vx*W/2,cy+uy*L/2+vy*W/2);" p2="(cx+ux*L/2-vx*W/2,cy+uy*L/2-vy*W/2)" p3="(cx-ux*L/2-vx*W/2,cy-uy*L/2-vy*W/2);" p4="(cx-ux*L/2+vx*W/2,cy-uy*L/2+vy*W/2)" p="" p_br_y="" patch="" plasma="" plt.savefig="" plt.show="" plt.subplots="" plt.tight_layout="" poly="" print="" pulse_surf="" px_th="" r--="" r-="" r_circle="(d_bee/thread)*size" radius="r_circle," ravel="" rect="[0,0.06,1,1])" res_base="" return="" ri="col-cmin,row-rmin;" row="cells[idx];" segs:="" segs="[(px_th,L_th,T_surf_bees[k,1])," sharex="True)" side="" states="" surf="" surf_bees.ravel="" surf_bees="" t="" t_abdomen.="" t_air_y="" t_amb="{T_ambient}°C" t_ambient="" t_br_base="res_base[" t_br_only="" t_br_y="res_surf[" t_brood="" t_cur="" tab_val="Tcur[idx,I_AB];" tb_base="res_base[" tb_only="res_brood_only[" tb_surf="res_surf[" tcur="" th="" tight="" title="Обогрев обсидчиками (dT от базового)" ux="" uy="" vals_all.append="" vals_all="[T_air_Y.ravel()]" vals_base.append="" vals_base="[res_base[" vx="" vy="-uy,ux" x="" y="hex_xy(ci,ri)" zorder="8,">6} {'мин':>6} {'макс':>6} {'ΔT':>6} {'≥34.5':>6} {'>38':>4}"
print(hdr); print(f"  {'-'*66}")
for name, Tb in [('Без пчёл', Tb_only),
                 ('Только грелки ', Tb_base),
                 ('Грелки+обсидчики ', Tb_surf)]:
    print(f"  {name:<28 b.mean="">6.2f} {Tb.min():>6.2f} {Tb.max():>6.2f} "
          f"{Tb.mean()-T_ambient:>+6.2f} {int((Tb>=34.5).sum()):>6} {int((Tb>38).sum()):>4}")
print(f"  {'-'*66}")
print(f"  Доля времени в режиме печки:")
print(f"    грелки (без обсидчиков):  {duty_heater_base:.0f}%")
print(f"    грелки (с обсидчиками):   {duty_heater_surf:.0f}%")
print(f"    обсидчики:                {duty_surf:.0f}%")
print(f"{'='*72}")

Комментарии

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

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

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

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

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

Статус ячейки с расплодом

 В заметке Пчелиная ячейка  были перечислены возможные состояния пчелиной ячейки (будем называть их статусы):  1 - ячейка пуста  2 - в ячейке пчела  3 - запечатанная ячейка с мёдом  4 - незапечатанная ячейка с мёдом, может характеризоваться параметром "% заполнения"  5 - запечатанная ячейка с пергой  6 - незапечатанная ячейка с пергой, с параметром "% заполнения"  7 - печатный расплод, с параметром возраст расплода  8 - незапечатанный расплод, с параметром возраст расплода.  В заметке Тепловое взаимодействие ячеек в соте были представлены статусы ячейки с пчелой и пустой ячейки. Статус ячейки с медом или пергой можно упрощенно построить так же, как и ячейки с воздухом, но с другой эквивалентной теплоемкостью, зависящей от заполненности. Точно так же можно, казалось бы, поступить и с расплодной ячейкой, если знать как меняется по времени ее эквивалентная теплоемкость, но таких данных в литературе нет. Кроме того, расплод - личинка и...