def _sample_paths(self, times, time_step, num_time_steps, num_samples, random_type, skip, seed): """Returns a sample of paths from the process.""" # Initial state should be broadcastable to batch_shape + [num_samples, dim] initial_state = tf.zeros(self._batch_shape + [1, self._dim], dtype=self._dtype) # Note that we need a finer simulation grid (determnied by `dt`) to compute # discount factors accurately. The `times` input might not be granular # enough for accurate calculations. time_step_internal = time_step if num_time_steps is not None: num_time_steps = tf.convert_to_tensor(num_time_steps, dtype=tf.int32, name='num_time_steps') time_step_internal = times[-1] / tf.cast(num_time_steps, dtype=self._dtype) times, _, time_indices = utils.prepare_grid( times=times, time_step=time_step_internal, dtype=self._dtype, num_time_steps=num_time_steps) # Add zeros as a starting location dt = times[1:] - times[:-1] # xy_paths.shape = (num_samples, num_times, nfactors+nfactors^2) xy_paths = euler_sampling.sample(self._dim, self._drift_fn, self._volatility_fn, times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, time_step=time_step, num_time_steps=num_time_steps, skip=skip) x_paths = xy_paths[..., :self._factors] y_paths = xy_paths[..., self._factors:] # shape=(batch_shape, num_times) f_0_t = self._instant_forward_rate_fn(times) # shape=(batch_shape, num_samples, num_times) rate_paths = tf.math.reduce_sum(x_paths, axis=-1) + tf.expand_dims( f_0_t, axis=-2) dt = tf.concat([tf.convert_to_tensor([0.0], dtype=self._dtype), dt], axis=0) discount_factor_paths = tf.math.exp( -utils.cumsum_using_matvec(rate_paths * dt)) return (tf.gather(rate_paths, time_indices, axis=-1), tf.gather(discount_factor_paths, time_indices, axis=-1), tf.gather(x_paths, time_indices, axis=self._batch_rank + 1), tf.gather(y_paths, time_indices, axis=self._batch_rank + 1))
def test_prepare_grid_time_step(self, dtype): times = tf.constant([0.1, 0.5, 1, 2], dtype=dtype) time_step = tf.constant(0.1, dtype=tf.float64) grid, _, time_indices = utils.prepare_grid( times=times, time_step=time_step, dtype=dtype) expected_grid = np.linspace(0, 2, 21, dtype=dtype) recovered_times = tf.gather(grid, time_indices) with self.subTest('Grid'): self.assertAllClose(grid, expected_grid, rtol=1e-6, atol=1e-6) with self.subTest('Times'): self.assertAllClose(times, recovered_times, rtol=1e-6, atol=1e-6)
def test_prepare_grid_num_time_step(self, dtype): num_points = 100 times = tf.linspace(tf.constant(0.02, dtype=dtype), 1.0, 50) time_step = times[-1] / num_points grid, _, time_indices = utils.prepare_grid( times=times, time_step=time_step, dtype=dtype, num_time_steps=num_points) expected_grid = np.linspace(0, 1, 101, dtype=dtype) expected_indices = np.array([2 * i for i in range(1, 51)]) with self.subTest('Grid'): self.assertAllClose(grid, expected_grid, rtol=1e-6, atol=1e-6) with self.subTest('TimeIndex'): self.assertAllClose(time_indices, expected_indices, rtol=1e-6, atol=1e-6)
def _sample_paths(self, times, time_step, num_samples, random_type, skip, seed): """Returns a sample of paths from the process.""" initial_state = tf.zeros((self._dim, ), dtype=self._dtype) # Note that we need a finer simulation grid (determnied by `dt`) to compute # discount factors accurately. The `times` input might not be granular # enough for accurate calculations. times, keep_mask, _ = utils.prepare_grid(times=times, time_step=time_step, dtype=self._dtype) # Add zeros as a starting location dt = times[1:] - times[:-1] # xy_paths.shape = (num_samples, num_times, nfactors+nfactors^2) xy_paths = euler_sampling.sample(self._dim, self._drift_fn, self._volatility_fn, times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, time_step=time_step, skip=skip) x_paths = xy_paths[..., :self._factors] y_paths = xy_paths[..., self._factors:] f_0_t = self._instant_forward_rate_fn(times) # shape=(num_times,) rate_paths = tf.math.reduce_sum( x_paths, axis=-1) + f_0_t # shape=(num_samples, num_times) discount_factor_paths = tf.math.exp(-rate_paths[:, :-1] * dt) discount_factor_paths = tf.concat( [ tf.ones( (num_samples, 1), dtype=self._dtype), discount_factor_paths ], axis=1) # shape=(num_samples, num_times) discount_factor_paths = utils.cumprod_using_matvec( discount_factor_paths) return (tf.boolean_mask(rate_paths, keep_mask, axis=1), tf.boolean_mask(discount_factor_paths, keep_mask, axis=1), tf.boolean_mask(x_paths, keep_mask, axis=1), tf.boolean_mask(y_paths, keep_mask, axis=1))
def _sample_paths(self, times, time_step, num_samples, random_type, skip, seed): """Returns a sample of paths from the process.""" initial_state = tf.zeros((self._dim,), dtype=self._dtype) # Note that we need a finer simulation grid (determnied by `dt`) to compute # discount factors accurately. The `times` input might not be granular # enough for accurate calculations. times, _, time_indices = utils.prepare_grid( times=times, time_step=time_step, dtype=self._dtype) # Add zeros as a starting location dt = times[1:] - times[:-1] # Shape = (num_samples, num_times, nfactors) paths = euler_sampling.sample( self._dim, self._drift_fn, self._volatility_fn, times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, time_step=time_step, skip=skip) y_paths = self.state_y(times) # shape=(dim, dim, num_times) y_paths = tf.reshape( y_paths, tf.concat([[self._dim**2], tf.shape(times)], axis=0)) # shape=(num_samples, num_times, dim**2) y_paths = tf.repeat(tf.expand_dims(tf.transpose( y_paths), axis=0), num_samples, axis=0) f_0_t = self._instant_forward_rate_fn(times) # shape=(num_times,) rate_paths = tf.math.reduce_sum( paths, axis=-1) + f_0_t # shape=(num_samples, num_times) discount_factor_paths = tf.math.exp(-rate_paths[:, :-1] * dt) discount_factor_paths = tf.concat( [tf.ones((num_samples, 1), dtype=self._dtype), discount_factor_paths], axis=1) # shape=(num_samples, num_times) discount_factor_paths = utils.cumprod_using_matvec(discount_factor_paths) return (tf.gather(rate_paths, time_indices, axis=1), tf.gather(discount_factor_paths, time_indices, axis=1), tf.gather(paths, time_indices, axis=1), tf.gather(y_paths, time_indices, axis=1))
def _create_pde_time_grid(exercise_times, time_step_fd, num_time_steps_fd, dtype): """Create PDE time grid.""" with tf.name_scope('create_pde_time_grid'): exercise_times, _ = tf.unique(tf.reshape(exercise_times, shape=[-1])) if num_time_steps_fd is not None: num_time_steps_fd = tf.convert_to_tensor( num_time_steps_fd, dtype=tf.int32, name='num_time_steps_fd') time_step_fd = tf.math.reduce_max(exercise_times) / tf.cast( num_time_steps_fd, dtype=dtype) if time_step_fd is None and num_time_steps_fd is None: num_time_steps_fd = 100 pde_time_grid, _, _ = utils.prepare_grid( times=exercise_times, time_step=time_step_fd, dtype=dtype, num_time_steps=num_time_steps_fd) pde_time_grid_dt = pde_time_grid[1:] - pde_time_grid[:-1] pde_time_grid_dt = tf.concat([[100.0], pde_time_grid_dt], axis=-1) return pde_time_grid, pde_time_grid_dt
def sample(dim, drift_fn, volatility_fn, times, time_step, num_samples=1, initial_state=None, random_type=None, seed=None, swap_memory=True, skip=0, precompute_normal_draws=True, watch_params=None, dtype=None, name=None): """Returns a sample paths from the process using Euler method. For an Ito process, ``` dX = a(t, X_t) dt + b(t, X_t) dW_t ``` with given drift `a` and volatility `b` functions Euler method generates a sequence {X_n} as ``` X_{n+1} = X_n + a(t_n, X_n) dt + b(t_n, X_n) (N(0, t_{n+1}) - N(0, t_n)), ``` where `dt = t_{n+1} - t_n` and `N` is a sample from the Normal distribution. See [1] for details. #### References [1]: Wikipedia. Euler-Maruyama method: https://en.wikipedia.org/wiki/Euler-Maruyama_method Args: dim: Python int greater than or equal to 1. The dimension of the Ito Process. drift_fn: A Python callable to compute the drift of the process. The callable should accept two real `Tensor` arguments of the same dtype. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [dim]`. The result is value of drift a(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [dim]`. volatility_fn: A Python callable to compute the volatility of the process. The callable should accept two real `Tensor` arguments of the same dtype and shape `times_shape`. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [dim]`. The result is value of drift b(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [dim, dim]`. times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: Scalar real `Tensor` - maximal distance between points in grid in Euler schema. num_samples: Positive scalar `int`. The number of paths to draw. Default value: 1. initial_state: `Tensor` of shape `[dim]`. The initial state of the process. Default value: None which maps to a zero initial state. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the paths. Default value: None which maps to the standard pseudo-random numbers. seed: Seed for the random number generator. The seed is only relevant if `random_type` is one of `[STATELESS, PSEUDO, HALTON_RANDOMIZED, PSEUDO_ANTITHETIC, STATELESS_ANTITHETIC]`. For `PSEUDO`, `PSEUDO_ANTITHETIC` and `HALTON_RANDOMIZED` the seed should be a Python integer. For `STATELESS` and `STATELESS_ANTITHETIC `must be supplied as an integer `Tensor` of shape `[2]`. Default value: `None` which means no seed is set. swap_memory: A Python bool. Whether GPU-CPU memory swap is enabled for this op. See an equivalent flag in `tf.while_loop` documentation for more details. Useful when computing a gradient of the op since `tf.while_loop` is used to propagate stochastic process in time. Default value: True. skip: `int32` 0-d `Tensor`. The number of initial points of the Sobol or Halton sequence to skip. Used only when `random_type` is 'SOBOL', 'HALTON', or 'HALTON_RANDOMIZED', otherwise ignored. Default value: `0`. precompute_normal_draws: Python bool. Indicates whether the noise increments `N(0, t_{n+1}) - N(0, t_n)` are precomputed. For `HALTON` and `SOBOL` random types the increments are always precomputed. While the resulting graph consumes more memory, the performance gains might be significant. Default value: `True`. watch_params: An optional list of zero-dimensional `Tensor`s of the same `dtype` as `initial_state`. If provided, specifies `Tensor`s with respect to which the differentiation of the sampling function will happen. A more efficient algorithm is used when `watch_params` are specified. Note the the function becomes differentiable onlhy wrt to these `Tensor`s and the `initial_state`. The gradient wrt any other `Tensor` is set to be zero. dtype: `tf.Dtype`. If supplied the dtype for the input and output `Tensor`s. Default value: None which means that the dtype implied by `times` is used. name: Python string. The name to give this op. Default value: `None` which maps to `euler_sample`. Returns: A real `Tensor` of shape [num_samples, k, n] where `k` is the size of the `times`, `n` is the dimension of the process. Raises: ValueError: If `time_step` or `times` have a non-constant value (e.g., values are random), and `random_type` is `SOBOL`. This will be fixed with the release of TensorFlow 2.2. """ name = name or 'euler_sample' with tf.name_scope(name): times = tf.convert_to_tensor(times, dtype=dtype) if dtype is None: dtype = times.dtype if initial_state is None: initial_state = tf.zeros(dim, dtype=dtype) initial_state = tf.convert_to_tensor(initial_state, dtype=dtype, name='initial_state') num_requested_times = tf.shape(times)[0] # Create a time grid for the Euler scheme. times, keep_mask, time_indices = utils.prepare_grid( times=times, time_step=time_step, dtype=dtype) if watch_params is not None: watch_params = [ tf.convert_to_tensor(param, dtype=dtype) for param in watch_params ] return _sample(dim=dim, drift_fn=drift_fn, volatility_fn=volatility_fn, times=times, time_step=time_step, keep_mask=keep_mask, num_requested_times=num_requested_times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, swap_memory=swap_memory, skip=skip, precompute_normal_draws=precompute_normal_draws, watch_params=watch_params, time_indices=time_indices, dtype=dtype)
def sample(dim, drift_fn, volatility_fn, times, time_step=None, num_time_steps=None, num_samples=1, initial_state=None, random_type=None, seed=None, swap_memory=True, skip=0, precompute_normal_draws=True, watch_params=None, dtype=None, name=None): r"""Returns a sample paths from the process using the Milstein method. For an Ito process, ``` dX = a(t, X_t) dt + b(t, X_t) dW_t ``` given drift `a`, volatility `b` and derivative of volatility `b'`, the Milstein method generates a sequence {Y_n} approximating X ``` Y_{n+1} = Y_n + a(t_n, Y_n) dt + b(t_n, Y_n) dW_n + \frac{1}{2} b(t_n, Y_n) b'(t_n, Y_n) ((dW_n)^2 - dt) ``` where `dt = t_{n+1} - t_n`, `dW_n = (N(0, t_{n+1}) - N(0, t_n))` and `N` is a sample from the Normal distribution. In higher dimensions, when `a(t, X_t)` is a d-dimensional vector valued function and `W_t` is a d-dimensional Wiener process, we have for the kth element of the expansion: ``` Y_{n+1}[k] = Y_n[k] + a(t_n, Y_n)[k] dt + \sum_{j=1}^d b(t_n, Y_n)[k, j] dW_n[j] + \sum_{j_1=1}^d \sum_{j_2=1}^d L_{j_1} b(t_n, Y_n)[k, j_2] I(j_1, j_2) ``` where `L_{j} = \sum_{i=1}^d b(t_n, Y_n)[i, j] \frac{\partial}{\partial x^i}` is an operator and `I(j_1, j_2) = \int_{t_n}^{t_{n+1}} \int_{t_n}^{s_1} dW_{s_2}[j_1] dW_{s_1}[j_2]` is a multiple Ito integral. See [1] and [2] for details. Currently, only the one dimensional scheme has been implemented. #### References [1]: Wikipedia. Milstein method: https://en.wikipedia.org/wiki/Milstein_method [2]: Peter E. Kloeden, Eckhard Platen. Numerical Solution of Stochastic Differential Equations. Springer. 1992 Args: dim: Python int greater than or equal to 1. The dimension of the Ito Process. drift_fn: A Python callable to compute the drift of the process. The callable should accept two real `Tensor` arguments of the same dtype. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [dim]`. The result is value of drift a(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [dim]`. volatility_fn: A Python callable to compute the volatility of the process. The callable should accept two real `Tensor` arguments of the same dtype and shape `times_shape`. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [dim]`. The result is value of drift b(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [dim, dim]`. times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: An optional scalar real `Tensor` - maximal distance between points in grid in Milstein schema. Either this or `num_time_steps` should be supplied. Default value: `None`. num_time_steps: An optional Scalar integer `Tensor` - a total number of time steps performed by the algorithm. The maximal distance betwen points in grid is bounded by `times[-1] / (num_time_steps - times.shape[0])`. Either this or `time_step` should be supplied. Default value: `None`. num_samples: Positive scalar `int`. The number of paths to draw. Default value: 1. initial_state: `Tensor` of shape `[dim]`. The initial state of the process. Default value: None which maps to a zero initial state. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the paths. Default value: None which maps to the standard pseudo-random numbers. seed: Seed for the random number generator. The seed is only relevant if `random_type` is one of `[STATELESS, PSEUDO, HALTON_RANDOMIZED, PSEUDO_ANTITHETIC, STATELESS_ANTITHETIC]`. For `PSEUDO`, `PSEUDO_ANTITHETIC` and `HALTON_RANDOMIZED` the seed should be a Python integer. For `STATELESS` and `STATELESS_ANTITHETIC `must be supplied as an integer `Tensor` of shape `[2]`. Default value: `None` which means no seed is set. swap_memory: A Python bool. Whether GPU-CPU memory swap is enabled for this op. See an equivalent flag in `tf.while_loop` documentation for more details. Useful when computing a gradient of the op since `tf.while_loop` is used to propagate stochastic process in time. Default value: True. skip: `int32` 0-d `Tensor`. The number of initial points of the Sobol or Halton sequence to skip. Used only when `random_type` is 'SOBOL', 'HALTON', or 'HALTON_RANDOMIZED', otherwise ignored. Default value: `0`. precompute_normal_draws: Python bool. Indicates whether the noise increments `N(0, t_{n+1}) - N(0, t_n)` are precomputed. For `HALTON` and `SOBOL` random types the increments are always precomputed. While the resulting graph consumes more memory, the performance gains might be significant. Default value: `True`. watch_params: An optional list of zero-dimensional `Tensor`s of the same `dtype` as `initial_state`. If provided, specifies `Tensor`s with respect to which the differentiation of the sampling function will happen. A more efficient algorithm is used when `watch_params` are specified. Note the the function becomes differentiable onlhy wrt to these `Tensor`s and the `initial_state`. The gradient wrt any other `Tensor` is set to be zero. dtype: `tf.Dtype`. If supplied the dtype for the input and output `Tensor`s. Default value: None which means that the dtype implied by `times` is used. name: Python string. The name to give this op. Default value: `None` which maps to `milstein_sample`. """ name = name or 'milstein_sample' if dim != 1: raise NotImplementedError( 'Multi-dimensional Milstein is not supported yet.') with tf.name_scope(name): times = tf.convert_to_tensor(times, dtype=dtype) if dtype is None: dtype = times.dtype if initial_state is None: initial_state = tf.zeros(dim, dtype=dtype) initial_state = tf.convert_to_tensor( initial_state, dtype=dtype, name='initial_state') num_requested_times = tf.shape(times)[0] # Create a time grid for the Milstein scheme. if num_time_steps is not None and time_step is not None: raise ValueError('Only one of either `num_time_steps` or `time_step` ' 'should be defined but not both') if time_step is None: if num_time_steps is None: raise ValueError('Either `num_time_steps` or `time_step` should be ' 'defined.') num_time_steps = tf.convert_to_tensor( num_time_steps, dtype=tf.int32, name='num_time_steps') time_step = times[-1] / tf.cast(num_time_steps, dtype=dtype) else: time_step = tf.convert_to_tensor(time_step, dtype=dtype, name='time_step') times, keep_mask, time_indices = utils.prepare_grid( times=times, time_step=time_step, num_time_steps=num_time_steps, dtype=dtype) if watch_params is not None: watch_params = [ tf.convert_to_tensor(param, dtype=dtype) for param in watch_params ] def _grad_volatility_fn(current_time, current_state): return gradient.fwd_gradient( functools.partial(volatility_fn, current_time), current_state, unconnected_gradients=tf.UnconnectedGradients.ZERO) return _sample( dim=dim, drift_fn=drift_fn, volatility_fn=volatility_fn, grad_volatility_fn=_grad_volatility_fn, times=times, time_step=time_step, keep_mask=keep_mask, num_requested_times=num_requested_times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, swap_memory=swap_memory, skip=skip, precompute_normal_draws=precompute_normal_draws, watch_params=watch_params, time_indices=time_indices, dtype=dtype)
def sample(dim: int, drift_fn: Callable[..., types.RealTensor], volatility_fn: Callable[..., types.RealTensor], times: types.RealTensor, time_step: Optional[types.RealTensor] = None, num_time_steps: Optional[types.IntTensor] = None, num_samples: types.IntTensor = 1, initial_state: Optional[types.RealTensor] = None, random_type: Optional[random.RandomType] = None, seed: Optional[types.IntTensor] = None, swap_memory: bool = True, skip: types.IntTensor = 0, precompute_normal_draws: bool = True, times_grid: Optional[types.RealTensor] = None, normal_draws: Optional[types.RealTensor] = None, watch_params: Optional[List[types.RealTensor]] = None, validate_args: bool = False, tolerance: Optional[types.RealTensor] = None, dtype: Optional[tf.DType] = None, name: Optional[str] = None) -> types.RealTensor: """Returns a sample paths from the process using Euler method. For an Ito process, ``` dX = a(t, X_t) dt + b(t, X_t) dW_t X(t=0) = x0 ``` with given drift `a` and volatility `b` functions Euler method generates a sequence {X_n} as ``` X_{n+1} = X_n + a(t_n, X_n) dt + b(t_n, X_n) (N(0, t_{n+1}) - N(0, t_n)), X_0 = x0 ``` where `dt = t_{n+1} - t_n` and `N` is a sample from the Normal distribution. See [1] for details. #### Example Sampling from 2-dimensional Ito process of the form: ```none dX_1 = mu_1 * sqrt(t) dt + s11 * dW_1 + s12 * dW_2 dX_2 = mu_2 * sqrt(t) dt + s21 * dW_1 + s22 * dW_2 ``` ```python import tensorflow as tf import tf_quant_finance as tff import numpy as np mu = np.array([0.2, 0.7]) s = np.array([[0.3, 0.1], [0.1, 0.3]]) num_samples = 10000 dim = 2 dtype = tf.float64 # Define drift and volatility functions def drift_fn(t, x): return mu * tf.sqrt(t) * tf.ones([num_samples, dim], dtype=dtype) def vol_fn(t, x): return s * tf.ones([num_samples, dim, dim], dtype=dtype) # Set starting location x0 = np.array([0.1, -1.1]) # Sample `num_samples` paths at specified `times` using Euler scheme. times = [0.1, 1.0, 2.0] paths = tff.models.euler_sampling.sample( dim=dim, drift_fn=drift_fn, volatility_fn=vol_fn, times=times, num_samples=num_samples, initial_state=x0, time_step=0.01, seed=42, dtype=dtype) # Expected: paths.shape = [10000, 3, 2] ``` #### References [1]: Wikipedia. Euler-Maruyama method: https://en.wikipedia.org/wiki/Euler-Maruyama_method Args: dim: Python int greater than or equal to 1. The dimension of the Ito Process. drift_fn: A Python callable to compute the drift of the process. The callable should accept two real `Tensor` arguments of the same dtype. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [num_samples, dim]`. `batch_shape` is the shape of the independent stochastic processes being modelled and is inferred from the initial state `x0`. The result is value of drift a(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [num_samples, dim]`. volatility_fn: A Python callable to compute the volatility of the process. The callable should accept two real `Tensor` arguments of the same dtype and shape `times_shape`. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [num_samples, dim]`. The result is value of drift b(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [num_samples, dim, dim]`. times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: An optional scalar real `Tensor` - maximal distance between points in grid in Euler schema. Either this or `num_time_steps` should be supplied. Default value: `None`. num_time_steps: An optional Scalar integer `Tensor` - a total number of time steps performed by the algorithm. The maximal distance betwen points in grid is bounded by `times[-1] / (num_time_steps - times.shape[0])`. Either this or `time_step` should be supplied. Default value: `None`. num_samples: Positive scalar `int`. The number of paths to draw. Default value: 1. initial_state: `Tensor` of shape broadcastable with `batch_shape + [num_samples, dim]`. The initial state of the process. `batch_shape` represents the shape of the independent batches of the stochastic process. Note that `batch_shape` is inferred from the `initial_state` and hence when sampling is requested for a batch of stochastic processes, the shape of `initial_state` should be at least `batch_shape + [1, 1]`. Default value: None which maps to a zero initial state. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the paths. Default value: None which maps to the standard pseudo-random numbers. seed: Seed for the random number generator. The seed is only relevant if `random_type` is one of `[STATELESS, PSEUDO, HALTON_RANDOMIZED, PSEUDO_ANTITHETIC, STATELESS_ANTITHETIC]`. For `PSEUDO`, `PSEUDO_ANTITHETIC` and `HALTON_RANDOMIZED` the seed should be a Python integer. For `STATELESS` and `STATELESS_ANTITHETIC `must be supplied as an integer `Tensor` of shape `[2]`. Default value: `None` which means no seed is set. swap_memory: A Python bool. Whether GPU-CPU memory swap is enabled for this op. See an equivalent flag in `tf.while_loop` documentation for more details. Useful when computing a gradient of the op since `tf.while_loop` is used to propagate stochastic process in time. Default value: True. skip: `int32` 0-d `Tensor`. The number of initial points of the Sobol or Halton sequence to skip. Used only when `random_type` is 'SOBOL', 'HALTON', or 'HALTON_RANDOMIZED', otherwise ignored. Default value: `0`. precompute_normal_draws: Python bool. Indicates whether the noise increments `N(0, t_{n+1}) - N(0, t_n)` are precomputed. For `HALTON` and `SOBOL` random types the increments are always precomputed. While the resulting graph consumes more memory, the performance gains might be significant. Default value: `True`. times_grid: An optional rank 1 `Tensor` representing time discretization grid. If `times` are not on the grid, then the nearest points from the grid are used. When supplied, `num_time_steps` and `time_step` are ignored. Default value: `None`, which means that times grid is computed using `time_step` and `num_time_steps`. normal_draws: A `Tensor` of shape broadcastable with `batch_shape + [num_samples, num_time_points, dim]` and the same `dtype` as `times`. Represents random normal draws to compute increments `N(0, t_{n+1}) - N(0, t_n)`. When supplied, `num_samples` argument is ignored and the first dimensions of `normal_draws` is used instead. Default value: `None` which means that the draws are generated by the algorithm. By default normal_draws for each model in the batch are independent. watch_params: An optional list of zero-dimensional `Tensor`s of the same `dtype` as `initial_state`. If provided, specifies `Tensor`s with respect to which the differentiation of the sampling function will happen. A more efficient algorithm is used when `watch_params` are specified. Note the the function becomes differentiable onlhy wrt to these `Tensor`s and the `initial_state`. The gradient wrt any other `Tensor` is set to be zero. validate_args: Python `bool`. When `True` performs multiple checks: * That `times` are increasing with the minimum increments of the specified tolerance. * If `normal_draws` are supplied, checks that `normal_draws.shape[1]` is equal to `num_time_steps` that is either supplied as an argument or computed from `time_step`. When `False` invalid dimension may silently render incorrect outputs. Default value: `False`. tolerance: A non-negative scalar `Tensor` specifying the minimum tolerance for discernible times on the time grid. Times that are closer than the tolerance are perceived to be the same. Default value: `None` which maps to `1-e6` if the for single precision `dtype` and `1e-10` for double precision `dtype`. dtype: `tf.Dtype`. If supplied the dtype for the input and output `Tensor`s. Default value: None which means that the dtype implied by `times` is used. name: Python string. The name to give this op. Default value: `None` which maps to `euler_sample`. Returns: A real `Tensor` of shape batch_shape_process + [num_samples, k, n] where `k` is the size of the `times`, `n` is the dimension of the process. Raises: ValueError: (a) When `times_grid` is not supplied, and neither `num_time_steps` nor `time_step` are supplied or if both are supplied. (b) If `normal_draws` is supplied and `dim` is mismatched. tf.errors.InvalidArgumentError: If `normal_draws` is supplied and `num_time_steps` is mismatched. """ name = name or 'euler_sample' with tf.name_scope(name): times = tf.convert_to_tensor(times, dtype=dtype) if dtype is None: dtype = times.dtype asserts = [] if tolerance is None: tolerance = 1e-10 if dtype == tf.float64 else 1e-6 tolerance = tf.convert_to_tensor(tolerance, dtype=dtype) if validate_args: asserts.append( tf.assert_greater( times[1:], times[:-1] + tolerance, message='`times` increments should be greater ' 'than tolerance {0}'.format(tolerance))) if initial_state is None: initial_state = tf.zeros(dim, dtype=dtype) initial_state = tf.convert_to_tensor(initial_state, dtype=dtype, name='initial_state') batch_shape = tff_utils.get_shape(initial_state)[:-2] num_requested_times = tff_utils.get_shape(times)[0] # Create a time grid for the Euler scheme. if num_time_steps is not None and time_step is not None: raise ValueError( 'When `times_grid` is not supplied only one of either ' '`num_time_steps` or `time_step` should be defined but not both.' ) if times_grid is None: if time_step is None: if num_time_steps is None: raise ValueError( 'When `times_grid` is not supplied, either `num_time_steps` ' 'or `time_step` should be defined.') num_time_steps = tf.convert_to_tensor(num_time_steps, dtype=tf.int32, name='num_time_steps') time_step = times[-1] / tf.cast(num_time_steps, dtype=dtype) else: time_step = tf.convert_to_tensor(time_step, dtype=dtype, name='time_step') else: times_grid = tf.convert_to_tensor(times_grid, dtype=dtype, name='times_grid') if validate_args: asserts.append( tf.assert_greater( times_grid[1:], times_grid[:-1] + tolerance, message='`times_grid` increments should be greater ' 'than tolerance {0}'.format(tolerance))) times, keep_mask, time_indices = utils.prepare_grid( times=times, time_step=time_step, num_time_steps=num_time_steps, times_grid=times_grid, tolerance=tolerance, dtype=dtype) if normal_draws is not None: normal_draws = tf.convert_to_tensor(normal_draws, dtype=dtype, name='normal_draws') # Shape [num_time_points] + batch_shape + [num_samples, dim] normal_draws_rank = normal_draws.shape.rank perm = tf.concat( [[normal_draws_rank - 2], tf.range(normal_draws_rank - 2), [normal_draws_rank - 1]], axis=0) normal_draws = tf.transpose(normal_draws, perm=perm) num_samples = tf.shape(normal_draws)[-2] draws_dim = normal_draws.shape[-1] if dim != draws_dim: raise ValueError( '`dim` should be equal to `normal_draws.shape[2]` but are ' '{0} and {1} respectively'.format(dim, draws_dim)) if validate_args: draws_times = tff_utils.get_shape(normal_draws)[0] asserts.append( tf.assert_equal( draws_times, tf.shape(keep_mask)[0] - 1, message='`num_time_steps` should be equal to ' '`tf.shape(normal_draws)[1]`')) if validate_args: with tf.control_dependencies(asserts): times = tf.identity(times) if watch_params is not None: watch_params = [ tf.convert_to_tensor(param, dtype=dtype) for param in watch_params ] return _sample(dim=dim, batch_shape=batch_shape, drift_fn=drift_fn, volatility_fn=volatility_fn, times=times, keep_mask=keep_mask, num_requested_times=num_requested_times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, swap_memory=swap_memory, skip=skip, precompute_normal_draws=precompute_normal_draws, normal_draws=normal_draws, watch_params=watch_params, time_indices=time_indices, dtype=dtype)
def sample(dim, drift_fn, volatility_fn, times, time_step=None, num_time_steps=None, num_samples=1, initial_state=None, random_type=None, seed=None, swap_memory=True, skip=0, precompute_normal_draws=True, times_grid=None, normal_draws=None, watch_params=None, validate_args=False, dtype=None, name=None): """Returns a sample paths from the process using Euler method. For an Ito process, ``` dX = a(t, X_t) dt + b(t, X_t) dW_t ``` with given drift `a` and volatility `b` functions Euler method generates a sequence {X_n} as ``` X_{n+1} = X_n + a(t_n, X_n) dt + b(t_n, X_n) (N(0, t_{n+1}) - N(0, t_n)), ``` where `dt = t_{n+1} - t_n` and `N` is a sample from the Normal distribution. See [1] for details. #### References [1]: Wikipedia. Euler-Maruyama method: https://en.wikipedia.org/wiki/Euler-Maruyama_method Args: dim: Python int greater than or equal to 1. The dimension of the Ito Process. drift_fn: A Python callable to compute the drift of the process. The callable should accept two real `Tensor` arguments of the same dtype. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [dim]`. The result is value of drift a(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [dim]`. volatility_fn: A Python callable to compute the volatility of the process. The callable should accept two real `Tensor` arguments of the same dtype and shape `times_shape`. The first argument is the scalar time t, the second argument is the value of Ito process X - tensor of shape `batch_shape + [dim]`. The result is value of drift b(t, X). The return value of the callable is a real `Tensor` of the same dtype as the input arguments and of shape `batch_shape + [dim, dim]`. times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: An optional scalar real `Tensor` - maximal distance between points in grid in Euler schema. Either this or `num_time_steps` should be supplied. Default value: `None`. num_time_steps: An optional Scalar integer `Tensor` - a total number of time steps performed by the algorithm. The maximal distance betwen points in grid is bounded by `times[-1] / (num_time_steps - times.shape[0])`. Either this or `time_step` should be supplied. Default value: `None`. num_samples: Positive scalar `int`. The number of paths to draw. Default value: 1. initial_state: `Tensor` of shape `[dim]`. The initial state of the process. Default value: None which maps to a zero initial state. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the paths. Default value: None which maps to the standard pseudo-random numbers. seed: Seed for the random number generator. The seed is only relevant if `random_type` is one of `[STATELESS, PSEUDO, HALTON_RANDOMIZED, PSEUDO_ANTITHETIC, STATELESS_ANTITHETIC]`. For `PSEUDO`, `PSEUDO_ANTITHETIC` and `HALTON_RANDOMIZED` the seed should be a Python integer. For `STATELESS` and `STATELESS_ANTITHETIC `must be supplied as an integer `Tensor` of shape `[2]`. Default value: `None` which means no seed is set. swap_memory: A Python bool. Whether GPU-CPU memory swap is enabled for this op. See an equivalent flag in `tf.while_loop` documentation for more details. Useful when computing a gradient of the op since `tf.while_loop` is used to propagate stochastic process in time. Default value: True. skip: `int32` 0-d `Tensor`. The number of initial points of the Sobol or Halton sequence to skip. Used only when `random_type` is 'SOBOL', 'HALTON', or 'HALTON_RANDOMIZED', otherwise ignored. Default value: `0`. precompute_normal_draws: Python bool. Indicates whether the noise increments `N(0, t_{n+1}) - N(0, t_n)` are precomputed. For `HALTON` and `SOBOL` random types the increments are always precomputed. While the resulting graph consumes more memory, the performance gains might be significant. Default value: `True`. times_grid: An optional rank 1 `Tensor` representing time discretization grid. If `times` are not on the grid, then the nearest points from the grid are used. When supplied, `num_time_steps` and `time_step` are ignored. Default value: `None`, which means that times grid is computed using `time_step` and `num_time_steps`. normal_draws: A `Tensor` of shape `[num_samples, num_time_points, dim]` and the same `dtype` as `times`. Represents random normal draws to compute increments `N(0, t_{n+1}) - N(0, t_n)`. When supplied, `num_samples` argument is ignored and the first dimensions of `normal_draws` is used instead. Default value: `None` which means that the draws are generated by the algorithm. watch_params: An optional list of zero-dimensional `Tensor`s of the same `dtype` as `initial_state`. If provided, specifies `Tensor`s with respect to which the differentiation of the sampling function will happen. A more efficient algorithm is used when `watch_params` are specified. Note the the function becomes differentiable onlhy wrt to these `Tensor`s and the `initial_state`. The gradient wrt any other `Tensor` is set to be zero. validate_args: Python `bool`. When `True` and `normal_draws` are supplied, checks that `tf.shape(normal_draws)[1]` is equal to `num_time_steps` that is either supplied as an argument or computed from `time_step`. When `False` invalid dimension may silently render incorrect outputs. Default value: `False`. dtype: `tf.Dtype`. If supplied the dtype for the input and output `Tensor`s. Default value: None which means that the dtype implied by `times` is used. name: Python string. The name to give this op. Default value: `None` which maps to `euler_sample`. Returns: A real `Tensor` of shape [num_samples, k, n] where `k` is the size of the `times`, `n` is the dimension of the process. Raises: ValueError: (a) When `times_grid` is not supplied, and neither `num_time_steps` nor `time_step` are supplied or if both are supplied. (b) If `normal_draws` is supplied and `dim` is mismatched. tf.errors.InvalidArgumentError: If `normal_draws` is supplied and `num_time_steps` is mismatched. """ name = name or 'euler_sample' with tf.name_scope(name): times = tf.convert_to_tensor(times, dtype=dtype) if dtype is None: dtype = times.dtype if initial_state is None: initial_state = tf.zeros(dim, dtype=dtype) initial_state = tf.convert_to_tensor(initial_state, dtype=dtype, name='initial_state') num_requested_times = tf.shape(times)[0] # Create a time grid for the Euler scheme. if num_time_steps is not None and time_step is not None: raise ValueError( 'When `times_grid` is not supplied only one of either ' '`num_time_steps` or `time_step` should be defined but not both.') if times_grid is None: if time_step is None: if num_time_steps is None: raise ValueError( 'When `times_grid` is not supplied, either `num_time_steps` ' 'or `time_step` should be defined.') num_time_steps = tf.convert_to_tensor( num_time_steps, dtype=tf.int32, name='num_time_steps') time_step = times[-1] / tf.cast(num_time_steps, dtype=dtype) else: time_step = tf.convert_to_tensor(time_step, dtype=dtype, name='time_step') else: times_grid = tf.convert_to_tensor(times_grid, dtype=dtype, name='times_grid') times, keep_mask, time_indices = utils.prepare_grid( times=times, time_step=time_step, num_time_steps=num_time_steps, times_grid=times_grid, dtype=dtype) if normal_draws is not None: normal_draws = tf.convert_to_tensor(normal_draws, dtype=dtype, name='normal_draws') # Shape [num_time_points, num_samples, dim] normal_draws = tf.transpose(normal_draws, [1, 0, 2]) num_samples = tf.shape(normal_draws)[1] draws_dim = normal_draws.shape[2] if dim != draws_dim: raise ValueError( '`dim` should be equal to `normal_draws.shape[2]` but are ' '{0} and {1} respectively'.format(dim, draws_dim)) if validate_args: draws_times = tf.shape(normal_draws)[0] asserts = tf.assert_equal( draws_times, tf.shape(keep_mask)[0] - 1, message='`num_time_steps` should be equal to ' '`tf.shape(normal_draws)[1]`') with tf.compat.v1.control_dependencies([asserts]): normal_draws = tf.identity(normal_draws) if watch_params is not None: watch_params = [tf.convert_to_tensor(param, dtype=dtype) for param in watch_params] return _sample( dim=dim, drift_fn=drift_fn, volatility_fn=volatility_fn, times=times, keep_mask=keep_mask, num_requested_times=num_requested_times, num_samples=num_samples, initial_state=initial_state, random_type=random_type, seed=seed, swap_memory=swap_memory, skip=skip, precompute_normal_draws=precompute_normal_draws, normal_draws=normal_draws, watch_params=watch_params, time_indices=time_indices, dtype=dtype)
def _prepare_grid(times, time_step, dtype, *params, num_time_steps=None, times_grid=None): """Prepares grid of times for path generation. Args: times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: Rank 0 real `Tensor`. Maximal distance between points in resulting grid. dtype: `tf.Dtype` of the input and output `Tensor`s. *params: Parameters of the Heston model. Either scalar `Tensor`s of the same `dtype` or instances of `PiecewiseConstantFunc`. num_time_steps: Number of points on the grid. If suppied, a uniform grid is constructed for `[time_step, times[-1] - time_step]` consisting of max(0, num_time_steps - len(times)) points that is then concatenated with times. This parameter guarantees the number of points on the time grid is `max(len(times), num_time_steps)` and that `times` are included to the grid. Default value: `None`, which means that a uniform grid is created. containing all points from 'times` and the uniform grid of points between `[0, times[-1]]` with grid size equal to `time_step`. times_grid: An optional rank 1 `Tensor` representing time discretization grid. If `times` are not on the grid, then the nearest points from the grid are used. Default value: `None`, which means that times grid is computed using `time_step` and `num_time_steps`. Returns: Tuple `(all_times, mask)`. `all_times` is a 1-D real `Tensor` containing all points from 'times`, the uniform grid of points between `[0, times[-1]]` with grid size equal to `time_step`, and jump locations of piecewise constant parameters The `Tensor` is sorted in ascending order and may contain duplicates. `mask` is a boolean 1-D `Tensor` of the same shape as 'all_times', showing which elements of 'all_times' correspond to THE values from `times`. Guarantees that times[0]=0 and mask[0]=False. """ additional_times = [] for param in params: if isinstance(param, piecewise.PiecewiseConstantFunc): additional_times.append(param.jump_locations()) if times_grid is None: if time_step is not None: grid = tf.range(0.0, times[-1], time_step, dtype=dtype) all_times = tf.concat([grid, times] + additional_times, axis=0) elif num_time_steps is not None: grid = tf.linspace(tf.convert_to_tensor(0.0, dtype=dtype), times[-1] - time_step, num_time_steps) all_times = tf.concat([grid, times] + additional_times, axis=0) additional_times_mask = [ tf.zeros_like(times, dtype=tf.bool) for times in additional_times ] mask = tf.concat([ tf.zeros_like(grid, dtype=tf.bool), tf.ones_like(times, dtype=tf.bool) ] + additional_times_mask, axis=0) perm = tf.argsort(all_times, stable=True) all_times = tf.gather(all_times, perm) mask = tf.gather(mask, perm) else: all_times, mask, _ = utils.prepare_grid(times=times, time_step=time_step, times_grid=times_grid, dtype=dtype) return all_times, mask