Beispiel #1
0
  def _conditional_mean_x(self, t, mr_t, sigma_t):
    """Computes the drift term in [1], Eq. 10.39."""
    t = tf.repeat(tf.expand_dims(t, axis=0), self._dim, axis=0)
    time_index = tf.searchsorted(self._jump_locations, t)
    vn = tf.concat([self._zero_padding, self._jump_locations], axis=1)
    y_between_vol_knots = self._y_integral(self._padded_knots,
                                           self._jump_locations,
                                           self._jump_values_vol,
                                           self._jump_values_mr)

    y_at_vol_knots = tf.concat(
        [self._zero_padding,
         utils.cumsum_using_matvec(y_between_vol_knots)], axis=1)

    ex_between_vol_knots = self._ex_integral(self._padded_knots,
                                             self._jump_locations,
                                             self._jump_values_vol,
                                             self._jump_values_mr,
                                             y_at_vol_knots[:, :-1])

    ex_at_vol_knots = tf.concat(
        [self._zero_padding,
         utils.cumsum_using_matvec(ex_between_vol_knots)], axis=1)

    c = tf.gather(y_at_vol_knots, time_index, batch_dims=1)
    exp_x_t = self._ex_integral(
        tf.gather(vn, time_index, batch_dims=1), t, sigma_t, mr_t, c)
    exp_x_t = exp_x_t + tf.gather(ex_at_vol_knots, time_index, batch_dims=1)
    exp_x_t = (exp_x_t[:, 1:] - exp_x_t[:, :-1]) * tf.math.exp(
        -tf.broadcast_to(mr_t, t.shape)[:, 1:] * t[:, 1:])
    return exp_x_t
Beispiel #2
0
    def _conditional_variance_x(self, t, mr_t, sigma_t):
        """Computes the variance of x(t), see [1], Eq. 10.41."""
        # Shape [dim, num_times]
        t = tf.broadcast_to(t, tf.concat([[self._dim], tf.shape(t)], axis=-1))
        var_x_between_vol_knots = self._variance_int(self._padded_knots,
                                                     self._jump_locations,
                                                     self._jump_values_vol,
                                                     self._jump_values_mr)
        varx_at_vol_knots = tf.concat([
            self._zero_padding,
            utils.cumsum_using_matvec(var_x_between_vol_knots)
        ],
                                      axis=1)

        time_index = tf.searchsorted(self._jump_locations, t)
        vn = tf.concat([self._zero_padding, self._jump_locations], axis=1)

        var_x_t = self._variance_int(tf.gather(vn, time_index, batch_dims=1),
                                     t, sigma_t, mr_t)
        var_x_t = var_x_t + tf.gather(
            varx_at_vol_knots, time_index, batch_dims=1)

        var_x_t = (var_x_t[:, 1:] - var_x_t[:, :-1]) * tf.math.exp(
            -2 * tf.broadcast_to(mr_t, tf.shape(t))[:, 1:] * t[:, 1:])
        return var_x_t
    def state_y(self, t):
        """Computes the state variable `y(t)` for tha Gaussian HJM Model.

    For Gaussian HJM model, the state parameter y(t), can be analytically
    computed as follows:

    y_ij(t) = exp(-k_i * t) * exp(-k_j * t) * (
              int_0^t rho_ij * sigma_i(u) * sigma_j(u) * du)

    Args:
      t: A rank 1 real `Tensor` of shape `[num_times]` specifying the time `t`.

    Returns:
      A real `Tensor` of shape [self._factors, self._factors, num_times]
      containing the computed y_ij(t).
    """
        t = tf.convert_to_tensor(t, dtype=self._dtype)
        t_shape = tf.shape(t)
        t = tf.broadcast_to(t, tf.concat([[self._dim], t_shape], axis=0))
        time_index = tf.searchsorted(self._jump_locations, t)
        # create a matrix k2(i,j) = k(i) + k(j)
        mr2 = tf.expand_dims(self._mean_reversion, axis=-1)
        # Add a dimension corresponding to `num_times`
        mr2 = tf.expand_dims(mr2 + tf.transpose(mr2), axis=-1)

        def _integrate_volatility_squared(vol, l_limit, u_limit):
            # create sigma2_ij = sigma_i * sigma_j
            vol = tf.expand_dims(vol, axis=-2)
            vol_squared = tf.expand_dims(
                self._rho, axis=-1) * (vol * tf.transpose(vol, perm=[1, 0, 2]))
            return vol_squared / mr2 * (tf.math.exp(mr2 * u_limit) -
                                        tf.math.exp(mr2 * l_limit))

        is_constant_vol = tf.math.equal(tf.shape(self._jump_values_vol)[-1], 0)
        v_squared_between_vol_knots = tf.cond(
            is_constant_vol,
            lambda: tf.zeros(shape=(self._dim, self._dim, 0),
                             dtype=self._dtype),
            lambda: _integrate_volatility_squared(  # pylint: disable=g-long-lambda
                self._jump_values_vol, self._padded_knots, self._jump_locations
            ))
        v_squared_at_vol_knots = tf.concat([
            tf.zeros((self._dim, self._dim, 1), dtype=self._dtype),
            utils.cumsum_using_matvec(v_squared_between_vol_knots)
        ],
                                           axis=-1)

        vn = tf.concat([self._zero_padding, self._jump_locations], axis=1)

        v_squared_t = _integrate_volatility_squared(
            self._volatility(t), tf.gather(vn, time_index, batch_dims=1), t)
        v_squared_t += tf.gather(v_squared_at_vol_knots,
                                 time_index,
                                 batch_dims=-1)

        return tf.math.exp(-mr2 * t) * v_squared_t
