Русский
Русский
English
Статистика
Реклама

Коды рида-соломона

QR-художество

12.10.2020 22:07:24 | Автор: admin

На хабре уже обсуждалось устройство QR-кодов и украшение их произвольными рисунками, но дизайнерская мысль до сих пор работала только в двух основных направлениях: замена квадратных модулей на более интересные формы, либо замена части кода рисунком. Такие художества возможны благодаря тому, что блоки данных в QR-коде дополняются кодами Рида-Соломона, позволяющими восстановить до 30% искажённых байтов. Основываясь на этом, дизайнеры QR-кодов давно уже наловчились заменять участок, занимающий до 30% площади кода, какой-нибудь картинкой. Я же решил испробовать другой подход художественно искажать в QR-коде отдельные биты в целях получения интересного изображения. Например, в этом коде инвертированы лишь 50 модулей из 841:

Важно понимать деление QR-кода на части. Например, QR-код размером 2929 модулей (Version 3) с уровнем избыточности Q либо H состоит из двух перемежающихся блоков по 35 байт, где каждый байт это восемь модулей, расположенных по четыре в двух соседних столбцах:

На этом рисунке один блок показан голубым цветом, другой сиреневым; и в каждом из них независимо от второго допускается искажение до 11 байт. (Сколько бит внутри одного байта искажены не имеет значения.) В зависимости от размера QR-кода и уровня избыточности, число перемежающихся блоков может быть от 1 до 81, а размер одного блока от 25 до 153 байт: чем выше уровень избыточности, тем мельче каждый блок. Удобны для экспериментов коды Version 4 H, состоящие из четырёх блоков по 25 байт, в каждом из которых можно испортить по 8 байт:

Но рисовать по сиренево-голубому трафарету и подсчитывать число изменённых байтов в каждом блоке не так удобно, как иметь редактор QR-кодов, который бы подсвечивал модули, изменение которых приведёт ко сбою расшифровки. Именно такой редактор я создал на https://tyomitch.github.io/qr.html с использованием библиотеки https://github.com/cozmo/jsQR, реализующей расшифровку QR-кодов на чистом JS.

Подробнее..

Коды избыточности простыми словами о том, как надёжно и дёшево хранить данные

08.07.2020 14:22:16 | Автор: admin


Так выглядит избыточность.


Коды избыточности* широко применяются в компьютерных системах для увеличения надёжности хранения данных. В Яндексе их используют в очень многих проектах. Например, применение кодов избыточности вместо репликации в нашем внутреннем объектном хранилище экономит миллионы без снижения надёжности. Но несмотря на широкое распространение, понятное описание того, как работают коды избыточности, встречается очень редко. Желающие разобраться сталкиваются примерно со следующим (из Википедии):



Меня зовут Вадим, в Яндексе я занимаюсь разработкой внутреннего объектного хранилища MDS. В этой статье я простыми словами опишу теоретические основы кодов избыточности (кодов Рида Соломона и LRC). Расскажу, как это работает, без сложной математики и редких терминов. В конце приведу примеры использования кодов избыточности в Яндексе.


Ряд математических деталей я не буду рассматривать подробно, но дам ссылки для тех, кто хочет погрузиться глубже. Также замечу, что некоторые математические определения могут быть не строгими, так как статья рассчитана не на математиков, а на инженеров, желающих разобраться в сути вопроса.


* В англоязычной литературе коды избыточности часто называют erasure codes.


1. Суть кодов избыточности


Суть всех кодов избыточности предельно простая: хранить (или передавать) данные так, чтобы они не пропадали при возникновении ошибок (поломках дисков, ошибках передачи данных и т. д.).


В большинстве* кодов избыточности данные разбиваются на n блоков данных, для них считается m блоков кодов избыточности, всего получается n + m блоков. Коды избыточности строятся таким образом, чтобы можно было восстановить n блоков данных, используя только часть из n + m блоков. Далее мы рассмотрим только блочные коды избыточности, то есть такие, в которых данные разбиваются на блоки.



Чтобы восстановить все n блоков данных, нужно иметь минимум n из n + m блоков, так как нельзя получить n блоков, имея только n-1 блок (в этом случае пришлось бы 1 блок брать из воздуха). Достаточно ли n произвольных блоков из n + m блоков для восстановления всех данных? Это зависит от типа кодов избыточности, например коды Рида Соломона позволяют восстановить все данные с помощью произвольных n блоков, а коды избыточности LRC не всегда.


Хранение данных


В системах хранения данных, как правило, каждый из блоков данных и блоков кодов избыточности записывается на отдельный диск. Тогда при поломке произвольного диска исходные данные все равно можно будет восстановить и прочитать. Данные можно будет восстановить даже при одновременной поломке нескольких дисков.


Передача данных


Коды избыточности можно использовать для надёжной передачи данных в ненадёжной сети. Передаваемые данные разбивают на блоки, для них считают коды избыточности. По сети передаются и блоки данных, и блоки кодов избыточности. При возникновении ошибок в произвольных блоках (вплоть до некоторого количества блоков), данные все равно можно безошибочно передать по сети. Коды Рида Соломона, например, используют для передачи данных по оптическим линиям связи и в спутниковой связи.


* Есть также коды избыточности, в которых данные не разбиваются на блоки, например коды Хэмминга и коды CRC, широко применяемые для передачи данных в сетях Ethernet. Это коды для помехоустойчивого кодирования, они предназначены для обнаружения ошибок, а не для их исправления (код Хэмминга также позволяет частично исправлять ошибки).


2. Коды Рида Соломона


Коды Рида Соломона одни из наиболее широко распространённых кодов избыточности, изобретённые ещё в 1960-х и впервые получившие широкое применение в 1980-х для серийного выпуска компакт-дисков.


Ключевых вопросов для понимания кодов Рида Соломона два: 1) как создавать блоки кодов избыточности; 2) как восстанавливать данные с помощью блоков кодов избыточности. Найдем на них ответы.
Для упрощения далее будем считать, что n=6 и m=4. Другие схемы рассматриваются по аналогии.


Как создавать блоки кодов избыточности


Каждый блок кодов избыточности считается независимо от остальных. Для подсчёта каждого блока используются все n блоков данных. На схеме ниже X1-X6 блоки данных, P1P4 блоки кодов избыточности.



Все блоки данных должны быть одинакового размера, для выравнивания можно использовать нулевые биты. Полученные блоки кодов избыточности будут иметь тот же размер, что и блоки данных. Все блоки данных разбиваются на слова (например, по 16 бит). Допустим, мы разбили блоки данных на k слов. Тогда все блоки кодов избыточности тоже будут разбиты на k слов.



Для подсчёта i-го слова каждого блока избыточности будут использоваться i-е слова всех блоков данных. Они будут считаться по следующей формуле:



Здесь значения x слова блоков данных, p слова блоков кодов избыточности, все альфа, бета, гамма и дельта специальным образом подобранные числа, одинаковые для всех i. Сразу нужно сказать, что все эти значения не обычные числа, а элементы поля Галуа, операции +, -, *, / не привычные всем нам операции, а специальные операции, введённые над элементами поля Галуа.


Зачем нужны поля Галуа



Казалось бы, всё просто: разбиваем данные на блоки, блоки на слова, с помощью слов блоков данных считаем слова блоков кодов избыточности получаем блоки кодов избыточности. В целом это так и работает, но дьявол в деталях:


  1. Как сказано выше, размер слова фиксированный, в нашем примере 16 бит. Формулы выше для кодов Рида Соломона таковы, что при использовании обычных целых чисел результат вычисления p может быть не представим с помощью слова допустимого размера.
  2. При восстановлении данных формулы выше будут рассматриваться как система уравнений, которую нужно решить, чтобы восстановить данные. В процессе решения может появиться необходимость выполнить деление целых чисел друг на друга, результатом чего будет вещественное число, которое нельзя точно представить в памяти компьютера.

