def test_construct_vol_covar_and_scalar_vol(self):
        dtype = np.float64
        vol = tf.constant(0.94, dtype=dtype)
        # Note that the total covariance function we supply is deliberately not
        # the one that is implied by the volatility function.
        np.random.seed(1235)
        dim = 5
        vol_matrix = np.random.randn(dim, dim)
        covar_matrix = np.matmul(vol_matrix, vol_matrix.transpose())

        def tc_fn(t1, t2):
            return bm_utils.outer_multiply(t2 - t1, covar_matrix)

        times = np.array([[0.12, 0.44], [0.48, 1.698]]).astype(dtype)
        actual_vol_fn, actual_tc_fn = bm_utils.construct_vol_data(
            vol, tc_fn, dim, dtype)
        actual_vols = self.evaluate(actual_vol_fn(times))
        np.testing.assert_array_equal(actual_vols.shape, [2, 2, dim, dim])
        for i in range(2):
            for j in range(2):
                np.testing.assert_allclose(actual_vols[i, j],
                                           np.eye(dim).astype(dtype) * 0.94)

        actual_tc = self.evaluate(actual_tc_fn(times, times + 1.0))
        np.testing.assert_array_equal(actual_tc.shape, [2, 2, dim, dim])
        for i in range(2):
            for j in range(2):
                np.testing.assert_allclose(actual_tc[i, j], covar_matrix)
    def test_construct_vol_covar_and_no_vol(self):
        # Check the None, None
        dtype = np.float64
        covar_matrix = np.array([[0.15, 0.30], [0.30, 0.60]]).astype(dtype)

        def covar_fn(t1, t2):
            return bm_utils.outer_multiply(t2 - t1, covar_matrix)

        vol_fn, covar_fn = bm_utils.construct_vol_data(None, covar_fn, 2,
                                                       dtype)

        times = tf.constant([0.5, 1.0, 2.0, 3.0], dtype=dtype)
        vols = self.evaluate(vol_fn(times))
        np.testing.assert_array_equal(vols.shape, [4, 2, 2])
        # Directly testing vols is not sensible because given a covariance matrix
        # there are many volatility matrices that would lead to the same covar.
        # So we instead check the implied covariance.
        for i in range(4):
            actual_covar = np.matmul(vols[i], vols[i].transpose())
            np.testing.assert_allclose(actual_covar, covar_matrix)
        times2 = times + 0.31415
        # Check that the total covariance is correct.
        tc = self.evaluate(covar_fn(times, times2))
        np.testing.assert_array_equal(tc.shape, [4, 2, 2])
        # Directly testing vols is not sensible because given a covariance matrix
        # there are many volatility matrices that would lead to the same covar.
        # So we instead check the implied covariance.
        for i in range(4):
            np.testing.assert_allclose(tc[i], covar_matrix * 0.31415)
 def test_construct_vol_defaults(self):
     # Check the None, None
     dtype = np.float64
     vol_fn, _ = bm_utils.construct_vol_data(None, None, 2, dtype)
     times = tf.constant([0.5, 1.0, 2.0, 3.0], dtype=dtype)
     vols = self.evaluate(vol_fn(times))
     np.testing.assert_array_equal(vols.shape, [4, 2, 2])
    def test_construct_vol_no_covar_and_vector_vol(self):
        dtype = np.float64
        vol = np.array([0.94, 1.1, 0.42], dtype=dtype)
        np.random.seed(5321)
        dim = 3
        vol_matrix = np.diag(vol)
        covar_matrix = np.matmul(vol_matrix, vol_matrix.transpose())

        times = np.array([[0.12], [0.48]]).astype(dtype)
        actual_vol_fn, actual_tc_fn = bm_utils.construct_vol_data(
            tf.constant(vol), None, dim, dtype)
        actual_vols = self.evaluate(actual_vol_fn(times))
        np.testing.assert_array_equal(actual_vols.shape, [2, 1, dim, dim])
        for i in range(2):
            np.testing.assert_allclose(actual_vols[i, 0], np.diag(vol))
        actual_tc = self.evaluate(actual_tc_fn(times, times + 0.22))
        np.testing.assert_array_equal(actual_tc.shape, [2, 1, dim, dim])
        for i in range(2):
            np.testing.assert_allclose(actual_tc[i, 0], covar_matrix * 0.22)
    def test_construct_vol_no_covar_and_scalar_vol(self):
        dtype = np.float64
        vol = tf.constant(0.94, dtype=dtype)
        np.random.seed(1235)
        dim = 5
        covar_matrix = np.eye(dim) * 0.94 * 0.94
        times = np.array([[0.12, 0.44], [0.48, 1.698]]).astype(dtype)
        actual_vol_fn, actual_tc_fn = bm_utils.construct_vol_data(
            vol, None, dim, dtype)
        actual_vols = self.evaluate(actual_vol_fn(times))
        np.testing.assert_array_equal(actual_vols.shape, [2, 2, dim, dim])
        for i in range(2):
            for j in range(2):
                np.testing.assert_allclose(actual_vols[i, j],
                                           np.eye(dim).astype(dtype) * 0.94)

        actual_tc = self.evaluate(actual_tc_fn(times, times + 1.0))
        np.testing.assert_array_equal(actual_tc.shape, [2, 2, dim, dim])
        for i in range(2):
            for j in range(2):
                np.testing.assert_allclose(actual_tc[i, j], covar_matrix)
    def test_construct_vol_covar_and_vol_callables(self):
        dtype = np.float64
        vol_matrix = np.array([[1.0, 0.21, -0.33], [0.61, 1.5, 1.77],
                               [-0.3, 1.19, -0.55]]).astype(dtype)
        covar_matrix = np.matmul(vol_matrix, vol_matrix.transpose())
        vol_fn = lambda time: bm_utils.outer_multiply(time, vol_matrix)

        def tc_fn(t1, t2):
            return bm_utils.outer_multiply((t2**2 - t1**2) / 2, covar_matrix)

        times = np.array([[0.12, 0.44], [0.48, 1.698]]).astype(dtype)
        actual_vol_fn, actual_tc_fn = bm_utils.construct_vol_data(
            vol_fn, tc_fn, 3, dtype)
        actual_vols = self.evaluate(actual_vol_fn(times))
        np.testing.assert_array_equal(actual_vols.shape, [2, 2, 3, 3])
        np.testing.assert_allclose(actual_vols, self.evaluate(vol_fn(times)))
        times2 = times + np.array([[0.12, 0.34], [0.56, 0.78]]).astype(dtype)
        actual_tc = self.evaluate(actual_tc_fn(times, times2))
        np.testing.assert_array_equal(actual_tc.shape, [2, 2, 3, 3])
        np.testing.assert_allclose(actual_tc,
                                   self.evaluate(actual_tc_fn(times, times2)))
  def test_construct_vol_no_covar_and_vol_matrix(self):
    dtype = np.float64
    vol_matrix = np.array([[1.0, 0.21, -0.33], [0.61, 1.5, 1.77],
                           [-0.3, 1.19, -0.55]]).astype(dtype)
    covar_matrix = np.matmul(vol_matrix, vol_matrix.transpose())
    times = np.array([[0.12, 0.44], [0.48, 1.698]]).astype(dtype)
    actual_vol_fn, actual_tc_fn = bm_utils.construct_vol_data(
        vol_matrix, None, 3, dtype)
    actual_vols = self.evaluate(actual_vol_fn(times))
    np.testing.assert_array_equal(actual_vols.shape, [2, 2, 3, 3])
    for i in range(2):
      for j in range(2):
        np.testing.assert_allclose(actual_vols[i, j], vol_matrix)

    dt = np.array([[0.12, 0.34], [0.56, 0.78]]).astype(dtype)
    times2 = times + dt
    actual_tc = self.evaluate(actual_tc_fn(times, times2))
    np.testing.assert_array_equal(actual_tc.shape, [2, 2, 3, 3])
    for i in range(2):
      for j in range(2):
        np.testing.assert_allclose(actual_tc[i, j], covar_matrix * dt[i, j])
    def test_construct_vol_covar_and_vector_vol(self):
        dtype = np.float64
        vol = np.array([0.94, 1.1, 0.42], dtype=dtype)
        # Note that the total covariance function we supply is deliberately not
        # the one that is implied by the volatility function.
        np.random.seed(5321)
        dim = 3
        vol_matrix = np.random.randn(dim, dim)
        covar_matrix = np.matmul(vol_matrix, vol_matrix.transpose())

        def tc_fn(t1, t2):
            return bm_utils.outer_multiply(t2 - t1, covar_matrix)

        times = np.array([[0.12], [0.48]]).astype(dtype)
        actual_vol_fn, actual_tc_fn = bm_utils.construct_vol_data(
            tf.constant(vol), tc_fn, dim, dtype)
        actual_vols = self.evaluate(actual_vol_fn(times))
        np.testing.assert_array_equal(actual_vols.shape, [2, 1, dim, dim])
        for i in range(2):
            np.testing.assert_allclose(actual_vols[i, 0], np.diag(vol))
        actual_tc = self.evaluate(actual_tc_fn(times, times + 0.22))
        np.testing.assert_array_equal(actual_tc.shape, [2, 1, dim, dim])
        for i in range(2):
            np.testing.assert_allclose(actual_tc[i, 0], covar_matrix * 0.22)
    def __init__(self,
                 dim=1,
                 drift=None,
                 volatility=None,
                 total_drift_fn=None,
                 total_covariance_fn=None,
                 dtype=None,
                 name=None):
        """Initializes the Brownian motion class.

    Represents the Ito process:

    ```None
      dX_i = a_i(t) dt + Sum(S_{ij}(t) dW_j for j in [1 ... n]), 1 <= i <= n

    ```

    `a_i(t)` is the drift rate of this process and the `S_{ij}(t)` is the
    volatility matrix. Associated to these parameters are the integrated
    drift and covariance functions. These are defined as:

    ```None
      total_drift_{i}(t1, t2) = Integrate(a_{i}(t), t1 <= t <= t2)
      total_covariance_{ij}(t1, t2) = Integrate(inst_covariance_{ij}(t),
                                                     t1 <= t <= t2)
      inst_covariance_{ij}(t) = (S.S^T)_{ij}(t)
    ```

    Sampling from the Brownian motion process with time dependent parameters
    can be done efficiently if the total drift and total covariance functions
    are supplied. If the parameters are constant, the total parameters can be
    automatically inferred and it is not worth supplying then explicitly.

    Currently, it is not possible to infer the total drift and covariance from
    the instantaneous values if the latter are functions of time. In this case,
    we use a generic sampling method (Euler-Maruyama) which may be
    inefficient. It is advisable to supply the total covariance and total drift
    in the time dependent case where possible.

    ## Example
    The following is an example of a 1 dimensional brownian motion using default
    arguments of zero drift and unit volatility.

    ```python
    process = bm.BrownianMotion()
    times = np.array([0.2, 0.33, 0.7, 0.9, 1.88])
    num_samples = 10000
    with tf.Session() as sess:
      paths = sess.run(process.sample_paths(
          times,
          num_samples=num_samples,
          initial_state=np.array(0.1),
          seed=1234))

    # Compute the means at the specified times.
    means = np.mean(paths, axis=0)
    print (means)  # Mean values will be near 0.1 for each time

    # Compute the covariances at the given times
    covars = np.cov(paths.reshape([num_samples, 5]), rowvar=False)

    # covars is a 5 x 5 covariance matrix.
    # Expected result is that Covar(X(t), X(t')) = min(t, t')
    expected = np.minimum(times.reshape([-1, 1]), times.reshape([1, -1]))
    print ("Computed Covars: {}, True Covars: {}".format(covars, expected))
    ```

    Args:
      dim: Python int greater than or equal to 1. The dimension of the Brownian
        motion.
        Default value: 1 (i.e. a one dimensional brownian process).
      drift: The drift of the process. The type and shape of the value must be
        one of the following (in increasing order of generality) (a) A real
        scalar `Tensor`. This corresponds to a time and component independent
        drift. Every component of the Brownian motion has the same drift rate
        equal to this value. (b) A real `Tensor` of shape `[dim]`. This
        corresponds to a time independent drift with the `i`th component as the
        drift rate of the `i`th component of the Brownian motion. (c) A Python
        callable accepting a single positive `Tensor` of general shape (referred
        to as `times_shape`) and returning a `Tensor` of shape `times_shape +
        [dim]`. The input argument is the times at which the drift needs to be
        evaluated. This case corresponds to a general time and direction
        dependent drift rate.
        Default value: None which maps to zero drift.
      volatility: The volatility of the process. The type and shape of the
        supplied value must be one of the following (in increasing order of
        generality) (a) A positive real scalar `Tensor`. This corresponds to a
        time independent, diagonal volatility matrix. The `(i, j)` component of
        the full volatility matrix is equal to zero if `i != j` and equal to the
        supplied value otherwise. (b) A positive real `Tensor` of shape `[dim]`.
        This corresponds to a time independent volatility matrix with zero
        correlation. The `(i, j)` component of the full volatility matrix is
        equal to zero `i != j` and equal to the `i`th component of the supplied
        value otherwise. (c) A positive definite real `Tensor` of shape `[dim,
        dim]`. The full time independent volatility matrix. (d) A Python
        callable accepting a single positive `Tensor` of general shape (referred
        to as `times_shape`) and returning a `Tensor` of shape `times_shape +
        [dim, dim]`. The input argument are the times at which the volatility
        needs to be evaluated. This case corresponds to a general time and axis
        dependent volatility matrix.
        Default value: None which maps to a volatility matrix equal to identity.
      total_drift_fn: Optional Python callable to compute the integrated drift
        rate between two times. The callable should accept two real `Tensor`
        arguments. The first argument contains the start times and the second,
        the end times of the time intervals for which the total drift is to be
        computed. Both the `Tensor` arguments are of the same dtype and shape.
        The return value of the callable should be a real `Tensor` of the same
        dtype as the input arguments and of shape `times_shape + [dim]` where
        `times_shape` is the shape of the times `Tensor`. Note that it is an
        error to supply this parameter if the `drift` is not supplied.
        Default value: None.
      total_covariance_fn: A Python callable returning the integrated covariance
        rate between two times. The callable should accept two real `Tensor`
        arguments. The first argument is the start times and the second is the
        end times of the time intervals for which the total covariance is
        needed. Both the `Tensor` arguments are of the same dtype and shape. The
        return value of the callable is a real `Tensor` of the same dtype as the
        input arguments and of shape `times_shape + [dim, dim]` where
        `times_shape` is the shape of the times `Tensor`. Note that it is an
        error to suppy this argument if the `volatility` is not supplied.
        Default value: None.
      dtype: The default dtype to use when converting values to `Tensor`s.
        Default value: None which means that default dtypes inferred by
          TensorFlow are used.
      name: str. The name scope under which ops created by the methods of this
        class are nested.
        Default value: None which maps to the default name `brownian_motion`.

    Raises:
      ValueError if the dimension is less than 1 or if total drift is supplied
        but drift is not supplied or if the total covariance is supplied but
        but volatility is not supplied.
    """
        super(BrownianMotion, self).__init__()

        if dim < 1:
            raise ValueError('Dimension must be 1 or greater.')
        if drift is None and total_drift_fn is not None:
            raise ValueError('total_drift_fn must not be supplied if drift'
                             ' is not supplied.')
        if volatility is None and total_covariance_fn is not None:
            raise ValueError(
                'total_covariance_fn must not be supplied if drift'
                ' is not supplied.')
        self._dim = dim
        self._dtype = dtype
        self._name = name or 'brownian_motion'

        drift_fn, total_drift_fn = bmu.construct_drift_data(
            drift, total_drift_fn, dim, dtype)
        self._drift_fn = drift_fn
        self._total_drift_fn = total_drift_fn

        vol_fn, total_covar_fn = bmu.construct_vol_data(
            volatility, total_covariance_fn, dim, dtype)
        self._volatility_fn = vol_fn
        self._total_covariance_fn = total_covar_fn
 def test_construct_vol_no_covar_vol_callable(self):
     vol_fn = tf.sin
     _, total_cov = bm_utils.construct_vol_data(vol_fn, None, 1, tf.float32)
     self.assertIsNone(total_cov)