def _bond_option_variance(model, option_expiry, bond_maturity, dim):
    """Computes black equivalent variance for bond options.

  Black equivalent variance is definied as the variance to use in the Black
  formula to obtain the model implied price of European bond options.

  Args:
    model: An instance of `VectorHullWhiteModel`.
    option_expiry: A rank 1 `Tensor` of real dtype specifying the time to
      expiry of each option.
    bond_maturity: A rank 1 `Tensor` of real dtype specifying the time to
      maturity of underlying zero coupon bonds.
    dim: Dimensionality of the Hull-White process.

  Returns:
    A rank 1 `Tensor` of same dtype and shape as the inputs with computed
    Black-equivalent variance for the underlying options.
  """
    # pylint: disable=protected-access
    if model._sample_with_generic:
        raise ValueError('The paramerization of `mean_reversion` and/or '
                         '`volatility` does not support analytic computation '
                         'of bond option variance.')
    mean_reversion = model.mean_reversion(option_expiry)
    volatility = model.volatility(option_expiry)

    option_expiry = tf.repeat(tf.expand_dims(option_expiry, axis=0),
                              dim,
                              axis=0)
    bond_maturity = tf.repeat(tf.expand_dims(bond_maturity, axis=0),
                              dim,
                              axis=0)

    var_between_vol_knots = model._variance_int(model._padded_knots,
                                                model._jump_locations,
                                                model._jump_values_vol,
                                                model._jump_values_mr)
    varx_at_vol_knots = tf.concat([
        model._zero_padding,
        utils.cumsum_using_matvec(var_between_vol_knots)
    ],
                                  axis=1)

    time_index = tf.searchsorted(model._jump_locations, option_expiry)
    vn = tf.concat([model._zero_padding, model._jump_locations], axis=1)

    var_expiry = model._variance_int(tf.gather(vn, time_index, batch_dims=1),
                                     option_expiry, volatility, mean_reversion)
    var_expiry = var_expiry + tf.gather(
        varx_at_vol_knots, time_index, batch_dims=1)
    var_expiry = var_expiry * (
        tf.math.exp(-mean_reversion * option_expiry) -
        tf.math.exp(-mean_reversion * bond_maturity))**2 / mean_reversion**2
    # gpylint: enable=protected-access
    return var_expiry
    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))
