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))
Пример #2
0
 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)
Пример #3
0
 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))
Пример #5
0
  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))
Пример #6
0
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)
Пример #8
0
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)
Пример #10
0
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)
Пример #11
0
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