Brainrot_Muxa/flyguard/track_readout.py

121 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Считывание по ТРЕКУ — тот же контур MBON, но на входе накопленная история.
Считывание в `mbon_readout.py` смотрит на один кадр: контур кандидата, его
тень, яркость, опору накопителя. Этого хватает вблизи и не хватает вдали —
за 120 м предмет даёт пять-шесть лучей, и по одному кадру он неотличим от
куска стены, попавшего в габарит.
Но у трека есть то, чего у кандидата нет в принципе: **история**. Поезд
проехал двадцать метров, а предмет, если он настоящий, остался на том же
месте в тоннеле. Воронка потерь (EXPERIMENTS п. 12) показывает, что именно
здесь и теряется дальнее обнаружение: на 120…160 м трек заводится в 62 %
наблюдений, а до решения доходит 38 % — улика есть, порога не набирает.
Поэтому вход здесь другой:
* `s_std` — разброс места в тоннеле, которое подразумевают наблюдения.
У неподвижного предмета оно постоянно по построению, у ложного трека,
собранного из разных кусков стены, — гуляет. В улику этот признак
сейчас не входит вообще, ни одним множителем;
* `rays_slope` — растёт ли число лучей так, как обязано расти при
сближении. У предмета оно идёт по закону решётки, у случайного
совпадения — как придётся;
* сводки веса наблюдения и покадрового отсчёта MBON: среднее и максимум
за всю жизнь трека, а не последнее значение.
Схема при этом не меняется: те же клетки Кеньона по шесть когтей, то же
торможение APL, тот же один выход. Это ровно та же операция, что MBON
проделывает над кандидатом, только этажом выше — над тем, что центральный
комплекс успел накопить. У мухи так же: нисходящие нейроны получают сходящийся
вход и от зрительных путей, и от центрального комплекса, а не только от сетчатки.
Стоимость — одно умножение матрицы на трек, и только для тех треков, что уже
прошли дешёвые проверки в `descending.py`.
"""
from __future__ import annotations
import numpy as np
from .central_complex import Track, expected_rays
from .mbon_readout import MbonConfig, MbonReadout
TRACK_FEATURES = (
"log_d", # дальность сейчас
"evidence", # накопленная улика — то, чем решают сегодня
"hits", # подтверждений
"age", # возраст, кадров
"hit_rate", # подтверждений на кадр жизни
"misses", # промахов подряд
"closed", # log(первая дальность / нынешняя): насколько подъехали
"novelty", # сглаженная новизна из памяти тоннеля
"width", "height", "h",
"speed_lat", # модуль поперечной скорости
"rays_now", # лучей к ожидаемым для этой дальности
"rays_mean", # то же, в среднем за жизнь
"rays_slope", # и наклон по времени
"w_mean", "w_max", # вес наблюдения
"p_mean", "p_max", # покадровый отсчёт считывания
"s_std", # разброс места в тоннеле, м
"u_std", # разброс поперечного положения, м
)
def _std(n: int, s: float, sq: float) -> float:
"""Разброс по накопленным суммам. Отрицательную дисперсию даёт
округление, а не данные, поэтому она просто отсекается."""
if n < 2:
return 0.0
var = sq / n - (s / n) ** 2
return float(np.sqrt(var)) if var > 0.0 else 0.0
def describe_track(t: Track, s_now: float) -> np.ndarray:
"""Вектор трека для считывания. Порядок — как в `TRACK_FEATURES`."""
d = max(t.distance(s_now), 1.0)
n = max(t.n_obs, 1)
den = t.n_obs * t.i_sq - t.i_sum * t.i_sum
slope = ((t.n_obs * t.ri_sum - t.i_sum * t.r_sum) / den
if t.n_obs > 1 and abs(den) > 1e-9 else 0.0)
return np.array((
np.log(d),
t.evidence,
float(t.hits),
float(t.age),
t.hits / max(t.age, 1),
float(t.misses),
np.log(max(t.first_d, 1.0) / d),
t.novelty,
t.width, t.height, t.h,
abs(t.speed_lat),
t.last_n_rays / expected_rays(d),
t.r_sum / n,
slope,
t.w_sum / n, t.w_max,
t.p_sum / n, t.p_max,
_std(t.n_obs, t.s_sum, t.s_sq),
_std(t.n_obs, t.u_sum, t.u_sq),
), np.float32)
class TrackReadout(MbonReadout):
"""Тот же разрежённый код, другой вход и другой учитель.
Ёмкость меньше, чем у покадрового считывания: примеров тут на порядок
меньше (треков, а не кандидатов), и восемь тысяч клеток на такой выборке
заучивают её наизусть. Значение выбирается развёрткой в
`tools/train_track.py`, а не на глаз.
"""
def __init__(self, cfg: MbonConfig | None = None,
n_pn: int = len(TRACK_FEATURES)):
super().__init__(cfg or MbonConfig(n_kc=2_000, seed=20260922),
n_pn=n_pn)
def annotate_tracks(self, tracks: list[Track], s_now: float) -> None:
"""Проставить каждому треку отсчёт в `extra`-поле `p_track`."""
if not tracks:
return
X = np.stack([describe_track(t, s_now) for t in tracks])
p = 1.0 / (1.0 + np.exp(-self.logit(X)))
for t, pp in zip(tracks, p):
t.p_track = float(pp)