LoginSignup
2
0

【Excel not VBA】Excel で xoshiro128**を書いてみた

Last updated at Posted at 2023-12-12

どうも、3 月 5 日を勝手に「世界ビット演算デー」にしたいたぬきです。なんで 3 月 5 日なのかって? $3_{10} = 11_2, 5_{10} = 101_2$ なので並べるだけで真理値表になるからです。ちなみに実際は「軍縮不拡散に関する啓発のための国際デー」らしいです。めちゃシリアスじゃん。

本記事は misskey.dev ユーザー Advent Calendar 2023 の 13 日目の記事でもあります。Qiitadon 爆破事件のあと dev に流れ着いた身なので、まあ Qiita に投げ込んでも問題ないやろ。

misskey.dev にまつわる記事と思いきや、

misskey.dev のユーザーならなんでもどうぞ

とのことなので、好き勝手書きます。私が好きなものといえばビット演算と Excel です。

初期空間を指定できる乱数関数がほしい

Excel には乱数を得る関数が色々ありますが、シード値を設定することができません。モンテカルロ法のシミュレーションで Excel を使うとすると、出た乱数を全部「値としてコピー」しなきゃいけないわけです。複数パターンの検証とか大変ですよね。

じゃあ自分で実装すればいいじゃない。というわけで xorshift 派生アルゴリズムの xoshiro ファミリーを実装していきます。Excel の数値型は(通貨を除いて)おおむね binary64 なので、32 bits 整数範囲は正確に表現できるはずです。よって xoshiro128**を実装します。

VBA ではなく LAMBDA で実装するため、マクロ禁止環境でも利用できます。

ユーティリティ関数ども

まず先に実装の際に便利ないくつかの関数を定義します。

AS32BITS

32 bits 整数範囲になるようマスクする関数です。binary64 では 53 bits までなら正確に表現できますが、今回の要件の場合シフト命令とかで正確に表現されると困るので、この関数を使います。

AS32BITS
= LAMBDA(var_n, BITAND(var_n, 4294967295))

image.png

BITLSHIFT32

BITLSHIFTAS32BITS してくれる関数です。それだけなのですが、毎回関数を二つ書くのは面倒なので(あと書き忘れとか怖い)この関数を使います。

BITLSHIFT32
= LAMBDA(var_n, var_width, AS32BITS(BITLSHIFT(var_n, var_width)))

image.png

BITLROTATE32

BITLSHIFT32 してはみ出たビットを右につけたす、いわゆるローテート演算を行う関数です。ローテートを利用するのが xoshiro ファミリーの特徴です。

BITLROTATE32
= LAMBDA(var_n, var_width, BITOR(BITLSHIFT32(var_n, var_width), BITRSHIFT(var_n, 32 - var_width)))

image.png

FLATINDEX

Excel に存在する INDEX 関数は行と列の添字をそれぞれ指定しますが、これを Z 字にたどることを想定して一つの添字で参照するための関数です。

(1, 1) (1, 2) (1, 3)
(2, 1) (2, 2) (2, 3)
(3, 1) (3, 2) (3, 3)

これが公式の INDEX ですが、これを

1 2 3
4 5 6
7 8 9

のように参照できるようにするのが FLATINDEX です。

SEQUENCE 関数と併用することでこのあとに定義する RESHAPE 関数の実装を容易にします。

FLATINDEX
= LAMBDA(var_array, var_i, LET(var_cols_num, COLUMNS(var_array), var_row_i, QUOTIENT(var_i - 1, var_cols_num) + 1, var_col_i, MOD(var_i - 1, var_cols_num) + 1, INDEX(var_array, var_row_i, var_col_i)))

これだと処理を追いづらいので整形すると

FLATINDEX
= LAMBDA(var_array, var_i,
    LET(
        var_cols_num, COLUMNS(var_array),
        var_row_i,    QUOTIENT(var_i - 1, var_cols_num) + 1,
        var_col_i,    MOD(var_i - 1, var_cols_num) + 1,

        INDEX(var_array, var_row_i, var_col_i)
    )
)

といった感じです。

image.png

