直交表の数理 ~ 有限射影幾何を用いた直交表の構築編 ~
前回の記事で有限射影空間の理論を用いた行列の構築について説明した。本稿では前回説明した手法で正しく直交表が構築できることを証明し、実際に直交表を構築するプログラム例を示したいと思う。
有限射影幾何を用いた直交表構築手法の証明
おさらい
を因子数、を直交表の強さ、を素数冪とする。行列の行列があって、その任意の () 行は上の次元ベクトル空間上で一次独立であるとする。このとき、行列の行列を用いて、直交表はという行列積により得られるとのことだった(詳細は前回の記事を参照)。以下ではこの事実の証明を考えていく。
事前に断らせて頂くと、証明はほぼ全て私が考えたものだが、有限体上のベクトル空間の扱いにあまり自信がない。有限体上のベクトル空間では、例えば内積が0でもベクトルが直交しないようなケースがあるなど、標数0の体で使える議論が使えないケースがある[1]。もしそういった点で誤りがあれば、Twitterやブログコメントにてご指摘頂けるとありがたい。
証明
を上の次元ベクトル空間上の行ベクトルの集合とし、これらのうち任意の個を選べばそれらは一次独立になっているとする。これを用いては以下のように書ける。
同様にを上の列ベクトルの集合とし、これらは相異なるとする。ベクトルの数がであるため、これらのベクトルはのベクトル全体の集合となる。これを用いては以下のように書ける。
これより、行列積は以下のようになる。
示したいことは、の任意の行を選んだとき、それらの各列の値から順序集合を形成したとき、同じ順序集合がちょうど回ずつ現れることである。というのも、列は全部で個あり、可能な順序集合のパターンは全部で通りとなるため、個の値の組がそれぞれ回ずつ出現すれば直交表としての性質を満たすためである。
以下、選ばれた個の行の番号をとする。
まず、全ての順序集合が回未満しか出現しないと仮定する。このとき、可能な順序集合の組み合わせが通りしかないため、最大でも個の列しか存在できなくなってしまう。そのため仮定は誤りで、少なくとも1つの順序集合は回以上出現する。
一番出現回数が多い順序集合について、それが出現する列がちょうど個であることを示す。そのために、このような列が個あると仮定して矛盾を導く。今、という番号の列について、行目の値から成る順序集合が一致したと仮定する。この時、について以下の式が成り立つ。
ただし、である。
についても同様に以下の式が得られる。
この関係を線形写像の観点から整理してみる。そのために以下のような行列を考える。
は一次独立なので、である。そのため、次元定理よりとなる。なので、はにおいて全て同一の次元空間に含まれる。そこで、のベクトル空間としての基底をとすると、は全て以下の形式で表すことができる。
ただし、である。これらの係数はの通りの値を取るため、この形式で表されるベクトルは全部で本となる。これはが相異なる個のベクトルであることに矛盾する。
よって仮定は誤りで、一番出現回数が多い列は個または個以上ということになる。後者は個の場合と同様の理由でありえないため、結局列数は個ということになる。
今考えた一番出現回数が多い順序集合が出現する列を除いて、他の列について同様の議論を繰り返すことで、結局全ての順序集合の出現回数は回と言える。よっては直交表になっている。
計算してみよう
証明ができて気分がすっきりしたところで、実際に計算してみよう。前回の記事で作成したgmat.pyをimportして直交表を構築するプログラム例を以下に示す。
#!/usr/bin/env sage import gmat import sys from sage.all import * def calcThetaMatrix(dim, order): K = GF(order, name="a", modulus="primitive") gamma = K.gen() Theta = calcThetaMatrixHelper(dim, order, gamma, 0, [0 for d in range(dim)]) Theta = Matrix(Theta) return Theta.transpose() def calcThetaMatrixHelper(dim, order, gamma, pos, currentVec): if pos == dim: return [currentVec.copy()] currentVec[pos] = 0 Theta = calcThetaMatrixHelper(dim, order, gamma, pos+1, currentVec) for q in range(1, order): currentVec[pos] = gamma**q Theta += calcThetaMatrixHelper(dim, order, gamma, pos+1, currentVec) return Theta def convertToInteger(oa, order): gf = GF(order, name="a", modulus="primitive") gamma = gf.gen() orderedGf = [0] + [gamma**(val) for val in range(order-1)] # Hasmap to convert elements of GF(q) to corresponding integers gfToInt = {} for i, val in enumerate(orderedGf): gfToInt[val] = i+1 intOa = [] for i in range(oa.nrows()): row = [] for j in range(oa.ncols()): row.append(gfToInt[oa[i][j]]) intOa.append(row) return Matrix(intOa) def isCorrectOA(oa, strength): vectorIndexSet = gmat.genVectorIndex([i for i in range(len(oa[0]))], strength) for vis in vectorIndexSet: tupleCount = {} for row in range(oa.nrows()): rowTuple = tuple([oa[row][val] for val in vis]) if rowTuple in tupleCount: tupleCount[rowTuple] += 1 else: tupleCount[rowTuple] = 1 count = 0 for v in tupleCount.values(): if count == 0: count = v elif count != v: return False return True def main(argc, argv): if argc != 4: print("usage: " + argv[0] + " dim order strength") return dim = int(argv[1]) order = int(argv[2]) strength = int(argv[3]) if order < 2 or dim < 1 or strength < 2: print("error: invalid parameter:") print(" dim={}, order={}, strength={}".format(dim, order, strength)) return if dim + 1 < strength: print("error: dim+1 should not be less than strength.") return G = gmat.calcGMatrix(dim, order, strength) print("G matrix:") print(G) Theta = calcThetaMatrix(dim+1, order) print("Theta matrix:") print(Theta) oa = G * Theta oa = oa.transpose() intOa = convertToInteger(oa, order) intOa = Matrix(sorted(intOa)) print("Resulting OA:") print(intOa) print("=== Sanity check ===") if isCorrectOA(intOa, strength): print("Correct OA.") else: print("Invalid OA.") if __name__ == "__main__": main(len(sys.argv), sys.argv)
を求めるところ(calcThetaMatrix関数)がややこしいが、単純なbacktrackアルゴリズムで生成している。
以下に実行例を示す。
shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py usage: ./pg.py dim order strength shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 2 2 2 G matrix: [0 0 1] [1 0 1] [0 1 1] [1 1 1] [0 1 0] [1 1 0] [1 0 0] Theta matrix: [0 0 0 0 1 1 1 1] [0 0 1 1 0 0 1 1] [0 1 0 1 0 1 0 1] Resulting OA: [1 1 1 1 1 1 1] [1 1 2 2 2 2 1] [1 2 1 2 1 2 2] [1 2 2 1 2 1 2] [2 1 1 2 2 1 2] [2 1 2 1 1 2 2] [2 2 1 1 2 2 1] [2 2 2 2 1 1 1] === Sanity check === Correct OA. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 2 2 3 G matrix: [0 0 1] [1 0 1] [0 1 1] [1 1 1] Theta matrix: [0 0 0 0 1 1 1 1] [0 0 1 1 0 0 1 1] [0 1 0 1 0 1 0 1] Resulting OA: [1 1 1 1] [1 1 2 2] [1 2 1 2] [1 2 2 1] [2 1 1 2] [2 1 2 1] [2 2 1 1] [2 2 2 2] === Sanity check === Correct OA. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 3 2 2 G matrix: [0 0 0 1] [1 0 0 1] [0 1 0 1] [1 1 0 1] [0 0 1 1] [1 0 1 1] [0 1 1 1] [1 1 1 1] [0 0 1 0] [1 0 1 0] [0 1 1 0] [1 1 1 0] [0 1 0 0] [1 1 0 0] [1 0 0 0] Theta matrix: [0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1] [0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1] [0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1] [0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1] Resulting OA: [1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] [1 1 1 1 2 2 2 2 2 2 2 2 1 1 1] [1 1 2 2 1 1 2 2 1 1 2 2 2 2 1] [1 1 2 2 2 2 1 1 2 2 1 1 2 2 1] [1 2 1 2 1 2 1 2 1 2 1 2 1 2 2] [1 2 1 2 2 1 2 1 2 1 2 1 1 2 2] [1 2 2 1 1 2 2 1 1 2 2 1 2 1 2] [1 2 2 1 2 1 1 2 2 1 1 2 2 1 2] [2 1 1 2 1 2 2 1 2 1 1 2 2 1 2] [2 1 1 2 2 1 1 2 1 2 2 1 2 1 2] [2 1 2 1 1 2 1 2 2 1 2 1 1 2 2] [2 1 2 1 2 1 2 1 1 2 1 2 1 2 2] [2 2 1 1 1 1 2 2 2 2 1 1 2 2 1] [2 2 1 1 2 2 1 1 1 1 2 2 2 2 1] [2 2 2 2 1 1 1 1 2 2 2 2 1 1 1] [2 2 2 2 2 2 2 2 1 1 1 1 1 1 1] === Sanity check === Correct OA. shinya@DESKTOP-Q1NSEEU:~/programs/blog/2021% ./pg.py 2 4 3 G matrix: [ 0 0 1] [ a 0 1] [ 0 a 1] [ a a 1] [ a 1 0] [a + 1 1 0] Theta matrix: [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 a a a a a a a a a a a a a a a a a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 a + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] [ 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1 0 0 0 0 a a a a a + 1 a + 1 a + 1 a + 1 1 1 1 1] [ 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1 0 a a + 1 1] Resulting OA: [1 1 1 1 1 1] [1 1 2 2 4 4] [1 1 3 3 2 2] [1 1 4 4 3 3] [1 2 1 2 2 3] [1 2 2 1 3 2] [1 2 3 4 1 4] [1 2 4 3 4 1] [1 3 1 3 3 4] [1 3 2 4 2 1] [1 3 3 1 4 3] [1 3 4 2 1 2] [1 4 1 4 4 2] [1 4 2 3 1 3] [1 4 3 2 3 1] [1 4 4 1 2 4] [2 1 1 2 3 2] [2 1 2 1 2 3] [2 1 3 4 4 1] [2 1 4 3 1 4] [2 2 1 1 4 4] [2 2 2 2 1 1] [2 2 3 3 3 3] [2 2 4 4 2 2] [2 3 1 4 1 3] [2 3 2 3 4 2] [2 3 3 2 2 4] [2 3 4 1 3 1] [2 4 1 3 2 1] [2 4 2 4 3 4] [2 4 3 1 1 2] [2 4 4 2 4 3] [3 1 1 3 4 3] [3 1 2 4 1 2] [3 1 3 1 3 4] [3 1 4 2 2 1] [3 2 1 4 3 1] [3 2 2 3 2 4] [3 2 3 2 4 2] [3 2 4 1 1 3] [3 3 1 1 2 2] [3 3 2 2 3 3] [3 3 3 3 1 1] [3 3 4 4 4 4] [3 4 1 2 1 4] [3 4 2 1 4 1] [3 4 3 4 2 3] [3 4 4 3 3 2] [4 1 1 4 2 4] [4 1 2 3 3 1] [4 1 3 2 1 3] [4 1 4 1 4 2] [4 2 1 3 1 2] [4 2 2 4 4 3] [4 2 3 1 2 1] [4 2 4 2 3 4] [4 3 1 2 4 1] [4 3 2 1 1 4] [4 3 3 4 3 2] [4 3 4 3 2 3] [4 4 1 1 3 3] [4 4 2 2 2 2] [4 4 3 3 4 4] [4 4 4 4 1 1] === Sanity check === Correct OA.
上記のプログラムではを転置しての計算結果を出力している点、及び入力する次元の値は射影空間の次元の値としている (つまりの値を入力する仕様である) 点に注意されたい。
また、有限体の元をそのまま出力すると分かりづらいので、以前の記事と同じ方法で整数値に変換している。その後、行を昇順にsortしたものを最終出力としている。
今回も最後に正しく直交表が生成できているかチェックしているが、全てパスしている。
ちなみに
最近気づいたのだが、実はsageには直交表を直接的に求めるための関数が存在している[2]。「なんだ、こんな便利な関数があるのか」とガックリしたのだが、少し触ってみたところ、指数2以上の直交表は構築できないような挙動をしている。単なる私の理解不足かもしれないが、なんだかいまいちな仕様であり、本稿で作ったプログラムも無意味ではなかったかなと思っている。
以下に実行例を示す。第1引数が因子数、第2引数が水準数で、というパラメータに強さの値を与えればよい。
sage: for row in designs.orthogonal_arrays.build(2, 2, t=2): ....: print(row) ....: [0, 0] [0, 1] [1, 0] [1, 1] sage: for row in designs.orthogonal_arrays.build(3, 2, t=2): ....: print(row) ....: [0, 0, 0] [0, 1, 1] [1, 0, 1] [1, 1, 0] sage: for row in designs.orthogonal_arrays.build(4, 2, t=2): ....: print(row) ....: --------------------------------------------------------------------------- EmptySetError Traceback (most recent call last) <ipython-input-156-349912d12039> in <module> ----> 1 for row in designs.orthogonal_arrays.build(Integer(4), Integer(2), t=Integer(2)): 2 print(row) 3 /usr/lib/python3/dist-packages/sage/combinat/designs/orthogonal_arrays.py in build(k, n, t, resolvable) 2142 2143 """ -> 2144 return orthogonal_array(k,n,t,resolvable=resolvable) 2145 2146 @staticmethod /usr/lib/python3/dist-packages/sage/combinat/designs/orthogonal_arrays.py in orthogonal_array(k, n, t, resolvable, check, existence, explain_construction) 891 if explain_construction: 892 return msg --> 893 raise EmptySetError(msg) 894 895 elif k <= t: EmptySetError: There exists no OA(4,2) as k(=4)>n+t-1=3
まとめ
本稿では有限射影幾何の理論を用いた直交表構築手法の証明を行った。その後、実際に直交表が構築できることをsageを使って確かめた。
もともとは単に直交表の生成アルゴリズムを調べていただけなのだが、有限幾何という非常に面白い分野に出くわしたのは幸運であった。せっかくなので、もう少し有限幾何で遊んでみたいと思う。
また、改めてsageの凄さを思い知らされた。計算機上で高度な数学的な計算が実行できるというのは、確実に数学の研究を加速させる有用なテクノロジーだと思う。こういった分野の歴史や手法について調べてみるのも面白いかもしれない。