def testFindsAnyRoots(self): objective_fn = lambda x: (63 * x**5 - 70 * x**3 + 15 * x + 2) / 8. left_bracket = [-10, 1] right_bracket = [10, -1] expected_num_iterations = [7, 6] expected_num_iterations, result = self.evaluate([ tf.constant(expected_num_iterations, dtype=tf.int32), root_search.brentq( objective_fn, tf.constant(left_bracket, dtype=tf.float64), tf.constant(right_bracket, dtype=tf.float64), stopping_policy_fn=tf.reduce_any) ]) roots, value_at_roots, num_iterations, _ = result expected_roots = [ optimize.brentq(objective_fn, left_bracket[0], right_bracket[0]), optimize.brentq(objective_fn, left_bracket[1], right_bracket[1]) ] self.assertNotAllClose(roots[0], expected_roots[0]) self.assertAllClose(roots[1], expected_roots[1]) self.assertNotAllClose(value_at_roots[0], 0.) self.assertAllClose(value_at_roots[0], objective_fn(roots[0])) self.assertAllClose(value_at_roots[1], 0.) self.assertAllEqual(num_iterations, expected_num_iterations)
def testFindsRootForFlatFunction(self): # Flat in the [-0.5, 0.5] range. objective_fn = lambda x: 0 if x == 0 else x * exp(-1 / x**2) left_bracket = [-10] right_bracket = [1] expected_num_iterations = [13] expected_num_iterations, result = self.evaluate([ tf.constant(expected_num_iterations, dtype=tf.int32), root_search.brentq(objective_fn, tf.constant(left_bracket, dtype=tf.float64), tf.constant(right_bracket, dtype=tf.float64)) ]) _, value_at_roots, num_iterations, _ = result # Simply check that the objective function is close to the root for the # returned estimate. Do not check the estimate itself. # Unlike Brent's original algorithm (and the SciPy implementation), this # implementation stops the search as soon as a good enough root estimate is # found. As a result, the estimate may significantly differ from the one # returned by SciPy for functions which are extremely flat around the root. self.assertAllClose(value_at_roots, [0.]) self.assertAllEqual(num_iterations, expected_num_iterations)
def testWithValueAtPositionssOfSameSign(self): f = lambda x: x**2 # Should fail: The objective function has the same sign at both positions. with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( root_search.brentq(f, tf.constant(-1, dtype=tf.float64), tf.constant(1, dtype=tf.float64), validate_args=True))
def body(right_bracket, converged, t_star): t_star, value_at_t_star, num_iterations, converged = root_search.brentq( fixed_point_function, left_bracket, right_bracket, None, None, 2e-12) t_star = t_star - value_at_t_star right_bracket = right_bracket * tf.constant(2.0, ztypes.float) return right_bracket, converged, t_star
def testWithInvalidMaxIterations(self): f = lambda x: x**3 # Should fail: Maximum number of iterations is negative. with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( root_search.brentq(f, tf.constant(-1, dtype=tf.float64), tf.constant(1, dtype=tf.float64), max_iterations=-1, validate_args=True))
def testWithInvalidValueTolerance(self): f = lambda x: x**3 # Should fail: Value tolerance is negative. with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( root_search.brentq(f, tf.constant(-1, dtype=tf.float64), tf.constant(1, dtype=tf.float64), function_tolerance=-2e-7, validate_args=True))
def testWithInvalidAbsoluteRootTolerance(self): f = lambda x: x**3 # Should fail: Absolute root tolerance is negative. with self.assertRaises(tf.errors.InvalidArgumentError): self.evaluate( root_search.brentq(f, tf.constant(-2, dtype=tf.float64), tf.constant(2, dtype=tf.float64), absolute_root_tolerance=-2e-7, validate_args=True))
def _testFindsAllRoots(self, objective_fn, left_bracket, right_bracket, expected_num_iterations, dtype=tf.float64, absolute_root_tolerance=2e-7, relative_root_tolerance=None, function_tolerance=2e-7): assert len(left_bracket) == len( right_bracket), "Brackets have different sizes" if relative_root_tolerance is None: relative_root_tolerance = root_search.default_relative_root_tolerance( dtype) expected_num_iterations, result = self.evaluate([ tf.constant(expected_num_iterations, dtype=tf.int32), root_search.brentq( objective_fn, tf.constant(left_bracket, dtype=dtype), tf.constant(right_bracket, dtype=dtype), absolute_root_tolerance=absolute_root_tolerance, relative_root_tolerance=relative_root_tolerance, function_tolerance=function_tolerance) ]) roots, value_at_roots, num_iterations, converged = result expected_roots = [] for i in range(0, len(left_bracket)): expected_roots.append( optimize.brentq( objective_fn, left_bracket[i], right_bracket[i], xtol=absolute_root_tolerance, rtol=relative_root_tolerance)) zeros = [0.] * len(left_bracket) # The output of SciPy and Tensorflow implementation should match for # well-behaved functions. self.assertAllClose( roots, expected_roots, atol=2 * absolute_root_tolerance, rtol=2 * relative_root_tolerance) self.assertAllClose(value_at_roots, zeros, atol=10 * function_tolerance) self.assertAllEqual(num_iterations, expected_num_iterations) self.assertAllEqual( converged, [abs(value) <= function_tolerance for value in value_at_roots])
def _testFindsAllRoots(self, objective_fn, left_bracket, right_bracket, expected_roots, expected_num_iterations, dtype=tf.float64, absolute_root_tolerance=2e-7, relative_root_tolerance=None, function_tolerance=2e-7, assert_check_for_num_iterations=True): # expected_roots are pre-calculated as follows: # import scipy.optimize as optimize # roots = optimize.brentq(objective_fn, # left_bracket[i], # right_bracket[i], # xtol=absolute_root_tolerance, # rtol=relative_root_tolerance) assert len(left_bracket) == len( right_bracket), "Brackets have different sizes" if relative_root_tolerance is None: relative_root_tolerance = root_search.default_relative_root_tolerance( dtype) expected_num_iterations, result = self.evaluate([ tf.constant(expected_num_iterations, dtype=tf.int32), root_search.brentq(objective_fn, tf.constant(left_bracket, dtype=dtype), tf.constant(right_bracket, dtype=dtype), absolute_root_tolerance=absolute_root_tolerance, relative_root_tolerance=relative_root_tolerance, function_tolerance=function_tolerance) ]) roots, value_at_roots, num_iterations, converged = result zeros = [0.] * len(left_bracket) # The output of SciPy and Tensorflow implementation should match for # well-behaved functions. self.assertAllClose(roots, expected_roots, atol=2 * absolute_root_tolerance, rtol=2 * relative_root_tolerance) self.assertAllClose(value_at_roots, zeros, atol=10 * function_tolerance) if assert_check_for_num_iterations: self.assertAllEqual(num_iterations, expected_num_iterations) self.assertAllEqual( converged, [abs(value) <= function_tolerance for value in value_at_roots])
def testWithNoIteration(self): left_bracket = [-10, 1] right_bracket = [10, -1] first_guess = tf.constant(left_bracket, dtype=tf.float64) second_guess = tf.constant(right_bracket, dtype=tf.float64) # Skip iteration entirely. # Should return a Tensor built from the best guesses in input positions. guess, result = self.evaluate([ tf.constant([-10, -1], dtype=tf.float64), root_search.brentq( polynomial5, first_guess, second_guess, max_iterations=0) ]) self.assertAllEqual(result.estimated_root, guess)
def testFindsAllRootsUsingFloat16(self): left_bracket = [-2, 1] right_bracket = [2, -1] expected_num_iterations = [9, 4] expected_num_iterations, result = self.evaluate([ tf.constant(expected_num_iterations, dtype=tf.int32), root_search.brentq(polynomial5, tf.constant(left_bracket, dtype=tf.float16), tf.constant(right_bracket, dtype=tf.float16)) ]) _, value_at_roots, num_iterations, _ = result # Simply check that the objective function is close to the root for the # returned estimates. Do not check the estimates themselves. # Using float16 may yield root estimates which differ from those returned # by the SciPy implementation. self.assertAllClose(value_at_roots, [0., 0.], atol=1e-3) self.assertAllEqual(num_iterations, expected_num_iterations)
def find_practical_support_bandwidth(kernel, bandwidth, absolute_tolerance=10e-5): """ Return the support for practical purposes. Used to find a support value for computations for kernel functions without finite (bounded) support. """ absolute_root_tolerance = 1e-3 relative_root_tolerance = root_search.default_relative_root_tolerance(ztypes.float) function_tolerance = 0 kernel_instance = kernel(loc=0, scale=bandwidth) def objective_fn(x): return kernel_instance.prob(x) - tf.constant(absolute_tolerance, ztypes.float) roots, value_at_roots, num_iterations, converged = root_search.brentq( objective_fn, tf.constant(0.0, dtype=ztypes.float), tf.constant(8.0, dtype=ztypes.float) * bandwidth, absolute_root_tolerance=absolute_root_tolerance, relative_root_tolerance=relative_root_tolerance, function_tolerance=function_tolerance ) return roots + absolute_root_tolerance
def _jamshidian_decomposition(hw_model, expiries, maturities, coefficients, dtype, name=None): """Jamshidian decomposition for European swaption valuation. Jamshidian decomposition is a widely used technique for the valuation of European swaptions (and options on coupon bearing bonds) when the underlying models for the term structure are short rate models (such as Hull-White model). The method transforms the swaption valuation to the valuation of a portfolio of call (put) options on zero-coupon bonds. Consider the following swaption payoff(assuming unit notional) at the exipration time (under a single curve valuation): ```None payoff = max(1 - P(T0, TN, r(T0)) - sum_1^N tau_i * X_i * P(T0, Ti, r(T0)), 0) = max(1 - sum_0^N alpha_i * P(T0, Ti, r(T0)), 0) ``` where `T0` denotes the swaption expiry, P(T0, Ti, r(T0)) denotes the price of the zero coupon bond at `T0` with maturity `Ti` and `r(T0)` is the short rate at time `T0`. If `r*` (or breakeven short rate) is the solution of the following equation: ```None 1 - sum_0^N alpha_i * P(T0, Ti, r*) = 0 (1) ``` Then the swaption payoff can be expressed as the following (Ref. [1]): ```None payoff = sum_1^N alpha_i max(P(T0, Ti, r*) - P(T0, Ti), 0) ``` where in the above formulation the swaption payoff is the same as that of a portfolio of bond options with strikes `P(T0, Ti, r*)`. The function accepts relevant inputs for the above computation and returns the strikes of the bond options computed using the Jamshidian decomposition. #### References: [1]: Leif B. G. Andersen and Vladimir V. Piterbarg. Interest Rate Modeling. Volume II: Term Structure Models. Chapter 10. Args: hw_model: An instance of `VectorHullWhiteModel`. The model used for the valuation. expiries: A real `Tensor` of any shape and dtype. The the time to expiration of the swaptions. maturities: A real `Tensor` of same shape and dtype as `expiries`. The payment times for fixed payments of the underlying swaptions. coefficients: A real `Tensor` of shape `expiries.shape + [n]` where `n` denotes the number of payments in the fixed leg of the underlying swaps. dtype: The default dtype to use when converting values to `Tensor`s. name: Python string. The name to give to the ops created by this function. Default value: `None` which maps to the default name `jamshidian_decomposition`. Returns: A real `Tensor` of shape expiries.shape + [dim] containing the forward bond prices computed at the breakeven short rate using the Jamshidian decomposition. `dim` stands for the dimensionality of the Hull-White process. """ name = name or 'jamshidian_decomposition' with tf.name_scope(name): dim = hw_model.dim() coefficients = tf.expand_dims(coefficients, axis=-1) def _zero_fun(x): # Get P(t0, t, r(t0)). p_t0_t = hw_model.discount_bond_price(x, expiries, maturities) # return_value.shape = batch_shape + [1] + [dim] return_value = tf.reduce_sum( coefficients * p_t0_t, axis=-2, keepdims=True) + [1.0] return return_value swap_shape = expiries.shape.as_list()[:-1] + [1] + [dim] lower_bound = -1 * tf.ones(swap_shape, dtype=dtype) upper_bound = 1 * tf.ones(swap_shape, dtype=dtype) # Solve Eq.(1) brent_results = root_search.brentq(_zero_fun, lower_bound, upper_bound) breakeven_short_rate = brent_results.estimated_root return hw_model.discount_bond_price(breakeven_short_rate, expiries, maturities)