Beispiel #6
0
  def _compute_yt(self, t, mr_t, sigma_t):
    """Computes y(t) as described in [1], section 10.1.6.1."""
    t = tf.repeat(tf.expand_dims(t, axis=0), self._dim, axis=0)
    time_index = tf.searchsorted(self._jump_locations, t)
    y_between_vol_knots = self._y_integral(
        self._padded_knots, self._jump_locations, self._jump_values_vol,
        self._jump_values_mr)
    y_at_vol_knots = tf.concat(
        [self._zero_padding,
         utils.cumsum_using_matvec(y_between_vol_knots)], axis=1)

    vn = tf.concat(
        [self._zero_padding, self._jump_locations], axis=1)
    y_t = self._y_integral(
        tf.gather(vn, time_index, batch_dims=1), t, sigma_t, mr_t)
    y_t = y_t + tf.gather(y_at_vol_knots, time_index, batch_dims=1)
    return tf.math.exp(-2 * mr_t * t) * y_t
Beispiel #7
0
def discount_factors_and_bond_prices_from_samples(
    expiries: types.RealTensor,
    payment_times: types.RealTensor,
    sample_discount_curve_paths_fn: Callable[..., Tuple[types.RealTensor,
                                                        types.RealTensor,
                                                        types.RealTensor]],
    num_samples: types.IntTensor,
    times: types.RealTensor = None,
    curve_times: types.RealTensor = None,
    dtype: tf.DType = None) -> Tuple[types.RealTensor, types.RealTensor]:
  """Utility function to compute the discount factors and the bond prices.

  Args:
    expiries: A real `Tensor` of any shape and dtype. The time to expiration of
      the swaptions. The shape of this input determines the number (and shape)
      of swaptions to be priced and the shape of the output - e.g. if there are
      two swaptions, and there are 11 payment dates for each swaption, then the
      shape of `expiries` is [2, 11], with entries repeated along the second
      axis.
    payment_times: A real `Tensor` of same dtype and compatible shape with
      `expiries` - e.g. if there are two swaptions, and there are 11 payment
      dates for each swaption, then the shape of `payment_times` should be [2,
      11]
    sample_discount_curve_paths_fn: Callable which takes the following args:
      1) times: Rank 1 `Tensor` of positive real values, specifying the times at
        which the path points are to be evaluated.
      2) curve_times: Rank 1 `Tensor` of positive real values, specifying the
        maturities at which the discount curve is to be computed at each
        simulation time.
      3) num_samples: Positive scalar integer specifying the number of paths to
        draw.
      Returns three `Tensor`s, the first being a N-D tensor of shape
        `model_batch_shape + [num_samples, m, k, d]` containing the simulated
        zero coupon bond curves, the second being a `Tensor` of shape
        `model_batch_shape + [num_samples, k, d]` containing the simulated
        short rate paths, the third `Tensor` of shape
        `model_batch_shape + [num_samples, k, d]` containing the simulated path
        discount factors. Here, m is the size of `curve_times`, k is the size
        of `times`, d is the dimensionality of the paths and
        `model_batch_shape` is shape of the batch of independent HJM models.
    num_samples: Positive scalar `int32` `Tensor`. The number of simulation
      paths during Monte-Carlo valuation.
    times: An optional rank 1 `Tensor` of increasing positive real values. The
      times at which Monte Carlo simulations are performed.
      Default value: `None`.
    curve_times: An optional rank 1 `Tensor` of positive real values. The
      maturities at which spot discount curve is computed during simulations.
      Default value: `None`.
    dtype: The default dtype to use when converting values to `Tensor`s.
      Default value: `None` which means that default dtypes inferred by
        TensorFlow are used.

  Returns:
    Two real tensors, `discount_factors` and `bond_prices`, both of shape
    [num_samples] + swaption_batch_shape + [dim], where `dim` is the dimension
    of each path (e.g for a Hull-White with two models, dim==2; while for HJM
    dim==1 always). `swaption_batch_shape` has the same rank as `expiries.shape`
    and its leading dimensions are broadcasted to `model_batch_shape`.
  """
  if times is not None:
    sim_times = tf.convert_to_tensor(times, dtype=dtype)
  else:
    # This might not be the most efficient if we have a batch of Models each
    # pricing swaptions with different expiries.
    sim_times = tf.reshape(expiries, shape=[-1])
    sim_times = tf.sort(sim_times, name='sort_sim_times')

  swaptionlet_shape = tf.shape(payment_times)
  tau = payment_times - expiries

  if curve_times is not None:
    curve_times = tf.convert_to_tensor(curve_times, dtype=dtype)
  else:
    # This might not be the most efficient if we have a batch of Models each
    # pricing swaptions with different expiries and payment times.
    curve_times = tf.reshape(tau, shape=[-1])
    curve_times, _ = tf.unique(curve_times)
    curve_times = tf.sort(curve_times, name='sort_curve_times')

  p_t_tau, r_t, discount_factors = sample_discount_curve_paths_fn(
      times=sim_times, curve_times=curve_times, num_samples=num_samples)

  dim = tf.shape(p_t_tau)[-1]
  model_batch_shape = tf.shape(p_t_tau)[:-4]
  model_batch_rank = p_t_tau.shape[:-4].rank
  instr_batch_shape = tf.shape(expiries)[model_batch_rank:]
  try:
    swaptionlet_shape = tf.concat(
        [model_batch_shape, instr_batch_shape], axis=0)
    expiries = tf.broadcast_to(expiries, swaptionlet_shape)
    tau = tf.broadcast_to(tau, swaptionlet_shape)
  except:
    raise ValueError('The leading dimensions of `expiries` of shape {} are not '
                     'compatible with the batch shape {} of the model.'.format(
                         expiries.shape.as_list(),
                         p_t_tau.shape.as_list()[:-4]))

  if discount_factors is None:
    dt = tf.concat(axis=0, values=[[0.0], sim_times[1:] - sim_times[:-1]])
    dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0)

    # Transpose before (and after) because we want the cumprod along axis=1
    # but `cumsum_using_matvec` operates on the last axis. Also we use cumsum
    # and then exponentiate instead of taking cumprod of exponents for
    # efficiency.
    cumul_rdt = tf.transpose(
        utils.cumsum_using_matvec(tf.transpose(r_t * dt, perm=[0, 2, 1])),
        perm=[0, 2, 1])
    discount_factors = tf.math.exp(-cumul_rdt)

  # Make discount factors the same shape as `p_t_tau`. This involves adding
  # an extra dimension (corresponding to `curve_times`).
  discount_factors = tf.expand_dims(discount_factors, axis=model_batch_rank + 1)

  # tf.repeat is needed because we will use gather_nd later on this tensor.
  discount_factors_simulated = tf.repeat(
      discount_factors, tf.shape(p_t_tau)[model_batch_rank + 1],
      axis=model_batch_rank + 1)

  # `sim_times` and `curve_times` are sorted for simulation. We need to
  # select the indices corresponding to our input.
  new_shape = tf.concat([model_batch_shape, [-1]], axis=0)
  sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1]))
  curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1]))

  sim_time_index = tf.reshape(sim_time_index, new_shape)
  curve_time_index = tf.reshape(curve_time_index, new_shape)
  gather_index = tf.stack([curve_time_index, sim_time_index], axis=-1)

  # shape=[num_samples] + batch_shape + [len(sim_times_index), dim]
  discount_factors_simulated = _gather_tensor_at_swaption_payoff(
      discount_factors_simulated, gather_index)
  payoff_discount_factors = tf.reshape(
      discount_factors_simulated,
      tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0))

  # shape=[num_samples, len(sim_times_index), dim]
  p_t_tau = _gather_tensor_at_swaption_payoff(p_t_tau, gather_index)
  payoff_bond_price = tf.reshape(
      p_t_tau,
      tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0))

  return payoff_discount_factors, payoff_bond_price
