リードソロモン符号(= RS符号) を実装してみた(JavaScript)
リードソロモン符号(= RS符号) を実装してみた(JavaScript):
勉強したので習作。誤り訂正符号に関する勉強の集大成。QRコードで利用される。エンコードまではできたけど復号はできていない。
解きたい課題: ノイズにより誤る可能性があるので 検査用に長さ7 のバイト列を付け加えることで、最大で247バイトのデータ(つまり、符号長の最大長は 254バイト)を送信する。符号の内 3つのバイトが壊れてもそれを検知・訂正する(バイトが壊れるとは、バイトを構成する8つのビットのうち、どれか 1つ以上のビットが反転する状態のこと)。
本稿での実践: 参考資料p73 の再現。19バイトの情報に 7バイトをくっつけて 26バイトの符号を作る
αの正体を知ることよりも重要なのは、 αの性質を知ること:
α が住んでいる世界には、このようにαを何回もかけてできる 255種類の住人 と 0 という住人がいて、それで全員である。αのべき乗じゃないやつ、例えば、$1+α$ は住人じゃないの?というかもしれないけれど、実はそれも住人で、計算すればわかるんだけど $1+α = α^{25}$ が成立する。こんな感じで、任意のαの多項式が住人であるというすごく閉じた世界を作っている。
というか、そうなるように αが住む世界での足し算や掛け算を定義した、という方が正しくて、 それを2を法とした計算と呼んでいる。例えば、1+1=0といった感じ。このような計算ルールに加えて、αには、もう一つ重要な性質がある:
上記のような小さな世界 $GF(256)$ でよく使う関数を定義した(コードは末尾):
バイト列と多項式表現: 入力を 19バイトとしよう。これは「19個の0..255の整数」を係数に持つ18次の多項式に対応付けることも可能ではある。例えば冒頭の 19バイトの入力は $128x^{18}+68x^{17}+...+236x+17$に対応付けることもできる。がしかし、それをやめよう。そうではなく、$\alpha^{7}x^{18}+\alpha^{102}x^{17}+...+\alpha^{122}x+\alpha^{100}$に対応付ける。このように入力ビット列を GF(256)の係数を持つ x の多項式に「翻訳」する。翻訳さえしてしまえば、数学の力を存分に発揮できる。
体を作りたい: 上記の方法で、任意の入力ビット列を変換して、「αの多項式を係数にもつ xの多項式」を得られた。やるべきは これを要素とするような体を作ること。
和の定義: 和とは、$A(x) + B(x) = C(x)$ となる C を求めることだが、x の次数が同じ者同士をまとめるという意味で「普通に」定義すると、結局は同じ次数同士の係数の足し算: $f(α)x^4 + g(α)x^4 = h(α)x^4$ となるhをどう定義しようか?ということに帰着する。この係数の足し算、つまりαの多項式同士の足し算として、GF(256)の演算を採用する。
積の定義: 積とは、$A(x)B(x) = C(x)$ となるCを求めることだけど、分配則を使って展開して、同じ係数でまとめてというように「普通に」定義すると、結局 $f(α)g(α) = h(α)$となる h をどう定義しようか?ということに帰着する。αの多項式同士の掛け算をGF(256)での掛け算として定義する。
剰余算: ハミング符号やBCH符号と同じように考える。誤り訂正が3の場合、7次の生成多項式 $G(x) = x^7 + α^{87} x^6 + α^{229} x^5 + α^{146} x^4 + α^{149} x^3 + α^{238} x^2 + α^{102} x + α^{21}$ となる(係数の計算は、前述の
ハミング符号・BCH符号と全く同じ。入力ビット列に対応する多項式表現を求めて、検査分(今回は8)だけ多項式の次数をあげて、生成多項式で剰余算をして、出てきた多項式に対応するビット列が検査ビットとなる。
とりあえず参考資料を再現できた。
ざっくり言って以下の手順
https://kouyama.sci.u-toyama.ac.jp/main/education/2007/infomath/pdf/text/text09.pdf
勉強したので習作。誤り訂正符号に関する勉強の集大成。QRコードで利用される。エンコードまではできたけど復号はできていない。
1. 全体像
解きたい課題: ノイズにより誤る可能性があるので 検査用に長さ7 のバイト列を付け加えることで、最大で247バイトのデータ(つまり、符号長の最大長は 254バイト)を送信する。符号の内 3つのバイトが壊れてもそれを検知・訂正する(バイトが壊れるとは、バイトを構成する8つのビットのうち、どれか 1つ以上のビットが反転する状態のこと)。本稿での実践: 参考資料p73 の再現。19バイトの情報に 7バイトをくっつけて 26バイトの符号を作る
送りたい情報19バイト: 0x80, 0x44, 0x85, 0xA7, 0x49, 0xA7, 0x8B, 0x6C, 0x00, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 検査用の符号をくっつけて26バイトにする: 0x80, 0x44, 0x85, 0xA7, 0x49, 0xA7, 0x8B, 0x6C, 0x00, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xF9, 0xBB, 0x0B, 0xA1, 0x4B, 0x45, 0xF4
1. αを巡る計算ドリル
αの正体を知ることよりも重要なのは、 αの性質を知ること:- 性質1:$α^0, α^1, α^2, ..., α^{254}$ の255種類の数は全部違う(同じものがない)
- 性質2:$α^{255} = 1$ が成立するので、後は何乗しても種類が増えない
α が住んでいる世界には、このようにαを何回もかけてできる 255種類の住人 と 0 という住人がいて、それで全員である。αのべき乗じゃないやつ、例えば、$1+α$ は住人じゃないの?というかもしれないけれど、実はそれも住人で、計算すればわかるんだけど $1+α = α^{25}$ が成立する。こんな感じで、任意のαの多項式が住人であるというすごく閉じた世界を作っている。
というか、そうなるように αが住む世界での足し算や掛け算を定義した、という方が正しくて、 それを2を法とした計算と呼んでいる。例えば、1+1=0といった感じ。このような計算ルールに加えて、αには、もう一つ重要な性質がある:
- 性質3:$α^8 + α^{4} + α^3 + α^2 + 1 = 0$
上記のような小さな世界 $GF(256)$ でよく使う関数を定義した(コードは末尾):
-
ELEMENTS
: 長さ256の配列で、i 番目には $α^i$ の整数表現($α^i$ を αの7次以下の多項式に変換したとき、各係数をビットとみなした8ビットを整数に変換した値)を保持。最後の255番目には 0 を格納。 -
getIndexOf
: $α$の多項式(の整数表現)が与えられたときに、それが、$α$ の何乗であるか?を調べてくれる関数。 -
aPlus
: 任意の整数 i, j に対して、$α^i + α^j = α^k$ が成立する。k の値を計算する関数。
2. 数学
バイト列と多項式表現: 入力を 19バイトとしよう。これは「19個の0..255の整数」を係数に持つ18次の多項式に対応付けることも可能ではある。例えば冒頭の 19バイトの入力は $128x^{18}+68x^{17}+...+236x+17$に対応付けることもできる。がしかし、それをやめよう。そうではなく、$\alpha^{7}x^{18}+\alpha^{102}x^{17}+...+\alpha^{122}x+\alpha^{100}$に対応付ける。このように入力ビット列を GF(256)の係数を持つ x の多項式に「翻訳」する。翻訳さえしてしまえば、数学の力を存分に発揮できる。翻訳例.txt
<例: x^17の係数(多項式の前から二つ目の項の係数)を求める> 二つ目のバイト: 0x44 = 0b01000100 αの多項式表現と思い込むと: a^6 + a^2 これに対応する a^k は?: k = 102 (a^102 = a^6 + a^2) x^17 の係数は: a^102
和の定義: 和とは、$A(x) + B(x) = C(x)$ となる C を求めることだが、x の次数が同じ者同士をまとめるという意味で「普通に」定義すると、結局は同じ次数同士の係数の足し算: $f(α)x^4 + g(α)x^4 = h(α)x^4$ となるhをどう定義しようか?ということに帰着する。この係数の足し算、つまりαの多項式同士の足し算として、GF(256)の演算を採用する。
積の定義: 積とは、$A(x)B(x) = C(x)$ となるCを求めることだけど、分配則を使って展開して、同じ係数でまとめてというように「普通に」定義すると、結局 $f(α)g(α) = h(α)$となる h をどう定義しようか?ということに帰着する。αの多項式同士の掛け算をGF(256)での掛け算として定義する。
剰余算: ハミング符号やBCH符号と同じように考える。誤り訂正が3の場合、7次の生成多項式 $G(x) = x^7 + α^{87} x^6 + α^{229} x^5 + α^{146} x^4 + α^{149} x^3 + α^{238} x^2 + α^{102} x + α^{21}$ となる(係数の計算は、前述の
aPlus
関数で求められる)。多項式に対して、G で割った剰余多項式を法とする(同じ剰余多項式を持つ多項式同士を同一視する)。getIndexOf(0b1111111); // -> 87: x^6 の係数 getIndexOf(0b110011100110); // -> 229: x^5 の係数
3. エンコード
ハミング符号・BCH符号と全く同じ。入力ビット列に対応する多項式表現を求めて、検査分(今回は8)だけ多項式の次数をあげて、生成多項式で剰余算をして、出てきた多項式に対応するビット列が検査ビットとなる。const G = [0, 87, 229, 146, 149, 238, 102, 21]; function encode (arr) { const aArr = arr.map(getIndexOf); const shifted = [...aArr, ...Array(G.length - 1).fill(255)]; return [...aArr, ...rem256(shifted, G)].map(x => ELEMENTS[x]); } const data = [ 0x80, 0x44, 0x85, 0xA7, 0x49, 0xA7, 0x8B, 0x6C, 0x00, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, ]; console.log(encode(data).map(x => '0x' + x.toString(16).toUpperCase())); /* [ '0x80', '0x44', '0x85', '0xA7', '0x49', '0xA7', '0x8B', '0x6C', '0x0', '0xEC', '0x11', '0xEC', '0x11', '0xEC', '0x11', '0xEC', '0x11', '0xEC', '0x11', '0xF9', '0xBB', '0xB', '0xA1', '0x4B', '0x45', '0xF4' ] */
4. デコード
ざっくり言って以下の手順- シンドロームの計算
- 誤り数の判定
- 誤り箇所の判定
- 誤りの訂正
- エラーなしの場合に Y(β) = 0 となることは確認
- エラーありの場合に Y(β) ≠ 0 となることは確認
- 参考資料はエンコードまでしかやってなく、私に読める他の資料をさがしたが、見当たらなかった
- 本を買った方がよさそう
- 難しそう
5. コード
const assert = require('assert'); // utility const range = end => [...Array(end).keys()]; // 2を法とした算術において 多項式f を多項式g で割った余り.入出力は対応する整数値 function rem2 (f, g) { let result = f; while (true) { const shift = Math.clz32(g) - Math.clz32(result); if (shift < 0) break; result ^= g << shift; } return result; } // α^n (n= 0, 1, ... n-1) を求める function alphaPowersOf (n, g) { const result = []; let val = 1; range(n).forEach(() => { val = rem2(val, g); result.push(val); val = val << 1; }); return result; } const F = 0b100011101; // αの原始多項式 z8+z4+z3+z2+1 に対応 // GF(255)の要素。順番にこそ意味があって i 番目は α^i に対応 // 最後、つまり 255番目に 0を加えて 256種類 = 1Byteで表現できる多項式全体をカバー const ELEMENTS = [...alphaPowersOf(255, F), 0]; // 与えられたαの多項式が元の何番目か?(=αの何乗か?) // 例えば a12 + a7の場合 (1<<12) & (1<<7) を入力する function getIndexOf(n) { return ELEMENTS.findIndex(x => x === rem2(n, F)); } // α^i + α^j = α^k function aPlus2 (i, j) { assert(i < 256 || j < 256); if(i === j) return 255; // 255番目は 0 if(i === 255) return j; if(j === 255) return i; return getIndexOf(ELEMENTS[i] ^ ELEMENTS[j]); } const aPlus = (...args) => args.reduce(aPlus2, 255); // 256を法とした算術において 多項式f を多項式g で割った余り.入出力は対応する配列 // 注!: 簡単のため g の最高次数の係数が 1 (gArr[0] === 0)であることを前提にしている(G は実際そう) // 注!: 簡単のため f の各係数は 0..255 であることを前提(実際そう) // rem2 関数と比較すると実装を理解しやすい function rem256(fArr, gArr) { assert(gArr[0] === 0); assert(fArr.every(n => n < 256)); let result = [...fArr]; // copy while (true) { const shift = result.length - gArr.length; if (shift < 0) break; // 商を α^q とする. G の最高次数の係数が1を仮定しているので簡単 const q = result[0]; // console.log(q, JSON.stringify(result.map(n => ELEMENTS[n]))); range(gArr.length).forEach(i => { // 割る側の係数が 0 なら何もしない // 割る側がx3 + x + 1 のように穴あきの場合に考える必要 if (gArr[i] === 255) { assert(false); // 我々の利用ケースでは来ないはず return; } result[i] = aPlus(result[i], (q + gArr[i]) % 255); }); // 先頭の255(係数0に等しい)を取る [255, 255, 1,2,3] -> [1,2,3] assert(result[0] === 255); const where = result.findIndex(x => x !== 255); if (where === -1) { result = []; // 余りなし } else { result = result.slice(where); } } return result; } //const G = [0, 43, 139, 206, 78, 43, 239, 123, 206, 214, 147, 99, 150, 39, 243, 163, 136]; //const G = [0, 75, 249, 78, 6]; // 生成多項式 G: x4+ a75 x3 + a249 x2 + a75 x + a6 const G = [0, 87, 229, 146, 149, 238, 102, 21]; function encode (arr) { const aArr = arr.map(getIndexOf); const shifted = [...aArr, ...Array(G.length - 1).fill(255)]; return [...aArr, ...rem256(shifted, G)].map(x => ELEMENTS[x]); } const data = [ 0x80, 0x44, 0x85, 0xA7, 0x49, 0xA7, 0x8B, 0x6C, 0x00, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, ]; /* const word = encode(data); const eword = [...word]; eword[3] = 0x10; const A = ewords.map(getIndexOf); const s = rem256(A, G); let S = 0; console.log(A, r.length, r); console.log(E, s.length, s); */
コメント
コメントを投稿