Эти проблемы не позволяют использовать для кодов Рида Соломона целые числа. Решение проблемы оригинальное, его можно описать следующим образом: давайте придумаем специальные числа, которые можно представить с помощью слов нужной длины (например, 16 бит), и результат выполнения всех операций над которыми (сложение, вычитание, умножение, деление) также будет представлен в памяти компьютера при помощи слов нужной длины.


Такие специальные числа давно изучает математика, их называют полями. Поле это множество элементов с определёнными для них операциями сложения, вычитания, умножения и деления.


Поля Галуа* это поля, для которых существует и единственен результат каждой операции (+, -, *, /) для любых двух элементов поля. Поля Галуа можно построить для чисел, являющихся степенью 2: 2, 4, 8, 16 и т. д. (на самом деле степенью любого простого числа p, но на практике нас интересуют только степени 2). Например, для слов размером 16 бит это поле, содержащее 65 536 элементов, для каждой пары которых можно найти результат любой операции (+, -, *, /). Значения x, p, альфа, бета, гамма, дельта из уравнений выше для расчётов будут считаться элементами поля Галуа.


Таким образом, мы имеем систему уравнений, с помощью которых можно построить блоки кодов избыточности, написав соответствующую компьютерную программу. С помощью этой же системы уравнений можно выполнить восстановление данных.


* Это не строгое определение, скорее описание.


Как восстанавливать данные


Восстановление нужно тогда, когда из n + m блоков часть блоков отсутствует. Это могут быть как блоки данных, так и блоки кодов избыточности. Отсутствие блоков данных и/или блоков кодов избыточности будет означать, что в уравнениях выше неизвестны соответствующие переменные x и/или p.


Уравнения для кодов Рида Соломона можно рассматривать как систему уравнений, в которых все значения альфа, бета, гамма, дельта константы, все x и p, соответствующие доступным блокам, известные переменные, а остальные x и p неизвестные.


Например, пусть блоки данных 1, 2, 3 и блок кодов избыточности 2 недоступны, тогда для i-й группы слов будет следующая система уравнений (неизвестные отмечены красным):



Мы имеем систему из 4 уравнений с 4 неизвестными, значит можем решить её и восстановить данные!


Из этой системы уравнений следуют ряд выводов про восстановление данных для кодов Рида Соломона (n блоков данных, m блоков кодов избыточности):


  • Данные можно восстановить при потере любых m блоков или меньше. При потере m+1 и более блоков данные восстановить нельзя: нельзя решить систему из m уравнений с m + 1 неизвестными.
  • Для восстановления даже одного блока данных нужно использовать любые n из оставшихся блоков, при этом можно использовать любой из кодов избыточности.

Что ещё нужно знать


В описании выше я обхожу стороной ряд важных вопросов, для рассмотрения которых нужно глубже погружаться в математику. В частности, ничего не говорю про следующее:


  • Система уравнений для кодов Рида Соломона должна иметь (единственное) решение при любых комбинациях неизвестных (не более m неизвестных). Исходя из этого требования подбираются значения альфа, бета, гамма и дельта.
  • Систему уравнений нужно уметь автоматически строить (в зависимости от того, какие блоки недоступны) и решать.
  • Нужно построить поле Галуа: для заданного размера слова уметь находить результат любой операции (+, -, *, /) для любых двух элементов.

В конце статьи есть ссылки на литературу по этим важным вопросам.


Выбор n и m


Как на практике выбрать n и m? На практике в системах хранения данных коды избыточности используют для экономии места, поэтому m выбирают всегда меньше n. Их конкретные значения зависят от ряда факторов, в том числе:


  • Надёжность хранения данных. Чем больше m, тем большее количество отказов дисков можно пережить, то есть выше надёжность.
  • Избыточность хранения. Чем выше соотношение m / n, тем выше будет избыточность хранения, и тем дороже будет стоить система.
  • Время обработки запросов. Чем больше сумма n + m, тем дольше будет время ответа на запросы. Так как для чтения данных (во время восстановления) нужно прочитать n блоков, хранящихся на n разных дисках, то время чтения будет определяться самым медленным диском.

Кроме того, хранение данных в нескольких ДЦ накладывает дополнительные ограничения на выбор n и m: при отключении 1 ДЦ данные всё ещё должны быть доступны для чтения. Например, при хранении данных в 3 ДЦ должно выполняться условие: m >= n/2, в противном случае возможна ситуация, когда данные недоступны для чтения при отключении 1 ДЦ.


3. LRC Local Reconstruction Codes


Для восстановления данных с помощью кодов Рида Соломона приходится использовать n произвольных блоков данных. Это очень существенный минус для распредёленных систем хранения данных, ведь для восстановления данных на одном сломанном диске придётся читать данные с большинства остальных, создавая большую дополнительную нагрузку на диски и сеть.


Наиболее часто встречающиеся ошибки недоступность одного блока данных из-за поломки или перегруженности одного диска. Можно ли как-то уменьшить избыточную нагрузку для восстановления данных в таком (наиболее частом) случае? Оказывается, можно: специально для этого существуют коды избыточности LRC.


LRC (Local Reconstruction Codes) коды избыточности, придуманные в Microsoft для применения в Windows Azure Storage. Идея LRC максимально проста: разбить все блоки данных на две (или более) группы и считать часть блоков кодов избыточности для каждой группы по отдельности. Тогда часть блоков кодов избыточности будет подсчитана с помощью всех блоков данных (в LRC они называются глобальными кодами избыточности), а часть с помощью одной из двух групп блоков данных (они называются локальными кодами избыточности).


LRC обозначается тремя числам: n-r-l, где n количество блоков данных, r количество глобальных блоков кодов избыточности, l количество локальных блоков кодов избыточности. Для чтения данных при недоступности одного блока данных нужно прочитать только n/l блоков это в l раз меньше, чем в кодах Рида Соломона.


Для примера рассмотрим схему LRC 6-2-2. X1X6 6 блоков данных, P1, P2 2 глобальных блока избыточности, P3, P4 2 локальных блока избыточности.



Блоки кодов избыточности P1, P2 считаются с помощью всех блоков данных. Блок кодов избыточности P3 с помощью блоков данных X1X3, блок кодов избыточности P4 с помощью блоков данных X4X6.


Остальное делается в LRC по аналогии с кодами Рида Соломона. Уравнения для подсчёта слов блоков кодов избыточности будут такими:



Для подбора чисел альфа, бета, гамма, дельта нужно выполнить ряд условий, гарантирующих возможность восстановления данных (то есть решения системы уравнения). Подробнее о них можно прочитать в статье.
Также на практике для подсчёта локальных кодов избыточности P3, P4 применяют операцию XOR.


Из системы уравнений для LRC следует ряд выводов:


  • Для восстановления любого 1 блока данных достаточно прочитать n/l блоков (n/2 в нашем примере).
  • Если недоступно r + l блоков, и при этом все блоки входят в одну группу, то данные восстановить нельзя. Это легко объяснить на примере. Пусть недоступны блоки X1X3 и P3: это r + l блоков из одной группы, 4 в нашем случае. Тогда у нас есть система из 3 уравнений с 4 неизвестными, которую нельзя решить.
  • Во всех остальных случаях недоступности r + l блоков (когда из каждой группы доступен хотя бы один блок) данные в LRC можно восстановить.

Таким образом, LRC выигрывает у кодов Рида Соломона в восстановлении данных после одиночных ошибок. В кодах Рида Соломона для восстановления даже одного блока данных нужно использовать n блоков, а в LRC для восстановления одного блока данных достаточно использовать n/l блоков (n/2 в нашем примере). С другой стороны, LRC проигрывает кодам Рида Соломона по максимальному количеству допустимых ошибок. В примерах выше коды Рида Соломона могут восстановить данные при любых 4 ошибках, а для LRC существует 2 комбинации из 4 ошибок, когда данные восстановить нельзя.