def discount_factors_and_bond_prices_from_samples(
        expiries,
        payment_times,
        sample_discount_curve_paths_fn,
        num_samples,
        time_step,
        dtype=None):
    """Utility function to compute the discount factors and the bond prices.

  Args:
    expiries: A real `Tensor` of any and dtype. The time to expiration of the
      swaptions. The shape of this input determines the number (and shape) of
      swaptions to be priced and the shape of the output - e.g. if there are two
      swaptions, and there are 11 payment dates for each swaption, then the
      shape of `expiries` is [2, 11], with entries repeated along the second
      axis.
    payment_times: A real `Tensor` of same dtype and compatible shape with
      `expiries` - e.g. if there are two swaptions, and there are 11 payment
      dates for each swaption, then the shape of `payment_times` should be [2,
      11]
    sample_discount_curve_paths_fn: Callable which takes the following args:
      1) times: Rank 1 `Tensor` of positive real values, specifying the times at
        which the path points are to be evaluated.
      2) curve_times: Rank 1 `Tensor` of positive real values, specifying the
        maturities at which the discount curve is to be computed at each
        simulation time.
      3) num_samples: Positive scalar integer specifying the number of paths to
        draw.  Returns two `Tensor`s, the first being a Rank-4 tensor of shape
        [num_samples, m, k, d] containing the simulated zero coupon bond curves,
        and the second being a `Tensor` of shape [num_samples, k, d] containing
        the simulated short rate paths. Here, m is the size of `curve_times`, k
        is the size of `times`, and d is the dimensionality of the paths.
    num_samples: Positive scalar `int32` `Tensor`. The number of simulation
      paths during Monte-Carlo valuation.
    time_step: Scalar real `Tensor`. Maximal distance between time grid points
      in Euler scheme. Relevant when Euler scheme is used for simulation.
    dtype: The default dtype to use when converting values to `Tensor`s.
      Default value: `None` which means that default dtypes inferred by
        TensorFlow are used.

  Returns:
    Two real tensors, `discount_factors` and `bond_prices`, both of shape
    [num_samples] + shape(payment_times) + [dim], where `dim` is the dimension
    of each path (e.g for a Hull-White with two models, dim==2; while for HJM
    dim==1 always.)
  """
    sim_times, _ = tf.unique(tf.reshape(expiries, shape=[-1]))
    longest_expiry = tf.reduce_max(sim_times)
    sim_times, _ = tf.unique(
        tf.concat([sim_times,
                   tf.range(time_step, longest_expiry, time_step)],
                  axis=0))
    sim_times = tf.sort(sim_times, name='sort_sim_times')

    swaptionlet_shape = payment_times.shape
    tau = payment_times - expiries

    curve_times_builder, _ = tf.unique(tf.reshape(tau, shape=[-1]))
    curve_times = tf.sort(curve_times_builder, name='sort_curve_times')

    p_t_tau, r_t = sample_discount_curve_paths_fn(times=sim_times,
                                                  curve_times=curve_times,
                                                  num_samples=num_samples)
    dim = p_t_tau.shape[-1]

    dt = tf.concat(axis=0,
                   values=[
                       tf.convert_to_tensor([0.0], dtype=dtype),
                       sim_times[1:] - sim_times[:-1]
                   ])
    dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0)

    # Compute the discount factors. We do this by performing the following:
    #
    # 1. We compute the implied discount factors. These are the factors:
    #    P(t1) = exp(-r1 * t1),
    #    P(t1, t2) = exp(-r2 (t2 - t1))
    #    P(t2, t3) = exp(-r3 (t3 - t2))
    #    ...
    # 2. We compute the cumulative products to get P(t2), P(t3), etc.:
    #    P(t2) = P(t1) * P(t1, t2)
    #    P(t3) = P(t1) * P(t1, t2) * P(t2, t3)
    #    ...
    # We perform the cumulative product by taking the cumulative sum over
    # log P's, and then exponentiating the sum. However, since each P is itself
    # an exponential, this effectively amounts to taking a cumsum over the
    # exponents themselves, and exponentiating in the end:
    #
    # P(t1) = exp(-r1 * t1)
    # P(t2) = exp(-r1 * t1 - r2 * (t2 - t1))
    # P(t3) = exp(-r1 * t1 - r2 * (t2 - t1) - r3 * (t3 - t2))
    # P(tk) = exp(-r1 * t1 - r2 * (t2 - t1) ... - r_k * (t_k - t_k-1))

    # Transpose before (and after) because we want the cumprod along axis=1
    # but `cumsum_using_matvec` operates on the last axis.
    cumul_rdt = tf.transpose(utils.cumsum_using_matvec(
        tf.transpose(r_t * dt, perm=[0, 2, 1])),
                             perm=[0, 2, 1])
    discount_factors = tf.math.exp(-cumul_rdt)

    # Make discount factors the same shape as `p_t_tau`. This involves adding
    # an extra dimenstion (corresponding to `curve_times`).
    discount_factors = tf.expand_dims(discount_factors, axis=1)

    # tf.repeat is needed because we will use gather_nd later on this tensor.
    discount_factors_simulated = tf.repeat(discount_factors,
                                           tf.shape(p_t_tau)[1],
                                           axis=1)

    # `sim_times` and `curve_times` are sorted for simulation. We need to
    # select the indices corresponding to our input.
    sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1]))
    curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1]))

    gather_index = _prepare_indices_ijjk(tf.range(0, num_samples),
                                         curve_time_index, sim_time_index,
                                         tf.range(0, dim))

    # The shape after `gather_nd` will be `(num_samples*num_swaptionlets*dim,)`
    payoff_discount_factors_builder = tf.gather_nd(discount_factors_simulated,
                                                   gather_index)
    # Reshape to `[num_samples] + swaptionlet.shape + [dim]`
    payoff_discount_factors = tf.reshape(payoff_discount_factors_builder,
                                         [num_samples] + swaptionlet_shape +
                                         [dim])
    payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index)
    payoff_bond_price = tf.reshape(payoff_bond_price_builder,
                                   [num_samples] + swaptionlet_shape + [dim])

    return payoff_discount_factors, payoff_bond_price
