====== Работа с фильтрами ====== В системах автоматизации зачастую приходится работать с аналоговыми величинами - показаниями каких либо датчиков. Показания таких датчиков, сколь они качественными ни были, не могут быть идеальными, всегда присутствует небольшая погрешность, из-за чего выдаваемые показания всегда колеблются вокруг истинного значения. Если колебания незначительные, то их можно сгладить, получив значения, максимально приближенные к истинным. Однако такие колебания могут быть достаточно значительными, вплоть до того, что становится сложно определить истинные показания совсем. Такая ситуация может возникнуть при многих факторах, таких как некачественные датчики, плохой монтаж, нестабильное питание и тд. Во всех подобных ситуациях, если не удается устранить первопричину колебаний, используют алгоритмы фильтрации сигнала. Важно понимать, что любая фильтрация - это искажение исходного сигнала, и она может негативно сказаться на работе системы автоматизации. В случае небольших колебаний, ее использование вполне оправдано, но если колебания значительные, нужно стараться избавиться от их первопричины, фильтрацию использовать как последний шанс исправить ситуацию. Так же фильтрация сигнала, в зависимости от ее глубины, затормаживает время получения истинного сигнала, что так же негативно сказывается на времени реакции систем автоматизации, которые работают по показаниям таких датчиков. Тем не менее, такие ситуации, где другого варианта, кроме как использовать фильтры, встречаются достаточно часто, в связи с этим системы автоматизации обязательно должны поддерживать алгоритмы фильтрации. Библиотека функций скриптов DevelSCADA поддерживает работу с популярными алгоритмами фильтрации данных, такими как: * среднеарифметический; * медианный; * огибающий (по минимальному и максимальному значению). Рассмотрим несколько примеров для работы с данными алгоритмами. Для удобства восприятия данных, будем их отображать в виде графика. Для этого разместим в рабочей области элемент [[lib.ds.grafdata|График данных]]. В свойстве [[prop.config|Конфигурация]] зададим ему следующие настройки: { type: 'line', data: { labels: [], datasets: [ { label: 'Исходный', borderWidth: 1, borderColor: '#FF4444', tension: 0, data: [], yAxisID: 'sig1', }, { label: 'Обработанный', borderWidth: 4, borderColor: '#8888FF', tension: 0, data: [], yAxisID: 'sig2', }, ], }, options: { maintainAspectRatio: false, scales: { x: {}, sig1: { title: { display: true, text: 'Исходный', }, ticks: { color: '#FF4444', }, position: 'left', beginAtZero: true, min: 0, max: 100, }, sig2: { title: { display: true, text: 'Обработанный', }, ticks: { color: '#8888FF', }, position: 'left', beginAtZero: true, min: 0, max: 100, }, }, elements: { point: { radius: 0, }, }, plugins: { legend: { display: false, }, }, }, } В результате получим элемент графика, на котором будем отображать две кривые: исходный сигнал и обработанный фильтром. {{ :filt1.png?nolink |}} Далее в свойстве экрана, в событии **Запуск** зададим скрипт, который будет создавать значения, якобы полученные с нашего датчика, и над которыми будем проводить эксперименты. Для примера выведем обычную синусоиду. async function main() { let pointList = []; // список точек графика for (let i = 0; i < 300; i++) { // создаем точки в рабочей области графика let val = Math.sin(i / 30) * 40 + 50; // пока выведем точки только исходного графика pointList.push([ i, val]); } // выведем созданный список точек на график await ds.objCall('График данных 1', 'addPointList', pointList); } После чего запустим проект на исполнение, и увидим результат работы нашего испытательного стенда. {{ :filt2.png?nolink |}} В данной заготовке мы видим как выглядит идеальный сигнал с нашего выдуманного датчика. В дальнейшем будем вносить в него помехи и пробовать пропустить полученный сигнал через фильтр, и наблюдать как он влияет сигнал. ====== Среднеарифметический фильтр ====== Среднеарифметический фильтр, судя по его названию, пытается усреднять значения, выдавая их среднеарифметической значение на указанном диапазоне. Для примера добавим в код небольшую помеху со случайным отклонением сигнала, и попробуем применить к нему данный фильтр. // создаем фильтр с размерностью в 10 значений let filt = ds.crtFilt('sraf', 10); let pointList = []; for (let i = 0; i < 300; i++) { let val = Math.sin(i / 30) * 40 + 50; // добавляем к исходному сигналу случайную помеху val += Math.random() * 4 - 2; let filtVal = filt.add(val); pointList.push([ i, val, filtVal ]); } await ds.objCall('График данных 1', 'addPointList', pointList); {{ :filt3.png?nolink |}} В результате видим, что не смотря на то что в изначальном сигнале присутствует большое количество колебаний сигнала, поле пропускания через фильтр, нам удалось практически полностью восстановить его исходную форму. Однако при этом видно, что полученные данные немного отстают от исходных по времени (о чем было сказано ранее), но зачастую при небольших фильтрациях такие задержки допустимы. Теперь рассмотрим ситуацию, где помеха гораздо значительнее влияет на сигнал. let filt = ds.crtFilt('sraf', 10); let pointList = []; for (let i = 0; i < 300; i++) { let val = Math.sin(i / 30) * 40 + 50; // увеличиваем размер помехи val += Math.random() * 20 - 10; let filtVal = filt.add(val); pointList.push([ i, val, filtVal ]); } await ds.objCall('График данных 1', 'addPointList', pointList); {{ :filt4.png?nolink |}} В данном случае видим что отфильтрованный сигнал все равно сильно искажен, и вряд ли с ним можно будет работать в дальнейшем. Можно попробовать увеличить размер фильтра, попытавшись сгладить большее количество точек. // увеличиваем размер фильтра let filt = ds.crtFilt('sraf', 50); let pointList = []; for (let i = 0; i < 300; i++) { let val = Math.sin(i / 30) * 40 + 50; val += Math.random() * 20 - 10; let filtVal = filt.add(val); pointList.push([ i, val, filtVal ]); } await ds.objCall('График данных 1', 'addPointList', pointList); {{ :filt5.png?nolink |}} В результате видим, что хоть отфильтрованный график и начал принимать форму, похожую на изначальную синусоиду, но при этом видно что ее амплитуда сильно меньше, так же по времени отставание достаточно значительное. В таком случае зачастую никакой фильтр уже не поможет, необходимо разбираться с первопричиной помехи. ====== Медианный фильтр ====== Иногда бывает так, что сигнал продолжительное время достаточно чисто идет, и лишь кратковременно прилетает какая-то помеха (ошибка измерения, или включился где-то контактом с большим электромагнитным импульсом), в таком случае среднеарифметический фильтр справляется уже не так хорошо. let filt = ds.crtFilt('sraf', 10); let pointList = []; for (let i = 0; i < 300; i++) { let val = Math.sin(i / 30) * 40 + 50; // добавляем кратковременную большую помеху if (i % 33 == 0) val += Math.random() * 100 - 50; let filtVal = filt.add(val); pointList.push([ i, val, filtVal ]); } await ds.objCall('График данных 1', 'addPointList', pointList); {{ :filt6.png?nolink |}} На графике видно, что если даже одна точка выбивается из чистого графика, алгоритм среднеарифметического фильтра возмущение от нее еще видит достаточно продолжительное время. В данной ситуации хорошо работает медианный фильтр. Он, в отличие от среднеарифметического, который использует весь диапазон точек на интервале (в том числе и ту, которая с помехой), а выбирает из диапазона среднее значение из всех точек. Соответственно в данной ситуации точки с большим отклонением от остальных будут исключаться. // используем вместо среднеарифметического, медианный фильтр с таким же размером let filt = ds.crtFilt('median', 10); let pointList = []; for (let i = 0; i < 300; i++) { let val = Math.sin(i / 30) * 40 + 50; if (i % 33 == 0) val += Math.random() * 100 - 50; let filtVal = filt.add(val); pointList.push([ i, val, filtVal ]); } await ds.objCall('График данных 1', 'addPointList', pointList); {{ :filt7.png?nolink |}} Здесь хорошо видно, что помеха лишь незначительно исказила график (связано это с тем, что пришлось пропустить "бракованную" точку). ====== Огибающие фильтры ====== Не редко так же бывает ситуация, когда помеха улетает только в одну сторону относительно исходного графика. Такое часто бывает, к примеру, при периодическом пропадании связи с датчиком, и вместо показаний, получаем нулевое значение. В таких случаях хорошо отрабатывают огибающие фильтры. Их алгоритм работы похож на среднемедианный, но в отличие от него берется не среднее значение, а минимальное или максимальное на диапазоне значений (в зависимости от типа огибающего фильтра). // создаем огибающий фильтр по максимальному значению let filt = ds.crtFilt('max', 10); let pointList = []; for (let i = 0; i < 300; i++) { let val = Math.sin(i / 30) * 40 + 50; // эмулируем потерю сигнала if (i % 33 == 0) val = 0; let filtVal = filt.add(val); pointList.push([ i, val, filtVal ]); } await ds.objCall('График данных 1', 'addPointList', pointList); {{ :filt8.png?nolink |}} Так же плюс данного фильтра, в отличие от рассмотренных ранее, в том, что на участке роста значений, он не имеет отставания, и четко повторяет исходный сигнал. Такой фильтр удобно использовать с данными, которые изменяются в одном направлении (растут или падают, в случае с огибающим по минимальному значению). Так же продолжительность возмущения от помехи на данном типе фильтра длится не всю продолжительность работы фильтра, а лишь на единственной точке.