Что более важно зависит от конкретной ситуации, но зачастую экономия избыточной нагрузки, которую даёт LRC, перевешивает чуть меньшую надёжность хранения.


4. Другие коды избыточности


Помимо кодов Рида Соломона и LRC, есть много других кодов избыточности. Разные коды избыточности используют разную математику. Вот некоторые другие коды избыточности:


  • Код избыточности с помощью оператора XOR. Операция XOR выполняется над n блоками данных, и получается 1 блок кодов избыточности, то есть схема n+1 (n блоков данных, 1 код избыточности). Используется в RAID 5, где блоки данных и кодов избыточности циклически записываются на все диски массива.
  • Алгоритм even-odd, основанный на операции XOR. Позволяет построить 2 блока кодов избыточности, то есть схема n+2.
  • Алгоритм STAR, основанный на операции XOR. Позволяет построить 3 блока кодов избыточности, то есть схема n+3.
  • Pyramide codes ещё одни коды избыточности от Microsoft.

5. Использование в Яндексе


Ряд инфраструктурных проектов Яндекса применяет коды избыточности для надёжного хранения данных. Вот несколько примеров:


  • Внутреннее объектное хранилище MDS, о котором я писал в начале статьи.
  • YT MapReduce-система Яндекса.
  • YDB (Yandex DataBase) распределённая база данных newSQL.

В MDS используются коды избыточности LRC, схема 8-2-2. Данные с кодами избыточности пишутся на 12 разных дисков в разных серверах в 3 разных ДЦ: по 4 сервера в каждом ДЦ. Подробнее об этом читайте в статье.


В YT используются как коды Рида Соломона (схема 6-3), которые были реализованы первыми, так и коды избыточности LRC (схема 12-2-2), причём LRC предпочтительный способ хранения.


В YDB используются коды избыточности, основанные на even-odd (схема 4-2). Про коды избыточности в YDB уже рассказывали на Highload.


Применение разных схем кодов избыточности обусловлено разными требованиями, предъявляемыми к системам. Например, в MDS данные, хранимые с помощью LRC, размещаются сразу в 3 ДЦ. Нам важно, чтобы данные оставались доступными для чтения при выходе из строя 1 любого ДЦ, поэтому блоки должны быть распределены по ДЦ так, чтобы при недоступности любого ДЦ количество недоступных блоков было не больше допустимого. В схеме 8-2-2 можно разместить по 4 блока в каждом ДЦ, тогда при отключении любого ДЦ будет недоступно 4 блока, и данные можно будет читать. Какую бы схему мы ни выбрали при размещении в 3 ДЦ, в любом случае должно быть (r + l) / n >= 0,5, то есть избыточность хранения будет минимум 50%.


В YT ситуация другая: каждый кластер YT целиком располагается в 1 ДЦ (разные кластеры в разных ДЦ), поэтому там нет такого ограничения. Схема 12-2-2 даёт избыточность 33%, то есть хранить данные выходит дешевле, при этом они также могут переживать до 4 одновременных отключений дисков, как и схема в MDS.


Есть ещё много особенностей применения кодов избыточности в системах хранения и обработки данных: нюансы восстановления данных, влияние восстановления на время выполнения запросов, особенности записи данных и т. д. Я собираюсь отдельно рассказать об этих и других особенностях применения кодов избыточности на практике, если тема будет интересна.


6. Ссылки


  1. Серия статей про коды Рида Соломона и поля Галуа: http://personeltest.ru/aways/habr.com/ru/company/yadro/blog/336286/
    http://personeltest.ru/aways/habr.com/ru/company/yadro/blog/341506/
    В них доступным языком глубже рассматривается математика.
  2. Статья от Microsoft про LRC: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/LRC12-cheng20webpage.pdf
    В разделе 2 кратко объясняется теория, далее рассматривается опыт применения LRC на практике.
  3. Схема even-odd: https://people.eecs.berkeley.edu/~kubitron/courses/cs262a-F12/handouts/papers/p245-blaum.pdf
  4. Схема STAR: https://www.usenix.org/legacy/event/fast05/tech/full_papers/huang/huang.pdf
  5. Pyramid codes: https://www.microsoft.com/en-us/research/publication/pyramid-codes-flexible-schemes-to-trade-space-for-access-efficiency-in-reliable-data-storage-systems/
  6. Коды избыточности в MDS: http://personeltest.ru/aways/habr.com/ru/company/yandex/blog/311806
  7. Коды избыточности в YT: http://personeltest.ru/aways/habr.com/ru/company/yandex/blog/311104/
  8. Коды избыточности в YDB: https://www.youtube.com/watch?v=dCpfGJ35kK8
Подробнее..

Из песочницы Программная реализация умножения в полях Гаула

15.11.2020 20:17:49 | Автор: admin

Захотелось мне как-то сделать более надёжной передачу информации через радиоканал. Это не какой-то промышленный проект, или что-то другое серьёзное. Это скорее для хобби и саморазвития. Сказалась травма детства отсутствие нормально работающей радиоуправляемой машинки. С тех пор мне всегда хотелось уметь легко и непринуждённо управлять чем угодно по радио...


И так, я отвлёкся. В детстве-юношестве для помехоустойчивого кодирования можно было бы применить контроль чётности по матричному методу, но сейчас это не серьёзно. Полистав интернет я решил остановиться на кодировании по методу Рида-Соломона. Алгоритм не то, чтобы совсем новый, его ещё в первых CD применяли, но при этом, насколько мне известно, не потерявший своей актуальности и на данный момент. В этой статье о самих кодах Рида-Соломона я не буду сильно распространяться это за меня на Хабре сделали много раз и много кто. Здесь я хочу описать реализацию алгоритма умножения в GF[256]. Тем не менее, чтобы не заставлять читателя прыгать по ссылкам, кратенько опишу зачем вообще приходится иметь дело
с полями Гаула.


Избыточное кодирование Рида-Соломона это о матрицах. Мы используем матрицы при кодировании и при декодировании. В этих процессах присутствуют как операции умножения матриц, так и операции нахождения обратных матриц деления. Деление здесь требуется не приблизительное-целочисленное, а самое настоящее, точное. А точное деление для компьютера нерешаемая задача: один разделить на три это ноль целых и бесконечное количество троек после запятой, но память при этом 640 килобайт хватит каждому.


Гаул жил в начале 19 века, жил очень мало (21 год, вообще про его личность история интересная, но сейчас не об этом). Его работы очень сильно пригодились при цифровом кодировании. Конечное поле Гаула это набор чисел от нуля до n. Суть и интересность этих полей в том, что для элементов этого набора можно определить операции сложения-умножения-вычитания-деления такие, что результат операции будет находиться в самом этом поле. Например, возьмём набор (поле):

$$display$$[0,1,2,3,4,5,6,7]$$display$$

2+2=4 в этом поле также, как и в поле привычных нам, действительных чисел. А вот 5+6 равно не 11, как мы привыкли, а 5+6=3, и это замечательно! 3 входит в это поле (вообще сложение и вычитание для конкретно этого поля это просто побитовый XOR). Для умножения и деления тоже выполняется правило: результат от умножения или деления любых двух чисел из поля (набора Далее буду говорить только поле) будет находиться в этом поле.

Вообще, количество элементов поля это число не произвольное. Оно называется характеристикой поля и может быть либо простым числом, либо степенью простого числа (её называют порядком поля, основание этой степени называют основанием поля). К счастью, количество чисел, которое можно зашифровать с помощью одного байта равно 256, а это два (простое число) в степени 8, и мы можем принять множество всех возможных значений байта за конечное поле Гаула. По умному оно называется GF[256], GF значит Galois Field.


