Files
templates/python/set_devices/signal_joint_models.py
T
Andrey 41e24629ce Расширить DEIF, загрузку CAN и общую обработку сигналов
Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt.
Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor.
Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов.
Обновить справочники устройств, проекты CCS и регрессионные проверки.
2026-10-24 15:40:51 +03:00

80 lines
16 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.
"""Совместные электрические модели Uce, Uge и Ic на общей временной сетке.""" # Наличие трёх каналов даёт дополнительные проверяемые ограничения аппаратных гипотез.
import math # Корни дисперсий задают оценку неопределённости коэффициентов проводящего состояния.
import re # Физические каналы определяются по явным обозначениям, а не имени CSV.
from statistics import median # Медианные уровни и MAD устойчивы к отдельным коммутационным выбросам.
from set_devices.signal_fault_models import checkpoint, fit_model, interpolate # Переиспользуем численные методы без зависимости от GUI и циклического импорта ядра.
def _channels(traces): # Дублирующиеся обозначения нельзя выбирать произвольно для физических расчётов.
found = {} # Для каждой физической роли разрешён только один однозначный канал.
for trace in traces.values(): # Имена и значения предоставляются снимком исходной записи.
match = re.match(r'^\s*(uce|uge|ic)\s*\((V|A)\)\s*$', trace.name, re.IGNORECASE) # Требуем явные единицы В для напряжений и А для тока.
if match and match.group(2).lower() == ('a' if match.group(1).lower() == 'ic' else 'v'): # Не используем ток в мА как амперы без явного преобразования.
role = match.group(1).lower() # Внутренние обозначения не зависят от регистра заголовка.
found[role] = trace if role not in found else None # Повторная роль помечается неоднозначной и не участвует в выводе.
return {role: trace for role, trace in found.items() if trace is not None} # Неоднозначность не маскируется выбором первого столбца.
def fit_joint_channels(result, channels, events=None, cancel=lambda: False): # Все связанные каналы интерполируются на общую фактическую сетку сравнения.
if not channels: # Синхронные осциллограммы могут отсутствовать у одиночного источника.
return dict(available=False, reason='Для совместной проверки нужны Uce (V), Uge (V) и Ic (A) измерения и эталона.'), [] # Не подменяем отсутствующий ток или затвор выдуманными данными.
measured, reference = _channels(channels.get('measured', {})), _channels(channels.get('reference', {})) # Две стороны сравнения проверяются независимо.
if any(role not in measured or role not in reference for role in ('uce', 'uge', 'ic')): # Физические ограничения требуют полного набора каналов с однозначными единицами.
return dict(available=False, reason='Недостаточно однозначных синхронных каналов Uce/Uge/Ic в В/А.'), [] # Пропущенный канал остаётся ограничением диагностического вывода.
count = len(result.difference.times) # Общая область уже ограничена интервалом и временем эталона.
stride = max(1, math.ceil(count / 4096)) # До 4096 точек ограничивают дополнительную стоимость совместной электрической модели.
points = [] # Каждая строка содержит синхронные тройки фактического измерения и эталона.
for index in range(0, count, stride): # Изменение масштаба графика не влияет на используемую область записи.
if index % 1024 == 0: # Порционная проверка отмены поддерживает отзывчивость на больших записях.
checkpoint(cancel) # Не продолжаем подготовку после смены источника.
stamp = result.difference.times[index] # Время отсчитывается от первого измеренного отсчёта в миллисекундах.
a = {role: interpolate(trace.times, trace.values, stamp + channels['measured_origin_ms']) for role, trace in measured.items()} # Сохраняем временное взаимное положение измеренных каналов.
b = {role: interpolate(trace.times, trace.values, stamp + channels['reference_origin_ms'] - channels['shift_ms']) for role, trace in reference.items()} # К эталону применяется тот же пользовательский сдвиг, что в основном сравнении.
if all(value is not None for value in (*a.values(), *b.values())): # Только полностью наблюдаемые тройки пригодны для совместной физической проверки.
points.append((stamp * 1000., a, b)) # Человеко-читаемое время событий хранится в микросекундах.
if len(points) < 30: # Дополнительные модели требуют не менее 30 совместных наблюдений.
return dict(available=False, reason='Недостаточно общих наблюдений трёх физических каналов.'), [] # Число точек явно ограничивает возможность идентификации.
low = min(point[2]['uce'] for point in points) # Низкая полка эталона определяет проводящее состояние транзистора.
high = max(point[2]['uce'] for point in points) # Высокая полка соответствует выключенному ключу.
gate_high = max(point[2]['uge'] for point in points) # Опорный высокий уровень затвора определяется по данным эталона.
on = [point for point in points if point[2]['uce'] < low + .02 * (high - low) and point[1]['uce'] < low + .25 * (high - low) and min(point[1]['uge'], point[2]['uge']) > .98 * gate_high] # ВАХ оценивается на устойчивой эталонной полке с высоким затвором и фактически проводящим измеренным ключом.
off = [point for point in points if min(point[1]['uce'], point[2]['uce']) > low + .85 * (high - low) and max(point[1]['uge'], point[2]['uge']) < .3 * gate_high] # Утечка допустима как модель только при высоком Uce и низком Uge с обеих сторон; пробой с низким Uce рассматривается отдельно.
report, findings = dict(available=True, count=len(points), on_count=len(on), off_count=len(off), contact_events=[]), [] # Совместные выводы сохраняют фактическую область подтверждения.
fits = {} # Две модели проводимости отдельно оценивают исправный и измеренный ключ.
for side, position in (('measured', 1), ('reference', 2)): # Коэффициенты U0 и Rd вычисляются из собственных тока и напряжения каждой стороны.
if len(on) < 30: # Мало наблюдений проводящего состояния не позволяет оценить наклон ВАХ.
continue # Не выдаём эквивалентное сопротивление по одному значению тока.
currents = [point[position]['ic'] for point in on] # Ток задаётся в амперах, напряжение — в вольтах.
if max(currents) - min(currents) < .1 * max(max(abs(value) for value in currents), 1e-12): # Размах менее 10% уровня не разделяет надёжно U0 и дифференциальное сопротивление.
continue # Почти постоянная нагрузка не даёт идентифицировать две независимые величины.
model = fit_model('conduction', 'Uce=Rdiff*Ic+U0', [currents, [1.] * len(on)], [point[position]['uce'] for point in on], ['resistance_ohm', 'voltage_offset_v']) # Модель является локальной линейной ВАХ, а не постоянным сопротивлением IGBT во всех режимах.
if model: # QR-решатель дополнительно проверяет независимость тока и свободного члена.
training_current = [value for index, value in enumerate(currents) if index % 3 != 1] # Оценка неопределённости использует только наблюдения, участвовавшие в подборе коэффициентов.
average = sum(training_current) / len(training_current) # Центр обучающей выборки определяет ковариацию наклона и свободного члена.
spread = sum((value - average) ** 2 for value in training_current) # Знаменатель ковариации имеет единицу А² и не увеличивается отложенными контрольными точками.
model['slope_standard_error_ohm'] = math.sqrt(model['training_sse'] / max(1, len(training_current) - 2) / spread) # Стандартная ошибка оценивается по обучающим остаткам при независимом шуме и не покрывает систематическую ошибку датчика.
fits[side] = model # Сохраняем полные параметры и контрольную ошибку каждой ВАХ.
report['conduction'] = fits or dict(identifiable=False, reason='Нужны устойчивое включённое состояние и заметное изменение тока нагрузки.') # Неидентифицируемый наклон явно отличим от нулевого сопротивления.
if len(fits) == 2: # Сравнение деградации требует оценок обеих сторон на одном режиме нагрузки.
delta = fits['measured']['parameters']['resistance_ohm'] - fits['reference']['parameters']['resistance_ohm'] # Изменение дифференциального сопротивления измеряется в омах.
uncertainty = math.hypot(fits['measured']['slope_standard_error_ohm'], fits['reference']['slope_standard_error_ohm']) # Независимые статистические ошибки наклонов складываются по дисперсии.
report['conduction_change_ohm'] = delta # Численная разница доступна даже при недостаточной статистической значимости.
if delta > max(5. * uncertainty, 1e-9): # Порог пяти стандартных ошибок ограничивает реакции на один только случайный шум.
findings.append(dict(code='conduction', text=f'Совместная модель ВАХ: дифференциальное сопротивление выросло на {delta:.4g} Ом; возможны деградация ключа или сопротивление силового контакта.', model='Uce=Rdiff*Ic+U0')) # Обе физические причины дают похожий наклон и остаются альтернативами.
if len(on) >= 30 and len(off) >= 30: # Разность токов включённого и выключенного состояний отделяет утечку от постоянного нуля датчика.
on_error = [point[1]['ic'] - point[2]['ic'] for point in on] # Ошибка тока во включённом состоянии служит проверкой общей калибровки.
off_error = [point[1]['ic'] - point[2]['ic'] for point in off] # Остаточный ток при низком затворе является кандидатом модели утечки.
on_level, off_level = median(on_error), median(off_error) # Медианы подавляют отдельные импульсы коммутации.
noise = max(1e-9, 1.4826 * median([abs(value - on_level) for value in on_error] + [abs(value - off_level) for value in off_error])) # Множитель 1,4826 переводит MAD в оценку СКО нормального шума.
report['leakage'] = dict(formula='ΔIc_off=Ileak+b; ΔIc_on=b', off_current_a=off_level, on_offset_a=on_level, contrast_a=off_level - on_level, noise_sigma_a=noise) # Двухрежимная модель явно оценивает постоянную ошибку датчика b.
if off_level > 5. * noise and off_level - on_level > 5. * noise: # Одновременно нужен положительный ток выключения и отличие от общей ошибки нуля.
findings.append(dict(code='leakage', text=f'Совместная модель остаточного тока: при низком Uge дополнительный Ic≈{off_level - on_level:.4g} А; возможны утечка ключа или параллельная токовая цепь.', model='off_current')) # Посторонняя ветвь тока остаётся альтернативой утечке самого транзистора.
for event in events or []: # Сегменты предполагаемого дребезга проверяются по синхронным затвору и току.
interval = [point for point in points if event['start_us'] <= point[0] < event['stop_us']] # Проверяем только реально наблюдаемые точки внутри найденного прерывания.
if not interval: # Слишком короткий эпизод может не попасть в ограниченную совместную выборку.
continue # Отсутствие наблюдений не считается отсутствием провала.
gate_ratio = sum(point[1]['uge'] < .5 * point[2]['uge'] for point in interval) / len(interval) # Доля одновременного провала управления количественно проверяет причину повторного выключения.
current_ratio = sum(abs(point[1]['ic']) < .5 * abs(point[2]['ic']) for point in interval) / len(interval) # Провал тока ключа дополнительно подтверждает прерывание проводимости.
report['contact_events'].append(dict(**event, gate_drop_fraction=gate_ratio, current_drop_fraction=current_ratio, supported=gate_ratio >= .8 and current_ratio >= .8)) # Подтверждение требует совпадения обоих провалов хотя бы в 80% наблюдений эпизода.
return report, findings # Одновременные нарушения проводимости, утечки и контакта не скрывают друг друга.