code: def interpolate(
      times: types.RealTensor,
      interval_values: types.RealTensor,
      interval_times: types.RealTensor,
      validate_args: bool = False,
      dtype: tf.DType = None,
      name: str = None,
  ):
      """Performs the monotone convex interpolation.
  
      The monotone convex method is a scheme devised by Hagan and West (Ref [1]). It
      is a commonly used method to interpolate interest rate yield curves. For
      more details see Refs [1, 2].
  
      It is important to point out that the monotone convex method *does not* solve
      the standard interpolation problem but a modified one as described below.
  
      Suppose we are given a strictly increasing sequence of scalars (which we will
      refer to as time) `[t_1, t_2, ... t_n]` and a set of values
      `[f_1, f_2, ... f_n]`.
      The aim is to find a function `f(t)` defined on the interval `[0, t_n]` which
      satisfies (in addition to continuity and positivity conditions) the following
  
      ```None
        Integral[f(u), t_{i-1} <= u <= t_i] = f_i,  with t_0 = 0
  
      ```
  
      In the context of interest rate curve building, `f(t)` corresponds to the
      instantaneous forward rate at time `t` and the `f_i` correspond to the
      discrete forward rates that apply to the time period `[t_{i-1}, t_i]`.
      Furthermore, the integral of the forward curve is related to the yield curve
      by
  
      ```None
        Integral[f(u), 0 <= u <= t] = r(t) * t
  
      ```
  
      where `r(t)` is the interest rate that applies between `[0, t]` (the yield of
      a zero coupon bond paying a unit of currency at time `t`).
  
      This function computes both the interpolated value and the integral along
      the segment containing the supplied time. Specifically, given a time `t` such
      that `t_k <= t <= t_{k+1}`, this function computes the interpolated value
      `f(t)` and the value `Integral[f(u), t_k <= u <= t]`.
  
      This implementation of the method currently supports batching along the
      interpolation times but not along the interpolated curves (i.e. it is possible
      to evaluate the `f(t)` for `t` as a vector of times but not build multiple
      curves at the same time).
  
      #### Example
  
      ```python
      interval_times = tf.constant([0.25, 0.5, 1.0, 2.0, 3.0], dtype=dtype)
      interval_values = tf.constant([0.05, 0.051, 0.052, 0.053, 0.055],
                                    dtype=dtype)
      times = tf.constant([0.25, 0.5, 1.0, 2.0, 3.0, 1.1], dtype=dtype)
      # Returns the following two values:
      # interpolated = [0.0505, 0.05133333, 0.05233333, 0.054, 0.0555, 0.05241]
      # integrated =  [0, 0, 0, 0, 0.055, 0.005237]
      # Note that the first four integrated values are zero. This is because
      # those interpolation time are at the start of their containing interval.
      # The fourth value (i.e. at 3.0) is not zero because this is the last
      # interval (i.e. it is the integral from 2.0 to 3.0).
      interpolated, integrated = interpolate(
          times, interval_values, interval_times)
      ```
  
      #### References:
  
      [1]: Patrick Hagan & Graeme West. Interpolation Methods for Curve
        Construction. Applied Mathematical Finance. Vol 13, No. 2, pp 89-129.
        June 2006.
        https://www.researchgate.net/publication/24071726_Interpolation_Methods_for_Curve_Construction
      [2]: Patrick Hagan & Graeme West. Methods for Constructing a Yield Curve.
        Wilmott Magazine, pp. 70-81. May 2008.
  
      Args:
        times: Non-negative rank 1 `Tensor` of any size. The times for which the
          interpolation has to be performed.
        interval_values: Rank 1 `Tensor` of the same shape and dtype as
          `interval_times`. The values associated to each of the intervals specified
          by the `interval_times`. Must have size at least 2.
        interval_times: Strictly positive rank 1 `Tensor` of real dtype containing
          increasing values. The endpoints of the intervals (i.e. `t_i` above.).
          Note that the left end point of the first interval is implicitly assumed
          to be 0. Must have size at least 2.
        validate_args: Python bool. If true, adds control dependencies to check that
          the `times` are bounded by the `interval_endpoints`.
          Default value: False
        dtype: `tf.Dtype` to use when converting arguments to `Tensor`s. If not
          supplied, the default Tensorflow conversion will take place. Note that
          this argument does not do any casting.
          Default value: None.
        name: Python `str` name prefixed to Ops created by this class.
          Default value: None which is mapped to the default name 'interpolation'.
  
      Returns:
        A 2-tuple containing
          interpolated_values: Rank 1 `Tensor` of the same size and dtype as the
            `times`. The interpolated values at the supplied times.
          integrated_values: Rank 1 `Tensor` of the same size and dtype as the
            `times`. The integral of the interpolated function. The integral is
            computed from the largest interval time that is smaller than the time
            up to the given time.
      """
      if name is None:
          name = "interpolate"
      with tf.name_scope(name):
          times = tf.convert_to_tensor(times, dtype=dtype, name="times")
          interval_times = tf.convert_to_tensor(
              interval_times, dtype=dtype, name="interval_times"
          )
          interval_values = tf.convert_to_tensor(
              interval_values, dtype=dtype, name="interval_values"
          )
          control_deps = []
          if validate_args:
              control_deps = [
                  tf.compat.v1.debugging.assert_non_negative(times),
                  tf.compat.v1.debugging.assert_positive(
                      interval_times
                  ),
              ]
          with tf.compat.v1.control_dependencies(control_deps):
              # Step 1: Find the values at the endpoints.
              endpoint_values = _interpolate_adjacent(
                  interval_times, interval_values
              )
              endpoint_times = tf.concat(
                  [[0.0], interval_times], axis=0
              )
              intervals = piecewise.find_interval_index(
                  times, endpoint_times, last_interval_is_closed=True
              )
              # Comparing to the notation used in the paper:
              # f_left -> f_{i-1}
              # f_right -> f_i
              # t_left -> t_{i-1}
              # t_right -> t_i
              # fd -> f^d_i
              # g0 -> g0
              # g1 -> g1
              # g1plus2g0 -> g1 + 2 g0 (boundary line A)
              # g0plus2g1 -> g0 + 2 g1 (boundary line B)
              # x -> x
              f_left = tf.gather(endpoint_values, intervals)
              f_right = tf.gather(endpoint_values, intervals + 1)
              # fd is the discrete forward associated to each interval.
              fd = tf.gather(interval_values, intervals)
              t_left = tf.gather(endpoint_times, intervals)
              t_right = tf.gather(endpoint_times, intervals + 1)
              interval_lengths = t_right - t_left
              x = (times - t_left) / interval_lengths
  
              # TODO(b/140410758): The calculation below can be done more efficiently
              # if we instead do the following:
              # 1. Subdivide the regions further so that each subregion corresponds
              #   to a single quadratic in x. (Region 2, 3 and 4 get divided into 2
              #   pieces for a total of 7 cases.
              # 2. For each interval (i.e. [t_i, t{i+1}]) the case that applies to
              #   a point falling in that region can be decided and the corresponding
              #   quadratic coefficients computed once and for all.
              # 3. The above information is built once for the supplied forwards.
              # 4. Given the above information and a set of times to interpolate for,
              #   we map each time to the appropriate interval and compute the quadratic
              #   function value using that x.
  
              g0 = f_left - fd
              g1 = f_right - fd
              g1plus2g0 = g1 + 2 * g0
              g0plus2g1 = g0 + 2 * g1
  
              result = tf.zeros_like(times)
              integrated = tf.zeros_like(times)
  
              # The method uses quadratic splines to do the interpolation.
              # The specific spline used depends on the relationship between the
              # boundary values (`g0` and `g1` above).
              # The two dimensional plane determined by these two values is divided
              # into four wedge sections referred to as region 1, 2, 3 and 4 below.
              # For details of how the regions are defined, see Fig. 4 in Ref [2].
              (
                  is_region_1,
                  region_1_value,
                  integrated_value_1,
              ) = _region_1(g1plus2g0, g0plus2g1, g0, g1, x)
  
              result = tf.where(is_region_1, region_1_value, result)
              integrated = tf.where(
                  is_region_1, integrated_value_1, integrated
              )
  
              (
                  is_region_2,
                  region_2_value,
                  integrated_value_2,
              ) = _region_2(g1plus2g0, g0plus2g1, g0, g1, x)
  
              result = tf.where(is_region_2, region_2_value, result)
              integrated = tf.where(
                  is_region_2, integrated_value_2, integrated
              )
  
              (
                  is_region_3,
                  region_3_value,
                  integrated_value_3,
              ) = _region_3(g1plus2g0, g0plus2g1, g0, g1, x)
  
              result = tf.where(is_region_3, region_3_value, result)
              integrated = tf.where(
                  is_region_3, integrated_value_3, integrated
              )
  
              (
                  is_region_4,
                  region_4_value,
                  integrated_value_4,
              ) = _region_4(g1plus2g0, g0plus2g1, g0, g1, x)
              result = tf.where(is_region_4, region_4_value, result)
              integrated = tf.where(
                  is_region_4, integrated_value_4, integrated
              )
  
              # g0 = g1 = 0 requires special handling. Checking if the values are
              # legitimatey zero requires we pay close attention to the numerical
              # precision issues.
              g0_eps = tf.abs(tf.math.nextafter(fd, f_left) - fd) * 1.1
              g1_eps = tf.abs(tf.math.nextafter(fd, f_right) - fd) * 1.1
  
              is_origin = (tf.abs(g0) <= g0_eps) & (
                  tf.abs(g1) <= g1_eps
              )
  
              result = tf.where(
                  is_origin, tf.zeros_like(result), result
              )
              integrated = tf.where(
                  is_origin, tf.zeros_like(integrated), integrated
              )
  
              return (
                  result + fd,
                  (integrated + fd * x) * interval_lengths,
              )
  
file_path: tf-quant-finance/tf_quant_finance/rates/hagan_west/monotone_convex.py
imports: import tensorflow.compat.v2
  from tf_quant_finance import types
  from tf_quant_finance.math import piecewise
  from tf_quant_finance.math.diff_ops import diff
  from tf_quant_finance.rates.analytics import forwards
label_desc: Use image node_id 1 for calling a global function with example usage: interpolate(times, interval_values, interval_times, validate_args, dtype, name) and returns: unknown,  unknown