def test_construct_drift_callable(self): dtype = tf.float64 a, b = 0.1, -0.8 def test_drift_fn(t): return tf.expand_dims(t * a + b, axis=-1) def test_total_drift_fn(t1, t2): res = (t2**2 - t1**2) * a / 2 + (t2 - t1) * b return tf.expand_dims(res, axis=-1) drift_fn, total_drift_fn = bm_utils.construct_drift_data( test_drift_fn, test_total_drift_fn, 1, dtype) times = tf.constant([0.0, 1.0, 2.0], dtype=dtype) drift_vals = self.evaluate(drift_fn(times)) np.testing.assert_array_equal(drift_vals.shape, [3, 1]) np.testing.assert_allclose(drift_vals, [[-0.8], [-0.7], [-0.6]]) t1 = tf.constant([1.0, 2.0, 3.0], dtype=dtype) t2 = tf.constant([1.5, 3.0, 5.0], dtype=dtype) total_vals = self.evaluate(total_drift_fn(t1, t2)) np.testing.assert_array_equal(total_vals.shape, [3, 1]) np.testing.assert_allclose(total_vals, [[-0.3375], [-0.55], [-0.8]], atol=1e-7) # Tests that total drift is None if drift is a callable and no total_drift # is supplied _, total_drift = bm_utils.construct_drift_data(test_drift_fn, None, 1, dtype) self.assertIsNone(total_drift)
def test_construct_drift_constant(self): dtypes = [tf.float64, tf.float32] def make_total_drift_fn(v, dtype): def fn(t1, t2): return bm_utils.outer_multiply(t2 - t1, v * tf.ones([2], dtype=dtype)) return fn for dtype in dtypes: drift_const = tf.constant(2.0, dtype=dtype) drift_fn, total_drift_fn = bm_utils.construct_drift_data( drift_const, None, 2, dtype) times = tf.constant([0.3, 0.9, 1.5], dtype=dtype) drift_vals = self.evaluate(drift_fn(times)) np.testing.assert_array_equal(drift_vals.shape, [3, 2]) np.testing.assert_allclose(drift_vals, [[2.0, 2], [2, 2], [2, 2]]) total_vals = self.evaluate(total_drift_fn(times - 0.2, times)) np.testing.assert_array_equal(total_vals.shape, [3, 2]) np.testing.assert_allclose( total_vals, [[0.4, 0.4], [0.4, 0.4], [0.4, 0.4]], atol=1e-7) # Tests if both are supplied. Note we deliberately supply inconsistent # data to ensure that the method doesn't do anything clever. drift_fn_alt, total_drift_fn_alt = bm_utils.construct_drift_data( drift_const, make_total_drift_fn(4.0, dtype), 2, dtype) drift_vals = self.evaluate(drift_fn_alt(times)) np.testing.assert_array_equal(drift_vals.shape, [3, 2]) np.testing.assert_allclose(drift_vals, [[2.0, 2], [2, 2], [2, 2]]) total_vals_alt = self.evaluate(total_drift_fn_alt(times - 0.2, times)) np.testing.assert_array_equal(total_vals.shape, [3, 2]) np.testing.assert_allclose( total_vals_alt, [[0.8, 0.8], [0.8, 0.8], [0.8, 0.8]], atol=1e-5)
def test_construct_drift_default(self): dtypes = [tf.float64, tf.float32] for dtype in dtypes: drift_fn, total_drift_fn = bm_utils.construct_drift_data( None, None, 2, dtype) times = tf.constant([0.3, 0.9, 1.5], dtype=dtype) drift_vals = self.evaluate(drift_fn(times)) np.testing.assert_array_equal(drift_vals.shape, [3, 2]) np.testing.assert_allclose(drift_vals, [[0.0, 0], [0, 0], [0, 0]]) total_vals = self.evaluate(total_drift_fn(times - 0.2, times)) np.testing.assert_array_equal(total_vals.shape, [3, 2]) np.testing.assert_allclose(total_vals, [[0.0, 0], [0, 0], [0, 0]])
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