RESHAPE

配列の形状を要素数が同じ別の形状に変更します。とりあえず 1 列でバーッと乱数を出したあと所望の形状に変更するために使います。

RESHAPE
= LAMBDA(var_array, var_rows_num, var_cols_num, FLATINDEX(var_array, SEQUENCE(var_rows_num, var_cols_num)))

image.png

状態遷移関数

いよいよ xoshiro128**を実装していきます。といってもリファレンス実装を Excel 数式で書き下していくだけです。ただ、Excel 数式はプログラミング風にいうと宣言的なので、変数の書き換えとかはできません。よって一時変数をふたつ増やして実装しています。

必要な行ぶんの状態を一気に出していきます。繰り返しは使えないので再帰で実装します。1 行だけの場合は与えられた空間をそのまま吐きます。2 行以上の場合は再帰で 1 行前までの状態を吐き、その下に新しい行を追加します。

ところでこの「行を追加する」、HSTACK / VSTACK 関数ができてほんとに楽に書けるようになりました。配列リテラルは中身もリテラルじゃないといけない(数式を書けない)問題などがあり、かつては SEQUENCE 関数の結果を IFS 関数で書き換えるみたいなハックが必要でした。

XOSHIRO128STATE
= LAMBDA(var_states_num, var_state_1, var_state_2, var_state_3, var_state_4, IF(var_states_num = 1, HSTACK(AS32BITS(var_state_1), AS32BITS(var_state_2), AS32BITS(var_state_3), AS32BITS(var_state_4)), LET(var_old, XOSHIRO128STATE(var_states_num - 1, var_state_1, var_state_2, var_state_3, var_state_4), var_old_state_1, INDEX(var_old, var_states_num - 1, 1), var_old_state_2, INDEX(var_old, var_states_num - 1, 2), var_old_state_3, INDEX(var_old, var_states_num - 1, 3), var_old_state_4, INDEX(var_old, var_states_num - 1, 4), var_t, BITLSHIFT32(var_old_state_2, 9), var_u, BITXOR(var_old_state_3, var_old_state_1), var_v, BITXOR(var_old_state_4, var_old_state_2), var_new_state_1, BITXOR(var_old_state_1, var_v), var_new_state_2, BITXOR(var_old_state_2, var_u), var_new_state_3, BITXOR(var_u, var_t), var_new_state_4, BITLROTATE32(var_v, 11), var_new, HSTACK(var_new_state_1, var_new_state_2, var_new_state_3, var_new_state_4), VSTACK(var_old, var_new))))

これも整形すると、

XOSHIRO128STATE
= LAMBDA(var_states_num, var_state_1, var_state_2, var_state_3, var_state_4,
    IF(var_states_num = 1,
        HSTACK(AS32BITS(var_state_1), AS32BITS(var_state_2), AS32BITS(var_state_3), AS32BITS(var_state_4)),
        LET(
            var_old,         XOSHIRO128STATE(var_states_num - 1, var_state_1, var_state_2, var_state_3, var_state_4),
            var_old_state_1, INDEX(var_old, var_states_num - 1, 1),
            var_old_state_2, INDEX(var_old, var_states_num - 1, 2),
            var_old_state_3, INDEX(var_old, var_states_num - 1, 3),
            var_old_state_4, INDEX(var_old, var_states_num - 1, 4),
            var_t,           BITLSHIFT32(var_old_state_2, 9),
            var_u,           BITXOR(var_old_state_3, var_old_state_1),
            var_v,           BITXOR(var_old_state_4, var_old_state_2),
            var_new_state_1, BITXOR(var_old_state_1, var_v),
            var_new_state_2, BITXOR(var_old_state_2, var_u),
            var_new_state_3, BITXOR(var_u, var_t),
            var_new_state_4, BITLROTATE32(var_v, 11),
            var_new,         HSTACK(var_new_state_1, var_new_state_2, var_new_state_3, var_new_state_4),

            VSTACK(var_old, var_new)
        )
    )
)

となります。いやだいぶ長いな…

image.png

出力関数

