SobolSequenceGenerator.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.math4.legacy.random;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.Charset;
  23. import java.util.Arrays;
  24. import java.util.NoSuchElementException;
  25. import java.util.StringTokenizer;
  26. import java.util.function.Supplier;

  27. import org.apache.commons.math4.legacy.exception.MathInternalError;
  28. import org.apache.commons.math4.legacy.exception.MathParseException;
  29. import org.apache.commons.math4.legacy.exception.NotStrictlyPositiveException;
  30. import org.apache.commons.math4.legacy.exception.OutOfRangeException;
  31. import org.apache.commons.math4.core.jdkmath.JdkMath;

  32. /**
  33.  * Implementation of a Sobol sequence.
  34.  * <p>
  35.  * A Sobol sequence is a low-discrepancy sequence with the property that for all values of N,
  36.  * its subsequence (x1, ... xN) has a low discrepancy. It can be used to generate pseudo-random
  37.  * points in a space S, which are equi-distributed.
  38.  * <p>
  39.  * The implementation already comes with support for up to 21201 dimensions with direction numbers
  40.  * calculated from <a href="http://web.maths.unsw.edu.au/~fkuo/sobol/">Stephen Joe and Frances Kuo</a>.
  41.  * <p>
  42.  * The generator supports two modes:
  43.  * <ul>
  44.  *   <li>sequential generation of points: {@link #get()}</li>
  45.  *   <li>random access to the i-th point in the sequence: {@link #skipTo(int)}</li>
  46.  * </ul>
  47.  *
  48.  * @see <a href="http://en.wikipedia.org/wiki/Sobol_sequence">Sobol sequence (Wikipedia)</a>
  49.  * @see <a href="http://web.maths.unsw.edu.au/~fkuo/sobol/">Sobol sequence direction numbers</a>
  50.  *
  51.  * @since 3.3
  52.  */
  53. public class SobolSequenceGenerator implements Supplier<double[]> {
  54.     /** The number of bits to use. */
  55.     private static final int BITS = 52;

  56.     /** The scaling factor. */
  57.     private static final double SCALE = JdkMath.pow(2, BITS);

  58.     /** The maximum supported space dimension. */
  59.     private static final int MAX_DIMENSION = 21201;

  60.     /** The resource containing the direction numbers. */
  61.     private static final String RESOURCE_NAME = "/assets/org/apache/commons/math4/legacy/random/new-joe-kuo-6.21201";

  62.     /** Character set for file input. */
  63.     private static final String FILE_CHARSET = "US-ASCII";

  64.     /** Space dimension. */
  65.     private final int dimension;

  66.     /** The current index in the sequence. */
  67.     private int count;

  68.     /** The direction vector for each component. */
  69.     private final long[][] direction;

  70.     /** The current state. */
  71.     private final long[] x;

  72.     /**
  73.      * Construct a new Sobol sequence generator for the given space dimension.
  74.      *
  75.      * @param dimension the space dimension
  76.      * @throws OutOfRangeException if the space dimension is outside the allowed range of [1, 21201]
  77.      */
  78.     public SobolSequenceGenerator(final int dimension) {
  79.         if (dimension < 1 || dimension > MAX_DIMENSION) {
  80.             throw new OutOfRangeException(dimension, 1, MAX_DIMENSION);
  81.         }

  82.         // initialize the other dimensions with direction numbers from a resource
  83.         final InputStream is = getClass().getResourceAsStream(RESOURCE_NAME);
  84.         if (is == null) {
  85.             throw new MathInternalError();
  86.         }

  87.         this.dimension = dimension;

  88.         // init data structures
  89.         direction = new long[dimension][BITS + 1];
  90.         x = new long[dimension];

  91.         try {
  92.             initFromStream(is);
  93.         } catch (IOException e) {
  94.             // the internal resource file could not be read -> should not happen
  95.             throw new MathInternalError();
  96.         } catch (MathParseException e) {
  97.             // the internal resource file could not be parsed -> should not happen
  98.             throw new MathInternalError();
  99.         } finally {
  100.             try {
  101.                 is.close();
  102.             } catch (IOException e) { // NOPMD
  103.                 // ignore
  104.             }
  105.         }
  106.     }

  107.     /**
  108.      * Construct a new Sobol sequence generator for the given space dimension with
  109.      * direction vectors loaded from the given stream.
  110.      * <p>
  111.      * The expected format is identical to the files available from
  112.      * <a href="http://web.maths.unsw.edu.au/~fkuo/sobol/">Stephen Joe and Frances Kuo</a>.
  113.      * The first line will be ignored as it is assumed to contain only the column headers.
  114.      * The columns are:
  115.      * <ul>
  116.      *  <li>d: the dimension</li>
  117.      *  <li>s: the degree of the primitive polynomial</li>
  118.      *  <li>a: the number representing the coefficients</li>
  119.      *  <li>m: the list of initial direction numbers</li>
  120.      * </ul>
  121.      * Example:
  122.      * <pre>
  123.      * d       s       a       m_i
  124.      * 2       1       0       1
  125.      * 3       2       1       1 3
  126.      * </pre>
  127.      * <p>
  128.      * The input stream <i>must</i> be an ASCII text containing one valid direction vector per line.
  129.      *
  130.      * @param dimension the space dimension
  131.      * @param is the stream to read the direction vectors from
  132.      * @throws NotStrictlyPositiveException if the space dimension is &lt; 1
  133.      * @throws OutOfRangeException if the space dimension is outside the range [1, max], where
  134.      *   max refers to the maximum dimension found in the input stream
  135.      * @throws MathParseException if the content in the stream could not be parsed successfully
  136.      * @throws IOException if an error occurs while reading from the input stream
  137.      */
  138.     public SobolSequenceGenerator(final int dimension, final InputStream is)
  139.         throws IOException {
  140.         if (dimension < 1) {
  141.             throw new NotStrictlyPositiveException(dimension);
  142.         }

  143.         this.dimension = dimension;

  144.         // init data structures
  145.         direction = new long[dimension][BITS + 1];
  146.         x = new long[dimension];

  147.         // initialize the other dimensions with direction numbers from the stream
  148.         int lastDimension = initFromStream(is);
  149.         if (lastDimension < dimension) {
  150.             throw new OutOfRangeException(dimension, 1, lastDimension);
  151.         }
  152.     }

  153.     /**
  154.      * Load the direction vector for each dimension from the given stream.
  155.      * <p>
  156.      * The input stream <i>must</i> be an ASCII text containing one
  157.      * valid direction vector per line.
  158.      *
  159.      * @param is the input stream to read the direction vector from
  160.      * @return the last dimension that has been read from the input stream
  161.      * @throws IOException if the stream could not be read
  162.      * @throws MathParseException if the content could not be parsed successfully
  163.      */
  164.     private int initFromStream(final InputStream is) throws IOException {
  165.         // special case: dimension 1 -> use unit initialization
  166.         for (int i = 1; i <= BITS; i++) {
  167.             direction[0][i] = 1L << (BITS - i);
  168.         }

  169.         final Charset charset = Charset.forName(FILE_CHARSET);
  170.         final BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset));
  171.         int dim = -1;

  172.         try {
  173.             // ignore first line
  174.             reader.readLine();

  175.             int lineNumber = 2;
  176.             int index = 1;
  177.             String line = null;
  178.             while ( (line = reader.readLine()) != null) {
  179.                 StringTokenizer st = new StringTokenizer(line, " ");
  180.                 try {
  181.                     dim = Integer.parseInt(st.nextToken());
  182.                     if (dim >= 2 && dim <= dimension) { // we have found the right dimension
  183.                         final int s = Integer.parseInt(st.nextToken());
  184.                         final int a = Integer.parseInt(st.nextToken());
  185.                         final int[] m = new int[s + 1];
  186.                         for (int i = 1; i <= s; i++) {
  187.                             m[i] = Integer.parseInt(st.nextToken());
  188.                         }
  189.                         initDirectionVector(index++, a, m);
  190.                     }

  191.                     if (dim > dimension) {
  192.                         return dim;
  193.                     }
  194.                 } catch (NoSuchElementException e) {
  195.                     throw new MathParseException(line, lineNumber);
  196.                 } catch (NumberFormatException e) {
  197.                     throw new MathParseException(line, lineNumber);
  198.                 }
  199.                 lineNumber++;
  200.             }
  201.         } finally {
  202.             reader.close();
  203.         }

  204.         return dim;
  205.     }

  206.     /**
  207.      * Calculate the direction numbers from the given polynomial.
  208.      *
  209.      * @param d the dimension, zero-based
  210.      * @param a the coefficients of the primitive polynomial
  211.      * @param m the initial direction numbers
  212.      */
  213.     private void initDirectionVector(final int d, final int a, final int[] m) {
  214.         final int s = m.length - 1;
  215.         for (int i = 1; i <= s; i++) {
  216.             direction[d][i] = ((long) m[i]) << (BITS - i);
  217.         }
  218.         for (int i = s + 1; i <= BITS; i++) {
  219.             direction[d][i] = direction[d][i - s] ^ (direction[d][i - s] >> s);
  220.             for (int k = 1; k <= s - 1; k++) {
  221.                 direction[d][i] ^= ((a >> (s - 1 - k)) & 1) * direction[d][i - k];
  222.             }
  223.         }
  224.     }

  225.     /** {@inheritDoc} */
  226.     @Override
  227.     public double[] get() {
  228.         final double[] v = new double[dimension];
  229.         if (count == 0) {
  230.             count++;
  231.             return v;
  232.         }

  233.         // find the index c of the rightmost 0
  234.         int c = 1;
  235.         int value = count - 1;
  236.         while ((value & 1) == 1) {
  237.             value >>= 1;
  238.             c++;
  239.         }

  240.         for (int i = 0; i < dimension; i++) {
  241.             x[i] ^= direction[i][c];
  242.             v[i] = (double) x[i] / SCALE;
  243.         }
  244.         count++;
  245.         return v;
  246.     }

  247.     /**
  248.      * Skip to the i-th point in the Sobol sequence.
  249.      * <p>
  250.      * This operation can be performed in O(1).
  251.      *
  252.      * @param index the index in the sequence to skip to
  253.      * @return the i-th point in the Sobol sequence
  254.      * @throws org.apache.commons.math4.legacy.exception.NotPositiveException NotPositiveException if index &lt; 0
  255.      */
  256.     public double[] skipTo(final int index) {
  257.         if (index == 0) {
  258.             // reset x vector
  259.             Arrays.fill(x, 0);
  260.         } else {
  261.             final int i = index - 1;
  262.             final long grayCode = i ^ (i >> 1); // compute the gray code of i = i XOR floor(i / 2)
  263.             for (int j = 0; j < dimension; j++) {
  264.                 long result = 0;
  265.                 for (int k = 1; k <= BITS; k++) {
  266.                     final long shift = grayCode >> (k - 1);
  267.                     if (shift == 0) {
  268.                         // stop, as all remaining bits will be zero
  269.                         break;
  270.                     }
  271.                     // the k-th bit of i
  272.                     final long ik = shift & 1;
  273.                     result ^= ik * direction[j][k];
  274.                 }
  275.                 x[j] = result;
  276.             }
  277.         }
  278.         count = index;
  279.         return get();
  280.     }

  281.     /**
  282.      * Returns the index i of the next point in the Sobol sequence that will be returned
  283.      * by calling {@link #get()}.
  284.      *
  285.      * @return the index of the next point
  286.      */
  287.     public int getNextIndex() {
  288.         return count;
  289.     }
  290. }