Так вот. Если использовать арифметику в поле Гаула над байтами, то при делении матриц, состоящих из байтов никогда не получится чисел, которые нельзя записать в один байт, а значит то, как реализовывать в железе (я использую stm32 в основном) алгоритмы кодирования-декодирования становится немного понятнее.


Немного об арифметике. Сложение и вычитание, как упоминалось ранее, это одна и та же операция в полях Гаула (это точно верно для полей с основанием 2), и реализуется она как побитовый XOR.

$$display$$A+B = A xor B$$display$$

$$display$$A-B = A xor B$$display$$

Просто до ужаса. Но когда заходит речь об умножении и делении начитнается что-то, что для человека с натянутыми отношениями с математикой (да, это про меня) не так ясно, как день на луне.

При умножении каждый операнд представляется как полином. Происходит это следующим образом: берётся двоичное представление числа, и записывается сумма, где степень икс есть номер двоичного разряда, а его коэффициент это значение разряда.


Пример:

$$display$$5=101_2=1*x^2+0*x^1+1*x^0=x^2+1$$display$$


Далее полиномы перемножаются по правилам перемножения полиномов, то есть каждый член в первой скобке умножается на каждый член во второй, но не просто так, а с учётом того, что коэффициент не может быть больше одного.

$$display$$x^2+x^2=0$$display$$

$$display$$x^2+x^2+x^2=x^2$$display$$

$$display$$x^2+x^2+x^2+x^2=0$$display$$

То есть коэффициенты складываются по модулю 2. Пример умножения в GF[8]:

$$display$$6*3 = 110_2*11_2= (1*x^2+1*x^1+0*x^0)*(1*x^1+1*x^0)=$$display$$

$$display$$ =(x^2+x)(x+1)=x^2*x+x^2*1+x*x+x*1=$$display$$

$$display$$=x^3+x^2+x^2+x$$display$$

Далее складываем (в кавычках потому, что по модулю 2) подобные члены, и получаем $inline$x^3+x$inline$, что мы переводим в обычное число $inline$1010_2=10$inline$.

Так как это умножение мы подразумевали в GF[8], то число 10 в результате должно вызвать недоумение: Как же так? 10 нету в наборе [0,1,2,3,4,5,6,7]. И да. Тут кроется ещё ложка дёгтя. Если получается такая ситуация (результат перемножения не в поле), то далее мы проводим операцию деления по правилам полиномов. Причём делим мы не на что попало, а на определённый, так называемый порождающий полином. Откуда он берётся? Почему делится только сам на себя? И прочие вопросы я оставлю без ответа (не потому, что жадный, а потому что не хочу вводить в заблуждение, сам плохо понимаю). Скажу лишь, что для конкретного поля Гаула есть один или несколько таких подходящих порождающих полиномов. Для GF[8] их всего два: 11 и 13, то есть 1011 и 1101 или $inline$x^3+x+1$inline$ и $inline$x^3+x^2+1$inline$


И так, чтобы умножить 6 на 3 в поле GF[8] недостаточно лишь перемножить два многочлена. После перемножения надо результат $inline$x^3+x$inline$ поделить на один из возможных порождающих многочленов. Здесь мы выберем $inline$x^3+x+1$inline$. Правила деления многочленов столбиком здесь похожи на те, которые мы должны помнить из школьной программы, но во-первых должны, а во вторых они всё же отличаются. Поясню на нашем примере. Берём старшую степень делимого, в нашем случае это $inline$x^3$inline$ и делим на старшую степень делителя (здесь это также $inline$x^3$inline$). Записываем результат 1. Далее получившийся результат умножаем на весь делитель (получается $inline$x^3+x+1$inline$). Это мы записываем под делимым, и проводим вычитание (а в поле GF[8] вычитание и сложение есть одно и то же) то есть $inline$(x^3+x) +(x^3+x+1)$inline$. Сложение проводим по модулю 2, этому нас обязывает работа в поле, и получаем 1. Далее этот результат мы опять бы делили на порождающий многочлен таким же образом, если бы степень получившегося результата (после вычитания) была бы больше или равной степени делителя (порождающего многочлена). А так как степень меньше, то мы за результат деления принимаем остаток от деления, то есть результат вычитания(сложения здесь это одно и тоже), то есть 1.


Всё. 6 * 3 = 1 для GF[8] c порождающим полиномом 11.


Казалось бы всё непросто. С такими сложностями нужно много-много кода и суперкомпьютеры и вообще проще хранить таблицу умножения как массив 256х256 (ну или 8х8, смотря какое поле мы используем), Но всё не так плохо. У полей Гаула есть ещё одно замечательное свойство, и связано оно с операцией возведения в степень. Так как возведение в степень это просто последовательное умножение, то результат возведения в степень так же является элементом поля Гаула. Также для любого поля верно утверждение, что в нём есть минимум один элемент, степени которого содержат в себе всё поле (кроме 0). То есть, если взять для GF[8] число 2, то
$inline$2^0=1$inline$$inline$2^7=1$inline$
$inline$2^1=2$inline$$inline$2^8=2$inline$
$inline$2^2=4$inline$$inline$2^9=4$inline$
$inline$2^3=3$inline$$inline$2^{10}=3$inline$
$inline$2^4=6$inline$$inline$2^{11}=6$inline$
$inline$2^5=7$inline$$inline$2^{12}=7$inline$
$inline$2^6=5$inline$$inline$2^{13}=5$inline$


Если присмотреться, то можно обратить внимание, что любое число от 1 до 7 можно однозначно представить как 2 в какой либо степени. Так же свойство операции возведения в степень в этом поле таково, что $inline$2^7=2^0$inline$ ещё $inline$2^8=2^1$inline$, $inline$2^9=2^2$inline$ и так далее. Это значит, что 2 в любой степени может быть представлено в виде двойки в степени от нуля до 6 с помощью операции получения остатка от деления на 7 в случае положительной степени и простой формулы $inline$2^{-x}=2^{(7-(x\: mod\:7))}$inline$, если показатель отрицательное число


Собственно, если вспомнить свойство умножения степеней, то умножение чисел многократно упрощается. И чтобы умножить 6 на 3 мы теперь смотрим, в какой степени двойка равна 6 и в какой степени двойка равна 3, складываем степени и смотрим результат два в какой-то степени, которую можно представть как 2 в степени от 0 до 6 пример:

$$display$$6*3=2^4*2^3=2^{(4+3)} = 2^7 = 2^0 = 1 $$display$$

С делением тоже всё становится предельно понятно:

$$display$$3/6=2^3/2^4=2^{(3-4)} = 2^{-1} = 2^{(7-(1\: mod\:7))} = 2^6 = 5 $$display$$


И вроде бы вот оно! счастье программиста реализация арифметики в поле Гаула пара строчек кода, не нужно заморачиваться с полиномами Да и быстродействие такого кода будет высоким, но тут я столкнулся с ещё одной проблемой: Таблицу степеней двойки в поле GF[8] с порождающим полиномом 11 найти легко. Даже на этом ресурсе есть. А вот таблицу степеней для GF[256] я с нахрапа не нагуглил, так что решил её составить самостоятельно, C# мне в помощь. Пришлось реализовать алгоритм умножения по правилам полиномов.

Привожу рабочую функцию перемножения для GF[2^n] c произвольным полиномом. Ограничение я испольую 32-битную арифметику, так что n должно быть меньше 16. Также здесь добавлена функция поиска номера старшего бита числа