def discount_factors_and_bond_prices_from_samples(
        expiries,
        payment_times,
        sample_discount_curve_paths_fn,
        num_samples,
        times=None,
        curve_times=None,
        dtype=None):
    """Utility function to compute the discount factors and the bond prices.

  Args:
    expiries: A real `Tensor` of any and dtype. The time to expiration of the
      swaptions. The shape of this input determines the number (and shape) of
      swaptions to be priced and the shape of the output - e.g. if there are two
      swaptions, and there are 11 payment dates for each swaption, then the
      shape of `expiries` is [2, 11], with entries repeated along the second
      axis.
    payment_times: A real `Tensor` of same dtype and compatible shape with
      `expiries` - e.g. if there are two swaptions, and there are 11 payment
      dates for each swaption, then the shape of `payment_times` should be [2,
      11]
    sample_discount_curve_paths_fn: Callable which takes the following args:
      1) times: Rank 1 `Tensor` of positive real values, specifying the times at
        which the path points are to be evaluated.
      2) curve_times: Rank 1 `Tensor` of positive real values, specifying the
        maturities at which the discount curve is to be computed at each
        simulation time.
      3) num_samples: Positive scalar integer specifying the number of paths to
        draw.  Returns two `Tensor`s, the first being a Rank-4 tensor of shape
        [num_samples, m, k, d] containing the simulated zero coupon bond curves,
        and the second being a `Tensor` of shape [num_samples, k, d] containing
        the simulated short rate paths. Here, m is the size of `curve_times`, k
        is the size of `times`, and d is the dimensionality of the paths.
    num_samples: Positive scalar `int32` `Tensor`. The number of simulation
      paths during Monte-Carlo valuation.
    times: An optional rank 1 `Tensor` of increasing positive real values. The
      times at which Monte Carlo simulations are performed.
      Default value: `None`.
    curve_times: An optional rank 1 `Tensor` of positive real values. The
      maturities at which spot discount curve is computed during simulations.
      Default value: `None`.
    dtype: The default dtype to use when converting values to `Tensor`s.
      Default value: `None` which means that default dtypes inferred by
        TensorFlow are used.

  Returns:
    Two real tensors, `discount_factors` and `bond_prices`, both of shape
    [num_samples] + shape(payment_times) + [dim], where `dim` is the dimension
    of each path (e.g for a Hull-White with two models, dim==2; while for HJM
    dim==1 always.)
  """
    if times is not None:
        sim_times = tf.convert_to_tensor(times, dtype=dtype)
    else:
        sim_times = tf.reshape(expiries, shape=[-1])
        sim_times = tf.sort(sim_times, name='sort_sim_times')

    swaptionlet_shape = tf.shape(payment_times)
    tau = payment_times - expiries

    if curve_times is not None:
        curve_times = tf.convert_to_tensor(curve_times, dtype=dtype)
    else:
        curve_times = tf.reshape(tau, shape=[-1])
        curve_times, _ = tf.unique(curve_times)
        curve_times = tf.sort(curve_times, name='sort_curve_times')

    p_t_tau, r_t, discount_factors = sample_discount_curve_paths_fn(
        times=sim_times, curve_times=curve_times, num_samples=num_samples)
    dim = tf.shape(p_t_tau)[-1]

    if discount_factors is None:
        dt = tf.concat(axis=0, values=[[0.0], sim_times[1:] - sim_times[:-1]])
        dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0)

        # Transpose before (and after) because we want the cumprod along axis=1
        # but `cumsum_using_matvec` operates on the last axis. Also we use cumsum
        # and then exponentiate instead of taking cumprod of exponents for
        # efficiency.
        cumul_rdt = tf.transpose(utils.cumsum_using_matvec(
            tf.transpose(r_t * dt, perm=[0, 2, 1])),
                                 perm=[0, 2, 1])
        discount_factors = tf.math.exp(-cumul_rdt)

    # Make discount factors the same shape as `p_t_tau`. This involves adding
    # an extra dimenstion (corresponding to `curve_times`).
    discount_factors = tf.expand_dims(discount_factors, axis=1)

    # tf.repeat is needed because we will use gather_nd later on this tensor.
    discount_factors_simulated = tf.repeat(discount_factors,
                                           tf.shape(p_t_tau)[1],
                                           axis=1)

    # `sim_times` and `curve_times` are sorted for simulation. We need to
    # select the indices corresponding to our input.
    sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1]))
    curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1]))

    gather_index = _prepare_indices_ijjk(tf.range(0, num_samples),
                                         curve_time_index, sim_time_index,
                                         tf.range(0, dim))

    # The shape after `gather_nd` will be `(num_samples*num_swaptionlets*dim,)`
    payoff_discount_factors_builder = tf.gather_nd(discount_factors_simulated,
                                                   gather_index)
    # Reshape to `[num_samples] + swaptionlet.shape + [dim]`
    payoff_discount_factors = tf.reshape(
        payoff_discount_factors_builder,
        tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0))
    payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index)
    payoff_bond_price = tf.reshape(
        payoff_bond_price_builder,
        tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0))

    return payoff_discount_factors, payoff_bond_price