Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt. Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor. Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов. Обновить справочники устройств, проекты CCS и регрессионные проверки.
80 lines
16 KiB
Python
80 lines
16 KiB
Python
"""Совместные электрические модели 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 # Одновременные нарушения проводимости, утечки и контакта не скрывают друг друга.
|