private uint GetLeadBitNum(UInt32 Val) {    int BitNum = 31;    uint CmpVal = 1u << BitNum;    while (Val < CmpVal) {        CmpVal >>= 1;        BitNum--;    }    return (uint)BitNum;}private uint Galois_b2_ext_mult(uint m1, uint m2, uint Poly) {    if (0 == m1 || 0 == m2) { return 0; }    uint m1_tmp = m1;    uint m2_tmp;    uint m1_bit_num = 0;        //Перемножение двух полиномов, при использовании арифметики по модулю 2 достаточно простое занятие.    //перебираем единички и нолики (для каждого бита первого числа перебираем все биты второго (или наоборот)), складываем номера позиций битов,    //но не всегда, а только когда оба перебираемых бита - единицы, и инвертируем бит результата под номером, равном сумме позиций для данного шага перебора    //(инверсия - это прибавление единицы по модулю 2)    uint PolyMultRez = 0;    while (m1_tmp != 0) {        uint bit_m1 = (m1_tmp & 1u) == 0u ? 0u : 1u;        m1_tmp = m1_tmp >> 1;        m2_tmp = m2;        uint m2_bit_num;        m2_bit_num = 0;        while (m2_tmp != 0) {            uint bit_m2 = (m2_tmp & 1u) == 0u ? 0u : 1u;            m2_tmp = m2_tmp >> 1;            if ((bit_m1 != 0) && (bit_m2 != 0)) {                int BitNum = (int)(m2_bit_num + m1_bit_num);                PolyMultRez ^= 1u << BitNum;            }            m2_bit_num = m2_bit_num + 1;        }        m1_bit_num = m1_bit_num + 1;    }    //Тут есть результат умножения полиномов PolyMultRez. Осталось найти остаток от деления на выбранный порождающий полином.    //Деление полиномов происходит так: Берём старшую степень делимого, и вычитаем старшую степень делителя.     //Получаем число - степень частного    //Теперь перемножаем, а по сути, просто прибавляем к каждой степени делителя степень получившегося частного    //и повторяем всё по кругу, пока степень делимого не окажется меньше степени делителя    uint TmpDivisor_lead_bit_n;    uint TmpQuotient;    uint TmpDivisor = Poly;    uint TmpDividend = PolyMultRez;    uint TmpDividend_LeadBitNum;    uint TmpMult_bitNum;    uint TmpMult_rez;    TmpDividend_LeadBitNum = GetLeadBitNum(TmpDividend);    TmpDivisor_lead_bit_n = GetLeadBitNum(TmpDivisor);    while (TmpDividend_LeadBitNum >= TmpDivisor_lead_bit_n) {        TmpQuotient = (TmpDividend_LeadBitNum - TmpDivisor_lead_bit_n);        TmpMult_bitNum = 0;        TmpMult_rez = 0;        while (TmpDivisor != 0) {            uint bit_TmpMult = (TmpDivisor & 1u) == 0u ? 0u : 1u;            TmpDivisor >>= 1;            TmpMult_rez ^= bit_TmpMult << (int)(TmpQuotient + TmpMult_bitNum);            TmpMult_bitNum = TmpMult_bitNum + 1;        }        TmpDividend = TmpDividend ^ TmpMult_rez;        TmpDivisor = Poly;        TmpDividend_LeadBitNum = GetLeadBitNum(TmpDividend);    }                //Результат умножения числел есть остаток от деления произведения многочленов на порождающий полином.    return TmpDividend;}

Теперь, с помощью приведённой выше функции можно составить таблицу степеней двойки для нужного мне GF[256] и написать модуль арифметики Гаула для stm32 с использованием двух таблиц по 256 одна для прямого, вторая для обратного преобразования числа в его степень. К самой реализации кодирования Рида-Соломона я ещё даже не приступал, но, когда будет готово, думаю поделюсь здесь же. Надеюсь, получится короче.

Подробнее..

Кодирование Рида-Соломона для чайников

07.02.2021 16:04:25 | Автор: admin

Есть способ передавать данные, теряя часть по пути, но так, чтобы потерянное можно было вернуть по прибытии. Это третья, завершающая часть моего простого изложения алгоритма избыточного кодирования по Риду-Соломону. Реализовать это в коде не прочитав первую, или хотя бы вторую часть на эту тему будет проблематично, но чтобы понять для себя что можно сделать с использованием кодировки Рида-Соломона, можно ограничиться прочтением этой статьи.

Что может этот код?

И так, что из себя представляет избыточный код Рида-Соломона с практической точки зрения? Допустим, есть у нас сообщение "DON'T PANIC". Если добавить к нему несколько избыточных байт, допустим 6 штук: "rrrrrrDON'T PANIC" (каждый r это рассчитанный по алгоритму байт), а затем передать через какую-нибудь среду с помехами, или сохранить там, где данные могут понемногу портиться, то по окончании передачи или хранения у нас может остаться такое, например: "rrrrrrDON'AAAAAAA" (6 байт оказались с ошибкой). Если мы знаем номера байтов, где вместо букв, которые были при создании кода, вдруг оказались какие-нибудь "A", то мы можем полностью восстановить сообщение в исходное "rrrrrrDON'T PANIC". После этого можно для красоты убрать избыточные символы. Теперь текст можно печатать на обложку.

Вообще, избыточных символов к сообщению мы можем добавить сколько угодно. Количество избыточных символов равно количеству исправляемых ошибок (это верно лишь в том случае, когда нам известны номера позиций ошибок). Как правило, ошибки, положение которых известно, называют erasures. Благозвучного перевода найти не могу ("стирание" мне не кажется благозвучным), так что в дальнейшем я буду применять термин "опечатки" и ставить его в кавычки (прекрасно понимаю, что этот термин обычно несёт похожий, но другой смысл). Исправление "опечаток" полезно, например, при восстановлении блоков QR кода, которые по какой-либо причине не удалось прочитать.

Также код Рида-Соломона позволяет исправлять ошибки, положение которых неизвестно, но тогда на каждую одну исправляемую ошибку должно приходиться 2 избыточных символа. "rrrrrrDON'T PANIC", принятые как "rrrrrrDO___ PANIC" легко будут исправлены без дополнительной информации. Неправильно принятый байт, положение которого неизвестно, в дальнейшем я буду называть "ошибкой" и тоже брать в кавычки.

Можно комбинировать исправление "ошибок" и "опечаток". Если, например, есть 3 избыточных символа, то можно исправить одну "ошибку" и одну "опечатку". Ещё раз обращу внимание на то, что чтобы исправить "опечатку", нужно каким-то образом (не связанным с алгоритмом Рида-Соломона) узнать номер байта "опечатки". Что важно, и "ошибки" и "опечатки" могут быть исправлены алгоритмом и в избыточных байтах тоже.

Стоит отметить, что если количество переданных и принятых байт отличается, то здесь код Рида-Соломона практически бессилен. То есть, если на расшифровку попадёт такое: "rrrrrrDO'AIC", то ничего сделать не получится, если, конечно, неизвестно какие позиции у пропавших букв.

Как закодировать сообщение?

Здесь уже не обойтись без понимания арифметики с полиномами в полях Галуа. Ранее мы научились представлять сообщения в виде полиномов и проводить операции сложения, умножения и деления над ними. Уже этого почти достаточно, чтобы создать код Рида-Соломона из сообщения. Единственно, для того, чтобы это сделать понадобится ещё полином-генератор. Это результат такого произведения:

(a^1\;\textbf-\;x)\cdot(a^2\;\textbf-\;x)\cdot...\cdot(a^M\;\textbf-\;x)

Где a это примитивный член поля (как правило, выбирают 2), а M это количество избыточных символов. То есть, прежде чем создавать код Рида-Соломона из сообщения, нужно определиться с количеством избыточных символов, которое мы считаем достаточным, затем перемножить биномы вида (a^n\;\textbf-\;x) в количестве M штук по правилам перемножения полиномов. Для любого сообщения можно использовать один и тот же полином-генератор, и любое сообщение в таком случае будет закодировано с одним и тем же количеством избыточных символов.

Пример: Мы решили использовать 4 избыточных символа, тогда нужно составить такое выражение:

(2^1\;\textbf-\;x)\cdot(2^2\;\textbf-\;x)\cdot(2^3\;\textbf-\;x)\cdot(2^4\;\textbf-\;x)

Так как мы работаем с полем Галуа, то вместо минуса можно смело писать плюс, не боясь никаких последствий. Жаль, что это не работает с количеством денег после похода в магазин. И так, возводим в степень, и перемножаем (по правилам поля Галуа GF[256], порождающий полином 285):

(2+x)\cdot(4+x)\cdot(8+x)\cdot(16+x)=116+167x+224x^2+30x^3+x^4Необязательное дополнение

Легко заметить (правда легко надо лишь взглянуть на произведение биномов), что корнями получившегося полинома будут как раз степени примитивного члена: 2, 4, 8, 16. Что самое интересное, если взять какой-нибудь другой полином, умножить его на x^4 (4 в данном случае это количество избыточных символов), получится тот же самый полином, только с нулями в коэффициентах перед первыми 4 младшими степенями, а затем разделить его на полином-генератор, и прибавить остаток от деления к нашему полиному с 4 нулями, то его корнями также будут эти 4 числа (2, 4, 8, 16).

Выражение выше есть полином-генератор, который необходим для того, чтобы закодировать сообщение любой длины, добавив к нему 4 избыточных символа, которые позволят скорректировать 2 "ошибки" или 4 "опечатки".

Прежде чем приводить пример кодирования, нужно договориться об обозначениях. Полиномы, записанные "по-математически" с иксами и степенями выглядят довольно-таки громоздко. На самом деле, при написании программы достаточно знать коэффициенты полинома, а степени x можно узнать из положения этих коэффициентов. Таким образом полученный в примере выше полином-генератор можно записать так: {116, 167, 224, 30, 1}. Также, для ещё большей компактности, можно опустить скобки и запятые и записать всё в шестнадцатеричном представлении: 74 E7 D8 1E 01. Выходит в 2 раза короче. Надо отметить, что если в "математической" записи мы не пишем члены, коэффициенты которых равны нулю, то при принятой здесь шестнадцатеричной записи они обязательны, и, например, 10x^4 нужно записывать так: 0x^0+0x^1+0x^2+0x^3+10x^4 или 00 00 00 00 0A. Там, где "математическая" запись позволит более понятно объяснить суть, я буду прибегать к ней.

И так, чтобы представить сообщение DON'T PANIC в полиномиальной форме, с учётом соглашения выше достаточно просто записать его байты:

44 4F 4E 27 54 20 50 41 4E 49 43.

Чтобы создать код Рида-Соломона с 4 избыточными символами, сдвигаем полином вправо на 4 позиции (что эквивалентно умножению его на x^4 ):

00 00 00 00 44 4F 4E 27 54 20 50 41 4E 49 43

Теперь делим полученный полином на полином-генератор (74 E7 D8 1E 01), берём остаток от деления (DB 22 58 5C) и записываем вместо нулей к полиному, который мы делили. (это эквивалентно операции сложения):

DB 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 43

Вот эта строка как раз и будет кодом Рида-Соломона для сообщения DON'T PANIC с 4 избыточными символами.

Некоторые пояснения

Порядок записи степеней при представлении сообщения в виде полинома имеет значение, ведь полином 116x^0+167x^1+224x^2+30x^3+1x^4 не эквивалентен полиному 116x^4+167x^3+224x^2+30x^1+1x^0 , поэтому следует определиться с этим порядком один раз и его придерживаться. Ещё раз: когда мы преобразуем:
сообщение -> полином, порядок имеет значение.

Так как избыточные символы подставляются именно в младшие степени при кодировании, то от выбора порядка степеней при представлении сообщения зависит положение избыточных символов в начале или в конце закодированного сообщения.

Изменение порядка записи никоим образом не влияет на арифметику с полиномами, ведь как полином не запиши другим он не становится. 3x^2+12x^1+7x^0 = 7x^0+12x^1+3x^2 . Это очевидно, но при составлении алгоритма легко запутаться.

В некоторых статьях полином-генератор начинается не с первой степени, как здесь: (a^1\;\textbf-\;x)\cdot(a^2\;\textbf-\;x)\cdot...\cdot(a^M\;\textbf-\;x) , а с нулевой: (a^0\;\textbf-\;x)\cdot(a^1\;\textbf-\;x)\cdot...\cdot(a^{M-1}\;\textbf-\;x) . Это не эквивалентные записи одного и того же, последующие вычисления будут отличаться в зависимости от этого выбора.

Также при создании кода можно не делить на полином-генератор, получая остаток, а умножать на него. Это слегка другая разновидность кода Рида-Соломона, в которой в закодированном сообщении не содержится в явном виде исходное.

Как раскодировать сообщение?

Здесь всё посложнее будет. Ненамного, но всё же. Вопрос про раскодировать, собственно "не вопрос!" убираем избыточные символы и остаётся исходное сообщение. Вопрос в том, как узнать, были ли ошибки при передаче, и если были, то как их исправить.

В первую очередь нужно отметить, что при проверке на наличие ошибок нужно знать количество избыточных символов. А во-вторую надо научиться считать значение полинома при определённом x . Про количество избыточных символов нам должен заранее сообщить тот, кто кодировал сообщение, а вот чтобы вычислить значение полинома нужно написать ещё одну функцию для работы с полиномами. Это элементарщина просто вместо x подставляется нужное значение. Но пример, всё же, никогда не помешает.

Пример: Нужно вычислить полином7x^0+12x^1+3x^2при x=4 . Подставляем, возводим в степень: 7\cdot1+12\cdot4+3\cdot16 , перемножаем, 7+48+48 , складываем и получаем число 7 . Сложение, умножение и возведение в степень здесь по правилам поля Галуа GF[256] (порождающий полином 285)

Код приводить не буду, оставлю ссылку на гитхаб: https://github.com/AV-86/Reed-Solomon-Demo/releases Там всё что я описывал в этой и предыдущих статьях реализовано на C#, в виде демо-приложения (собирается под win в VS2019, бинарник тоже выложен). Можно посмотреть как работает арифметика в поле Галуа, а также посмотреть, как работает кодирование Рида-Соломона.

И так, прежде чем исправлять "ошибки" или "опечатки" нужно узнать есть ли они. Элементарно. Нужно вычислить полином принятого сообщения с избыточными символами при x равном степеням примитивного члена. Это те же числа, которые мы использовали при составлении полинома-генератора: a^1,a^2,...,a^M , M количество избыточных символов, a примитивный член. Если ошибок нет, то все вычисленные значения будут равны нулю. Закодированное ранее сообщение DON'T PANIC с 4 избыточными символами, в виде полинома в шестнадцатеричном представлении:

DB 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 43,

если вычислить этот полином при x равном 2, 4, 8, 16, то получатся значения: 0, 0, 0, 0, ведь здесь сообщение точно в таком же виде, в котором оно и было закодировано. Если изменить хотя бы один байт, например, последний символ сделаем более правильным: 42 вместо 43:

DB 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 42,

то результат такого же вычисления станет равным 13, 18, B5, 5D. Эти значения называются синдромами. Их тоже можно принять за полином. Тогда это будет полином синдромов.

И так, чтобы узнать есть ли ошибки в принятом сообщении, нужно посчитать полином синдромов. Если он состоит из одних нулей (также можно говорить, что он равен нулю), то ошибок нет.

Важное, но совсем занудное дополнение

Может случиться так, что сообщение с ошибками будет иметь синдром равным нулю. Это случится в том случае, когда полином амплитуд ошибок (о нём будет ниже) кратен полиному-генератору. Так что проверку ошибок по полиному синдромов кода Рида-Соломона нельзя считать 100% гарантией отсутствия ошибок. Можно даже посчитать вероятность такого случая.

Допустим мы кодируем сообщение из 4 символов четырьмя же избыточными символами, то есть передаём 8 байт. Также возьмём для примера вероятность ошибки при передаче одного символа в 10%. То есть, в среднем на каждые 10 символов приходится один, который передался как случайное число от 00 до FF. Это, конечно же совсем синтетическая ситуация, которая вряд ли будет в реальности, но здесь можно точно вычислить вероятности.

Для рассчёта я рассуждаю так: Полиномы, кратные полиному-генератору получаются умножением генератора на другие полиномы. Пятизначный кратный полином - получается умножением на константу от 1 до 255. Шестизначный - умножением на бином первой степени а их, без нулей ровно 255^2 Те же рассуждения для 7 и 8 -значных полиномов, кратных генератору. Затем надо найти вероятности выпадения 5, 6, 7 и 8 ошибок подряд, и для каждой из них вычислить вероятность, что такая случайная последовательность ошибок окажется кратной полиному-генератору. Сложить их, и тогда мы получим вероятность того, что при передаче 4 байт с 4 избыточными символами, при вероятности ошибки при передаче одного символа 10% получится не обнаруживаемая кодом Рида-Соломона ошибочная передача. Рассчёт в маткаде:

Итого, на каждые ~500 Тб при такой передаче окажется один блок из 4 ошибочных символов, которые алгоритм посчитает корректными. Цифры большие, но вероятность не 0. При вероятности ошибки в 1% речь идёт об эксабайтах. Рассчёт, конечно не эталон, может быть даже с ошибками, но даёт понять об порядках чисел.

Что же делать, если синдром не равен нулю? Конечно же исправлять ошибки! Для начала рассмотрим случай с "опечатками", когда мы точно знаем номера позиций некорректно принятых байт. Ошибёмся намеренно в нашем закодированном сообщении 4 раза, столько же, сколько у нас избыточных символов:

DB 22 58 5C 44 4F 4E 27 54 20 41 41 41 41 41

41 это буква A, поэтому их 5 подряд получилось. Позиции ошибок считаются слева направо, начиная с 0. Для удобства используем шестнадцатеричную систему при нумерации:

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E
DB 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 43
DB 22 58 5C 44 4F 4E 27 54 20 41 41 41 41 41

Позиции ошибок: 0A 0C 0D 0E.

И так, если мы находимся на стороне приёмника, то у нас есть следующая информация:

  • Сообщение с 4 избыточными символами;

  • само сообщение: DB 22 58 5C 44 4F 4E 27 54 20 41 41 41 41 41;

  • В сообщении есть ошибки в позициях 0A 0C 0D 0E.

Этого достаточно, чтобы восстановить сообщение в исходное состояние. Но обо всём по порядку.

Для продолжения необходимо разучить ещё одну операцию с полиномами в полях Галуа - взятие формальной производной от полинома. Формальная производная полинома в поле Галуа похожа на обычную производную. Формальной она называется потому, что в полях вроде GF[256] нет дробных чисел, и соответственно нельзя определить производную, как отношение бесконечно малых величин. Вычисляется похоже на обычную производную, но с особенностями. Если при обычном дифференцировании (ax^n)'=a\cdot n\cdot x^{(n-1)} , то для формальной производной в поле Галуа с основанием 2, формула для дифференцирования члена такая: (ax^n)'=a\cdot (n \operatorname{mod}2)\cdot x^{(n-1)} . Это значит, что достаточно просто переписать полином, начиная с первой степени (нулевая выкидывается) и у оставшегося убрать (обнулить, извиняюсь) члены с нечётными степенями. Пример:

Необходимо найти производную

(1x^0+45x^1+165x^2+198x^3+140x^4+223x^5)'

(Это рандомный полином, не связан с примером). Производная суммы равна сумме производных, соответственно применяем формулу для производной члена и получаем:

0x^{-1}+45x^0+0x^1+198x^2+0x^3+223x^4

Или, если записывать в шестнадцатеричном виде, то это же самое выглядит так:

(01 2D A5 C6 8C DF )' = 2D 00 C6 00 DF .

Думаю, что из примера в шестнадцатеричном виде проще всего составить алгоритм нахождения формальной производной.

Теперь можно уже исправить "опечатки"? Как бы не так! Нужно ещё два полинома. Полином-локатор и полином ошибок.

Полином-локатор это полином, корнями которого являются числа обратные примитивному члену в степени позиции ошибки. Сложно? Можно проще. Полином-локатор это произведение вида

(1+x\cdot a^{E_1})\cdot (1+x\cdot a^{E_2})\cdot...\cdot(1+x\cdot a^{E_N})

где a это примитивный член, E_1, E_2 и так далее это позиции ошибок.

Пример: у нас есть позиции ошибок 10, 12, 13, 14; примитивный член a=2 тогда полином локатор будет таким:

(1+2^{10}x)\cdot(1+2^{12}x)\cdot(1+2^{13}x)\cdot(1+2^{14}x)=(1+116x)\cdot(1+205x)\cdot(1+135x)\cdot(1+19x)

Перемножаем и получаем полином-локатор для позиций ошибок 10, 12, 13, 14:

1x^0+45x^1+165x^2+198x^3+140x^4

Или в шестнадцатеричной записи: 01 2D A5 C6 8C.

Про полином-локатор нужно понять следующее: из него можно получить позиции ошибок, и наоборот из позиций ошибок можно получить полином-локатор. По сути, это две разные записи одного и того же позиций ошибок.

Полином ошибок его по-разному называют в разных статьях, он не так уж и сложен. Представляет из себя произведение полинома синдромов и полином-локатора, с отброшенными старшими степенями. Продолжая пример, найдём полином ошибок для искажённого сообщения:

DB 22 58 5C 44 4F 4E 27 54 20 41 41 41 41 41

Полином синдромов: 72 BD 22 5B

Произведение полинома синдромов и полинома-локатора не буду расписывать в "математическом" виде, напишу так:

(72 BD 22 5B)(01 2D A5 C6 8C) = 72 4B 10 22 D9 C0 57 15

У результата оставляем количество младших членов, равное количеству избыточных символов, в нашем случае их 4, старшие степени просто выбрасываем, они не нужны. Остаётся

72 4B 10 22

Это и есть полином ошибок.

Осталось посчитать амплитуды ошибок. Звучит угрожающе, но на деле это просто значения, которые нужно прибавить к искажённым символам сообщения чтобы получились неискажённые символы. Для этого воспользуемся алгоритмом Форни. Здесь придётся привести фрагмент кода, словами расписать так, чтобы было понятно, очень сложно.

Функция принимает на входе

  • полином синдромов (Syndromes),

  • полином, в котором члены позиции ошибок (ErrPos),

  • количество избыточных символов (NumOfErCorrSymbs).

Класс GF_Byte - это просто байт, для которого переопределены арифметические операции так, чтобы они выполнялись по правилам поля Галуа GF[256], класс GF_Poly Это полином в поле Галуа. По сути, массив GF_Byte. Для него также переопределны арифметические операции так, чтобы они выполнялись по правилам арифметики с полиномами в полях Галуа.

public static GF_Poly FindMagnitudesFromErrPos(   GF_Poly Syndromes,   GF_Poly ErrPos,   uint NumOfErCorrSymbs){ //Вычисление локатора из позиций ошибокGF_Poly Locator = CalcLocatorPoly(ErrPos);//Произведение для вычисления полинома ошибокGF_Poly Product = Syndromes * Locator;//Полином ошибок. DiscardHiDeg оставляет указаное количество младших степенейGF_Poly ErrPoly = Product.DiscardHiDeg(NumOfErCorrSymbs);//Производная локатораGF_Poly LocatorDer = Locator.FormalDerivative();//Здесь будут амплитуды ошибок. Количество членов - это самая большая позиция ошибкиGF_Poly Magnitudes = new GF_Poly(ErrPos.GetMaxCoef());//Перебор каждой заданной позиции ошибкиfor (uint i = 0; i < ErrPos.Len; i++) {//число обратное примитивному члену в степени позиции ошибкиGF_Byte Xi = 1 / GF_Byte.Pow_a(ErrPos[i]);//значение полинома ошибок при x = XiGF_Byte W = ErrPoly.Eval(Xi);//значение производной локатора при x = XiGF_Byte L = LocatorDer.Eval(Xi);//Это как раз и будет найденное значение ошибки,//которое надо вычесть из ошибочного символа, чтобы он стал не ошибочнымGF_Byte Magnitude = W / L;//запоминаем найденную амплитуду в текущей позиции ошибкиMagnitudes[ErrPos[i]] = Magnitude;}            return Magnitudes;}

Если скормить функции следующие параметры:

  • полином синдромов 72 BD 22 5B

  • полином, в котором члены - позиции ошибок 0A 0C 0D 0E

  • количество символов коррекции ошибок 4,

то на выходе она даст полином амплитуд ошибок:

00 00 00 00 00 00 00 00 00 00 11 00 0F 08 02.

Теперь можно прибавить полученное к искажённому сообщению

DB 22 58 5C 44 4F 4E 27 54 20 41 41 41 41 41

(по правилам сложения полиномов, конечно же), и на выходе получится исходное сообщение:

DB 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 43.

Первые 4 байта - это избыточные символы. Если бы в них оказались "опечатки", то разницы никакой для алгоритма нет, разве что они нам не нужны после исправления. Можно их просто отбросить:

44 4F 4E 27 54 20 50 41 4E 49 43 Это исходное сообщение DON'T PANIC.

Здесь должно быть понятно, как исправлять ошибки, положение которых известно. Само по себе уже это может нести практическую пользу. В QR кодах на обшарпанных стенах могут стереться некоторые квадратики, и программа, которая их расшифровывает сможет определить в каких именно местах находятся байты, которые не удалось прочитать, которые "стёрлись" erasures, или как мы договорились писать по-русски "опечатки". Но нам этого, конечно же недостаточно. Мы хотим уметь выявлять испорченные байты без дополнительной информации, чтобы передавать их по радио, или по лазерному лучу, или записывать на диски (кого я обманываю? CD давно мертвы), может быть, захотим реализовать передачу через ультразвук под водой, чтобы управлять моделью подводной лодки, а какие-нибудь неблагодарные дельфины будут портить случайные данные своими песнями. Для всего этого нам понадобится уметь выявлять, в каких именно байтах при передаче попортились биты.

Как найти позиции ошибок?

Вспомним про полином-локатор. Его можно составить из заранее известных позиций ошибок, а ещё его можно вычислить из полинома-синдромов и количества избыточных символов. Есть не один алгоритм, который позволяет это сделать. Здесь будет алгоритм алгоритм Берлекэмпа-Мэсси. Если хочется много математики, то гугл с википедией на неё не скупятся. Я, если честно, не вник до конца в циклические полиномы и прочее-прочее-прочее. Стыдно, немножко, конечно, но я взял реализацию этого алгоритма с сайта Wikiversity переписал его на C#, и постарался сделать его более доходчивым и читаемым:

public static GF_Poly CalcLocatorPoly(GF_Poly Syndromes, uint NumOfErCorrSymbs) {//Алгоритм Берлекэмпа-МэссиGF_Poly Locator;GF_Poly Locator_old;//Присваиваем локатору инициализирующее значение (1*X^0)Locator = new GF_Poly(new byte[] { 1 });Locator_old = new GF_Poly(Locator);uint Synd_Shift = 0;for (uint i = 0; i < NumOfErCorrSymbs; i++) {uint K = i + Synd_Shift;GF_Byte Delta = Syndromes[K];for (uint j = 1; j < Locator.Len; j++) {Delta += Locator[j] * Syndromes[K - j];}//Умножение полинома на икс (эквивалентно сдвигу вправо на 1 байт)Locator_old = Locator_old.MultiplyByXPower(1);if (Delta.val != 0) {if (Locator_old.Len > Locator.Len) {GF_Poly Locator_new = Locator_old.Scale(Delta);Locator_old = Locator.Scale(Delta.Inverse());Locator = Locator_new;}//Scale  умножение на константу. Можно было бы//вместо использования Scale //умножить на полином нулевой степени. Разницы нет, но так короче:Locator += Locator_old.Scale(Delta);}}return Locator;}
Пояснения по коду

Класс GF_Poly по сути обёртка над массивом GF_Byte. Есть ещё одна особенность. Свойство Lenght любого массива - возвращает количество его элементов независимо от значений элементов. Здесь Len - возвращает количество членов полинома. Массив может быть любой длины, но если начиная с какого-то номера все элементы равны нулю, то старшая степень полинома - это последний ненулевой элемент.

Приведённый алгоритм считает локатор. Если количество "ошибок" больше, чем количество избыточных символов, поделённое на 2, то алгоритм не сработает правильно.

Если в сообщении, которое мы используем для примера

DB 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 43,

ошибиться в нулевом и последнем символе (2 "ошибки", мы притворяемся, что не знаем в каких позициях ошиблись), получится такой полином:

02 22 58 5C 44 4F 4E 27 54 20 50 41 4E 49 01,

Полином синдромов для него 4B A7 E8 BD. Если выполнить функцию, приведённую выше с параметрами 4B A7 E8 BD, и 4 (количество избыточных символов), то она вернёт нам такой полином: 01 12 13. Это не похоже на позиции ошибок, которые мы ожидаем, но полином-локатор содержит в себе информацию о позициях ошибок, ведь это "полином, корнями которого являются числа обратные примитивному члену в степени позиции ошибки". Из этого, если немного поскрипеть мозгами или ручкой по бумаге следует, что позиция ошибки это логарифм числа по основанию примитивного члена, обратного корню полинома.

E=log_a(1/R)

E позиция ошибки, a примитивный член (2, как правило), R корень полинома.

Что-ж, будем искать корни в поле. Поиск корней полинома в поле Галуа занятие лёгкое и непыльное. В GF[256] может быть 256 числел всего, так что иксу негде разгуляться. Просто считаем полином 256 раз, подставляя вместо x число, и если полином посчитался как нуль, то записываем к массиву с корнями текущее значение x. Дальше считаем по формуле и получаем позиции ошибок 00 и 0E, именно там где они и были допущены. Теперь эти значения вместе с синдромами и цифрой 4 можно скармливать алгоритму Форни, чтобы он исправил "ошибки" также, как он исправлял "опечатки".

Ещё пара пояснений
  • Существуют более эффективные алгоритмы поиска корней полинома в поле Галуа. Перебор просто самый наглядный.

  • В позиции 00 в текущем примере находится избыточный символ. Алгоритмам Берлекэмпа-Месси и Форни это абсолютно неважно.

Если у нас есть 4 избыточных символа, при этом мы знаем что есть 2 "опечатки" в известных позициях, то алгоритм Берлекэмпа-Мэсси сможет найти ещё одну "ошибку". Но для этого его нужно будет совсем немного модифицировать. Всего то надо там где мы писали

//Присваиваем локатору инициализирующее значение (1*X^0)Locator = new GF_Poly(new byte[] { 1 });

нужно локатор инициализировать не единичным полиномом, а полиномом-локатором, рассчитанным из известных позиций ошибок. И ещё изменить пару строчек. Весь код, напомню, есть на гитхабе: https://github.com/AV-86/Reed-Solomon-Demo/releases

Надеюсь материал в этой статье поможет тем, кто захочет в каком-нибудь своём проекте реализовать избыточное кодирование без сторонних библиотек. Просьба: Если что-то не понятно, не стесняйтесь комментировать. Постараюсь ответить на вопросы, или внести правки в статью.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru