倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Java で球面上・球体内の一様分布を生成する (4):もう少し使いやすさなどを向上

前回までに単位球面上、球体内で一様分布する点の座標を生成する PolarRandomGenerator クラスを作成しました(一覧)。 ただ、このクラスは実際には少々使い勝手が悪いかと思います。 問題点には以下のものがあります:

  • コンストラクタjava.util.Random オブジェクトをセットするのが面倒
  • 座標をセットする double の配列を使用側が用意しなければならない
  • 座標の次元 n に1以下の整数が指定されたときの挙動が不明 
  • 球面・球体の半径が1に固定されている(単位球面、単位球体)

これらの改善のために、PolarRandomGenerator に新にメソッド等を定義しても良いんですが、新に PolarRandom クラスを作成して、「本質的なアルゴリズム以外を PolarRandom クラスに処理させる」ことにしましょう。

PolarRandom クラスとコンストラクタ


PolarRandom クラスは前回以前に作成した PolarRandomGenerator クラスをフィールドとして持ち、本質的なアルゴリズムによる計算はそちらに行わせるようにします:

public class PolarRandom{

    private final PolarRandomGenerator generator;

    public PolarRandom(){
        this(new Random());
    }
    
    public PolarRandom(long seed){
        this(new Random(seed));
    }
    
    private PolarRandom(Random random){
        this.generator = new PolarRandomGenerator(random);
    }
    
    /**
     * @param x n = x.length (>= 2) としたとき、(n-1) 次元球面上において、
     * 一様分布するランダムな点の n 次元座標をセットする配列
     */
    public void setPointOnSphere(double[] x){
        this.generator.setPointOnSphere(x);
    }

    // 単位球体内の一様分布についても同様
}

チョット解説:

  • コンストラクタjava.util.Random のコンストラクタと同じ引数(デフォルトコンストラクタと乱数の種 (seed) を指定するコンストラクタ)のものを定義し、それによって PolarRandomGenerator オブジェクトを生成・設定しています。
  • setPointOnSphere(), setPointOnBall() メソッドはフィールドの PolarRandomGenerator オブジェクトのメソッドを呼び出しているだけです。

PolarRandom オブジェクトの使い方は以下のようになります:

PolarRandom random = new PolarRandom(1L);
double[] point = new double[3];
random.setPointOnSphere(3);  // 2次元球面上に一様分布する(3次元)座標をセット

double の配列を生成するメソッド


一様分布する点の座標を取得するために毎回 double の配列を作成するのは面倒なので、座標の次元を指定して座標を表す double の配列を返すメソッドを定義しましょう。 メソッド名は

  • nextPointOnSphere(int)
  • nextPointInBall(int)

とします*1。 使い方は次のようになります:

PolarRandom random = ...;

// 2次元球面上に一様分布する点(3次元座標)を生成
double[] x = random.nextPointOnSphere(3);
assert x.length == 3;

うーむ、何て使いやすそうなんだ(笑)

実装例は以下のようになります:

public class PolarRandom{
    ...

    /**
     * @param n 座標の次元 (n >= 2)
     * @return (n-1) 次元球面上において、一様分布するランダムな点の n 次元座標(を表す配列)
     */
    public double[] nextPointOnSphere(int n){
        double[] x = new double[n];
        this.generator.setPointOnSphere(x);
        return x;
    }

    // 単位球体内の一様分布についても同様
}

座標の次元に対する妥当性検証


setPointOnSphere(double), setPointOnBall(double) メソッドは、引数として渡される配列の長さが 0, 1 の場合上手く働きません。 また、nextPointOnSphere(int), nextPointOnBall(int) メソッドは引数の整数が1以下だと上手く動きません。 こういった引数が指定された場合には IllegalArgumentException が投げられるようにしておきましょう*2

public class PolarRandom{
    ...
    
    private void validateDimension(int n){
        if(n <= 1)
            throw new IllegalArgumentException(
                    "座標の次元は2以上でなければなりません。指定された値:"+n);
    }
    
    /**
     * @param x n = x.length (>= 2) としたとき、(n-1) 次元球面上において、
     * 一様分布するランダムな点の n 次元座標をセットする配列
     * @throws IllegalArgumentException 座標の次元 n が1以下 (n &lt;= 1) のとき投げられます。
     */
    public void setPointOnSphere(double[] x){
        validateDimension(x.length);
        this.generator.setPointOnSphere(x);
    }

    /**
     * @param n 座標の次元 (n >= 2)
     * @return (n-1) 次元球面上において、一様分布するランダムな点の n 次元座標(を表す配列)
     * @throws IllegalArgumentException 座標の次元 n が1以下 (n &lt;= 1) のとき投げられます。
     */
    public double[] nextPointOnSphere(int n){
        validateDimension(n);
        double[] x = new double[n];
        this.generator.setPointOnSphere(x);
        return x;
    }

    // 単位球体内の一様分布についても同様
}

球面・球体の半径を指定できるようにする


場合によっては、単位球面・球体(半径が1)以外の場合も扱いたいことがあるでしょう。 この計算は簡単で、単位球面内・球体上の点の座標を計算したあと、それらの成分全てに半径の値を掛ければいいだけです。

メソッドとしては、半径を指定するメソッドをオーバーロードします:

  • setPointOnSphere(int n, double radius)
  • nextPointOnSphere(int n, double radius)
  • setPointInBall(int n, double radius)
  • nextPointInBall(int n, double radius)

これらのメソッドの使い方は次のようになります:

PolarRandom random = ...;

// 半径10の2次元球面上に一様分布する点(3次元座標)を生成
double[] x = random.nextPointOnSphere(3, 10.0);
assert x.length == 3;

実装例:

public class PolarRandom{
    ...
    
    /** 各座標を radius 倍する */
    private void enlarge(double[] x, double radius){
        for(int i = 0, n = x.length; i < n; i++)
            x[i] *= radius;
    }
    
    /**
     * @param x n = x.length (>= 2) としたとき、(n-1) 次元球面上において、
     * 一様分布するランダムな点の n 次元座標をセットする配列
     * @param radius n 次元球面の半径
     * @throws IllegalArgumentException 座標の次元 n が1以下 (n &lt;= 1) のとき投げられます。
     */
    public void setPointOnSphere(double[] x, double radius){
        setPointOnSphere(x);
        enlarge(x, radius);
    }
    
    /**
     * @param n 座標の次元 (n >= 2)
     * @param radius n 次元球面の半径
     * @return (n-1) 次元球面上において、一様分布するランダムな点の n 次元座標(を表す配列)
     * @throws IllegalArgumentException 座標の次元 n が1以下 (n &lt;= 1) のとき投げられます。
     */
    public double[] nextPointOnSphere(int n, double radius){
        double[] x = nextPointOnSphere(n);
        enlarge(x, radius);
        return x;
    }

    // 単位球体内の一様分布についても同様
}

コンストラクタ宣言とメソッド宣言のまとめ


PolarRandom クラスに定義されている publicコンストラクタ、メソッドをまとめると以下のようになります:

public class PolarRandom{
    
    public PolarRandom(){...}
    public PolarRandom(long seed){...}

    public void setPointOnSphere(double[] x){...}
    public void setPointOnSphere(double[] x, double radius){...}

    public double[] nextPointOnSphere(int n){...}
    public double[] nextPointOnSphere(int n, double radius){...}
    
    public void setPointInBall(double[] x){...}
    public void setPointInBall(double[] x, double radius){...}
    
    public double[] nextPointInBall(int n){...}
    public double[] nextPointInBall(int n, double radius){...}
}

*1:java.util.Random クラスには nextDouble(), nextInt() など、nextXxxx() という名前のメソッドが定義されているのを参考にしました。

*2:1次元球体(直線)内の一様分布を生成する場合、例外を投げずに直線上の一様分布を返す方がいいかも知れませんが、おそらく、その一様分布は java.util.Random#nextDouble() を使ったほうが効率的でしょう。 ということで、ここでは例外を投げるようにします