|
|
|
@ -43,19 +43,10 @@ class DataBuffer:
|
|
|
|
self.buffer_alarms = [0] * self.line_size
|
|
|
|
self.buffer_alarms = [0] * self.line_size
|
|
|
|
self.last_alarm_channels = []
|
|
|
|
self.last_alarm_channels = []
|
|
|
|
|
|
|
|
|
|
|
|
self.prev_values = [None] * self.line_size
|
|
|
|
|
|
|
|
self.trend_streak = [0] * self.line_size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Допускаем небольшой обратный ход, чтобы не сбрасываться от микрошума.
|
|
|
|
|
|
|
|
self.dbfs_max_backstep_db = float(os.getenv('dbfs_max_backstep_db', 0.25))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.freq_tag = '' if freq_tag is None else str(freq_tag)
|
|
|
|
self.freq_tag = '' if freq_tag is None else str(freq_tag)
|
|
|
|
suffix = f'_{self.freq_tag}' if self.freq_tag else ''
|
|
|
|
suffix = f'_{self.freq_tag}' if self.freq_tag else ''
|
|
|
|
|
|
|
|
|
|
|
|
# Параметры MAD-порогов (per-frequency с fallback на общие).
|
|
|
|
|
|
|
|
self.mad_k_on = float(os.getenv('mad_k_on' + suffix, os.getenv('mad_k_on', 5.0)))
|
|
|
|
self.mad_k_on = float(os.getenv('mad_k_on' + suffix, os.getenv('mad_k_on', 5.0)))
|
|
|
|
self.mad_k_off = float(os.getenv('mad_k_off' + suffix, os.getenv('mad_k_off', 2.5)))
|
|
|
|
|
|
|
|
self.mad_eps = float(os.getenv('mad_eps' + suffix, os.getenv('mad_eps', 0.05)))
|
|
|
|
self.mad_eps = float(os.getenv('mad_eps' + suffix, os.getenv('mad_eps', 0.05)))
|
|
|
|
self.dbfs_linear_offset_db = float(
|
|
|
|
self.dbfs_linear_offset_db = float(
|
|
|
|
os.getenv('dbfs_linear_offset_db' + suffix, os.getenv('dbfs_linear_offset_db', 0.0))
|
|
|
|
os.getenv('dbfs_linear_offset_db' + suffix, os.getenv('dbfs_linear_offset_db', 0.0))
|
|
|
|
@ -120,25 +111,24 @@ class DataBuffer:
|
|
|
|
median_value = float(median_value)
|
|
|
|
median_value = float(median_value)
|
|
|
|
return self.dbfs_linear_offset_db + self.dbfs_linear_abs_median_scale * abs(median_value)
|
|
|
|
return self.dbfs_linear_offset_db + self.dbfs_linear_abs_median_scale * abs(median_value)
|
|
|
|
|
|
|
|
|
|
|
|
def get_threshold(self, channel_idx, k=None):
|
|
|
|
def get_threshold(self, channel_idx):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Получить динамический порог в dB для канала:
|
|
|
|
Получить динамический порог в dB для канала:
|
|
|
|
threshold = median + linear_term(median) + k * MAD.
|
|
|
|
threshold = median + linear_term(median) + mad_k_on * MAD.
|
|
|
|
До завершения инициализации возвращает None.
|
|
|
|
До завершения инициализации возвращает None.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
if not self.check_init():
|
|
|
|
if not self.check_init():
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
coef = self.mad_k_on if k is None else float(k)
|
|
|
|
|
|
|
|
baseline = float(self.buffer_medians[channel_idx])
|
|
|
|
baseline = float(self.buffer_medians[channel_idx])
|
|
|
|
mad = max(float(self.buffer_mads[channel_idx]), self.mad_eps)
|
|
|
|
mad_eff = max(float(self.buffer_mads[channel_idx]), self.mad_eps)
|
|
|
|
linear_term = self.get_linear_term(baseline)
|
|
|
|
linear_term = self.get_linear_term(baseline)
|
|
|
|
return baseline + linear_term + coef * mad
|
|
|
|
return baseline + linear_term + self.mad_k_on * mad_eff
|
|
|
|
|
|
|
|
|
|
|
|
def get_thresholds(self, k=None):
|
|
|
|
def get_thresholds(self):
|
|
|
|
if not self.check_init():
|
|
|
|
if not self.check_init():
|
|
|
|
return [None] * self.line_size
|
|
|
|
return [None] * self.line_size
|
|
|
|
return [self.get_threshold(i, k) for i in range(self.line_size)]
|
|
|
|
return [self.get_threshold(i) for i in range(self.line_size)]
|
|
|
|
|
|
|
|
|
|
|
|
def log_threshold_update(self, updated_column):
|
|
|
|
def log_threshold_update(self, updated_column):
|
|
|
|
if not self.check_init():
|
|
|
|
if not self.check_init():
|
|
|
|
@ -152,22 +142,19 @@ class DataBuffer:
|
|
|
|
mad = float(self.buffer_mads[i])
|
|
|
|
mad = float(self.buffer_mads[i])
|
|
|
|
mad_eff = max(mad, self.mad_eps)
|
|
|
|
mad_eff = max(mad, self.mad_eps)
|
|
|
|
linear_term = self.get_linear_term(baseline)
|
|
|
|
linear_term = self.get_linear_term(baseline)
|
|
|
|
threshold_on = self.get_threshold(i, self.mad_k_on)
|
|
|
|
threshold = self.get_threshold(i)
|
|
|
|
threshold_off = self.get_threshold(i, self.mad_k_off)
|
|
|
|
|
|
|
|
packet_times = [self._format_ts(ts) for ts in self.buffer_timestamps[i]]
|
|
|
|
packet_times = [self._format_ts(ts) for ts in self.buffer_timestamps[i]]
|
|
|
|
print(
|
|
|
|
print(
|
|
|
|
f' ch={i} median={baseline:.6f} '
|
|
|
|
f' ch={i} median={baseline:.6f} '
|
|
|
|
f'linear_term={linear_term:.6f} '
|
|
|
|
f'linear_term={linear_term:.6f} '
|
|
|
|
f'mad={mad:.6f} mad_eff={mad_eff:.6f} '
|
|
|
|
f'mad={mad:.6f} mad_eff={mad_eff:.6f} '
|
|
|
|
f'mad_term_on={self.mad_k_on * mad_eff:.6f} mad_term_off={self.mad_k_off * mad_eff:.6f} '
|
|
|
|
f'mad_term={self.mad_k_on * mad_eff:.6f} '
|
|
|
|
f'threshold_on={threshold_on:.6f} threshold_off={threshold_off:.6f} '
|
|
|
|
f'threshold={threshold:.6f} '
|
|
|
|
f'packet_times={packet_times}'
|
|
|
|
f'packet_times={packet_times}'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def alarms_fill_zeros(self):
|
|
|
|
def alarms_fill_zeros(self):
|
|
|
|
self.buffer_alarms = [0] * self.line_size
|
|
|
|
self.buffer_alarms = [0] * self.line_size
|
|
|
|
self.trend_streak = [0] * self.line_size
|
|
|
|
|
|
|
|
self.prev_values = [None] * self.line_size
|
|
|
|
|
|
|
|
self.last_alarm_channels = []
|
|
|
|
self.last_alarm_channels = []
|
|
|
|
|
|
|
|
|
|
|
|
def update(self, data, packet_timestamps=None):
|
|
|
|
def update(self, data, packet_timestamps=None):
|
|
|
|
@ -215,55 +202,36 @@ class DataBuffer:
|
|
|
|
def check_alarm(self, data):
|
|
|
|
def check_alarm(self, data):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Проверка триггера системы по dBFS во времени.
|
|
|
|
Проверка триггера системы по dBFS во времени.
|
|
|
|
Триггер: превышение динамического MAD-порога
|
|
|
|
Один порог на канал, набор тревоги и сброс счетчиков как в main.
|
|
|
|
с подтверждением тренда и несколькими последовательными чтениями.
|
|
|
|
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
if self.check_init():
|
|
|
|
if self.check_init():
|
|
|
|
self.last_alarm_channels = []
|
|
|
|
self.last_alarm_channels = []
|
|
|
|
for i in range(len(data)):
|
|
|
|
for i in range(len(data)):
|
|
|
|
current = data[i]
|
|
|
|
current = data[i]
|
|
|
|
threshold_on = self.get_threshold(i, self.mad_k_on)
|
|
|
|
threshold = self.get_threshold(i)
|
|
|
|
threshold_off = self.get_threshold(i, self.mad_k_off)
|
|
|
|
exceeding = current >= threshold
|
|
|
|
|
|
|
|
|
|
|
|
prev = self.prev_values[i]
|
|
|
|
|
|
|
|
delta_db = 0.0 if prev is None else current - prev
|
|
|
|
|
|
|
|
monotonic_or_stable = (prev is None) or (delta_db >= -self.dbfs_max_backstep_db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if monotonic_or_stable:
|
|
|
|
|
|
|
|
self.trend_streak[i] += 1
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self.trend_streak[i] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Hysteresis: после начала серии используем более мягкий порог отпускания.
|
|
|
|
|
|
|
|
active_threshold = threshold_off if self.buffer_alarms[i] > 0 else threshold_on
|
|
|
|
|
|
|
|
exceeding = (
|
|
|
|
|
|
|
|
current >= active_threshold
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if exceeding:
|
|
|
|
if exceeding:
|
|
|
|
self.buffer_alarms[i] += 1
|
|
|
|
self.buffer_alarms[i] += 1
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
self.buffer_alarms[i] = 0
|
|
|
|
self.buffer_alarms[i] = 0
|
|
|
|
|
|
|
|
|
|
|
|
self.prev_values[i] = current
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.buffer_alarms[i] >= self.num_for_alarm:
|
|
|
|
if self.buffer_alarms[i] >= self.num_for_alarm:
|
|
|
|
self.last_alarm_channels = [i]
|
|
|
|
self.last_alarm_channels = [i]
|
|
|
|
self.buffer_alarms = [0] * self.line_size
|
|
|
|
self.buffer_alarms = [0] * self.line_size
|
|
|
|
self.trend_streak = [0] * self.line_size
|
|
|
|
|
|
|
|
return True
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def check_single_alarm(self, median, cur_channel):
|
|
|
|
def check_single_alarm(self, median, cur_channel):
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Проверка, является ли текущая метрика по каналу превышающей MAD-порог.
|
|
|
|
Проверка, является ли текущая метрика по каналу превышающей порог.
|
|
|
|
:param median: текущая метрика в dBFS.
|
|
|
|
:param median: текущая метрика в dBFS.
|
|
|
|
:param cur_channel: индекс канала внутри частоты.
|
|
|
|
:param cur_channel: индекс канала внутри частоты.
|
|
|
|
:return: Да/нет.
|
|
|
|
:return: Да/нет.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
if self.check_init():
|
|
|
|
if self.check_init():
|
|
|
|
threshold_on = self.get_threshold(cur_channel, self.mad_k_on)
|
|
|
|
threshold = self.get_threshold(cur_channel)
|
|
|
|
return median >= threshold_on
|
|
|
|
return median >= threshold
|
|
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|