次にこれらの内部空間から乱数の出力を得る関数を実装します。xoshiro128**では内部空間のうち 2 番目(1 オリジンで)のみを使います。

XOSHIRO128STARSTAROUTPUT
= LAMBDA(var_states, LET(var_states_num, ROWS(var_states), var_state_2_s, MAP(SEQUENCE(var_states_num), LAMBDA(row_i, INDEX(var_states, row_i, 2))), AS32BITS(BITLROTATE32(AS32BITS(var_state_2_s * 5), 7) * 9)))

整形すると、

XOSHIRO128STARSTAROUTPUT
= LAMBDA(var_states,
    LET(
        var_states_num, ROWS(var_states),
        var_state_2_s,  MAP(SEQUENCE(var_states_num), LAMBDA(row_i, INDEX(var_states, row_i, 2))),

        AS32BITS(BITLROTATE32(AS32BITS(var_state_2_s * 5), 7) * 9)
    )
)

となります。

image.png

なお、OFFSET 関数を使わず SEQUENCE 関数の戻り値に MAP 関数をかけるというややまだるっこしいことをしていますが、OFFSET 関数は、LET 関数で束縛された式などのような「実際にシート上にあるわけではない配列」に対してエラーを返す場合があるなど、やや動作が非直観的なため、ここでは使用していません。

UI

最後に、ユーザーが実際に使用するための関数を実装します。

XOSHIRO128STARSTAR
= LAMBDA(var_rows_num, var_cols_num, var_seed, LET(var_rands_num, var_rows_num * var_cols_num, var_seed_is_array, ROWS(var_seed) * COLUMNS(var_seed) > 1, var_seed_1, IF(var_seed_is_array, FLATINDEX(var_seed, 1), var_seed), var_seed_2, IF(var_seed_is_array, FLATINDEX(var_seed, 2), MOD(48271 * var_seed_1, 2147483647)), var_seed_3, IF(var_seed_is_array, FLATINDEX(var_seed, 3), MOD(48271 * var_seed_2, 2147483647)), var_seed_4, IF(var_seed_is_array, FLATINDEX(var_seed, 4), MOD(48271 * var_seed_3, 2147483647)), var_states, XOSHIRO128STATE(var_rands_num, var_seed_1, var_seed_2, var_seed_3, var_seed_4), var_rands, XOSHIRO128STARSTAROUTPUT(var_states), RESHAPE(var_rands, var_rows_num, var_cols_num)))

整形します。

XOSHIRO128STARSTAR
= LAMBDA(var_rows_num, var_cols_num, var_seed,
    LET(
        var_rands_num,     var_rows_num * var_cols_num,
        var_seed_is_array, ROWS(var_seed) * COLUMNS(var_seed) > 1,
        var_seed_1,        IF(var_seed_is_array, FLATINDEX(var_seed, 1), var_seed),
        var_seed_2,        IF(var_seed_is_array, FLATINDEX(var_seed, 2), MOD(48271 * var_seed_1, 2147483647)),
        var_seed_3,        IF(var_seed_is_array, FLATINDEX(var_seed, 3), MOD(48271 * var_seed_2, 2147483647)),
        var_seed_4,        IF(var_seed_is_array, FLATINDEX(var_seed, 4), MOD(48271 * var_seed_3, 2147483647)),
        var_states,        XOSHIRO128STATE(var_rands_num, var_seed_1, var_seed_2, var_seed_3, var_seed_4),
        var_rands,         XOSHIRO128STARSTAROUTPUT(var_states),

        RESHAPE(var_rands, var_rows_num, var_cols_num)
    )
)

image.png

要求量の乱数を生成して RESHAPE します。シードはひとつの値でも 4 個(以上)の値を持つ配列でも構いません。ひとつの値を与えた場合、残りの空間は線形合同法で埋めることにしました(正直微妙ではある)。

image.png

リファレンス実装の結果と比較。問題なさそうです。

おまけ

image.png

ほぎゃーーーーーっ!!

はい、Excel の再帰は割と貧弱なのでそんなにたくさんの乱数は作れません1。だめじゃん。

  1. 今回の関数の場合 861 個が限界でした。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0