Coverage for tests / test_interpolate.py: 99%

141 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-27 14:36 +0000

1import unittest 

2import numpy as np 

3from rivapy.tools.interpolate import Interpolator 

4from rivapy.tools.enums import InterpolationType, ExtrapolationType 

5 

6 

7# , delta=1e-5 ? 

8class TestInterpolator(unittest.TestCase): 

9 

10 def setUp(self): 

11 """Test data, simple linear case. Extend to more robust if requested.""" 

12 self.x = [0, 1, 2, 3] 

13 self.y = [0, 10, 20, 30] 

14 

15 def test_linear_interpolation_scalar(self): 

16 """Test single target x argument""" 

17 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR) 

18 result = interpolator.interp(self.x, self.y, 1.5, "LINEAR") 

19 self.assertAlmostEqual(result, 15.0) 

20 

21 def test_linear_interpolation_list(self): 

22 """Test multi target x arguments""" 

23 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR) 

24 result = interpolator.interp(self.x, self.y, [0.5, 1.5, 2.5], "LINEAR") 

25 expected = [5.0, 15.0, 25.0] 

26 self.assertEqual(len(result), len(expected)) 

27 for r, e in zip(result, expected): 

28 self.assertAlmostEqual(r, e) 

29 

30 def test_linear_extrapolation_left_right(self): 

31 """Test both edges of extrapolation.""" 

32 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR) 

33 result = interpolator.interp(self.x, self.y, -1, "LINEAR") 

34 expected = -10.0 # slope = 10/unit, so -1 → -10 from given self.x and self.y 

35 self.assertAlmostEqual(result, expected) 

36 

37 result = interpolator.interp(self.x, self.y, 4, "LINEAR") 

38 expected = 40 

39 self.assertAlmostEqual(result, expected) 

40 

41 def test_constant_extrapolation(self): 

42 """Test the case of CONSTANT extrapolation mode selected.""" 

43 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.CONSTANT) 

44 result_left = interpolator.interp(self.x, self.y, -10, "CONSTANT") 

45 result_right = interpolator.interp(self.x, self.y, 10, "CONSTANT") 

46 self.assertEqual(result_left, self.y[0]) 

47 self.assertEqual(result_right, self.y[-1]) 

48 

49 def test_no_extrapolation_raises(self): 

50 """Test for the case that extrapolation was set to NONE but target values 

51 are outside data range 

52 """ 

53 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.NONE) 

54 with self.assertRaises(ValueError): 

55 interpolator.interp(self.x, self.y, -1, "NONE") 

56 with self.assertRaises(ValueError): 

57 interpolator.interp(self.x, self.y, 4, "NONE") 

58 

59 def test_no_extrapolationType_raises(self): 

60 """Test for the case that extrapolation was set to NONE and the extrapolation 

61 argument given is ExtrapolationType and not a str 

62 """ 

63 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.NONE) 

64 with self.assertRaises(ValueError): 

65 interpolator.interp(self.x, self.y, -1, ExtrapolationType.NONE) 

66 with self.assertRaises(ValueError): 

67 interpolator.interp(self.x, self.y, 4, ExtrapolationType.NONE) 

68 

69 def test_mismatched_length_raises(self): 

70 """Test the case of incorrect data input length mismatch.""" 

71 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR) 

72 with self.assertRaises(ValueError): 

73 interpolator.interp([0, 1, 2], [10, 20], 1.5, "LINEAR") 

74 

75 

76class TestLinearLogInterpolator(unittest.TestCase): 

77 

78 def setUp(self): 

79 """Set up sample positive data suitable for log-linear interpolation.""" 

80 self.x = [0.0, 1.0, 2.0, 3.0] 

81 # discount factors that decline exponentially — log-linear interpolation should reproduce this exactly 

82 self.df = [1.0, np.exp(-0.02), np.exp(-0.04), np.exp(-0.06)] # constant rate 2% per year 

83 

84 def test_log_linear_exact_exponential(self): 

85 """ 

86 For exponentially decaying data, log-linear interpolation should be exact. 

87 DF(x) = exp(-0.02 * x) 

88 """ 

89 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR) 

90 for x in [0.5, 1.5, 2.5]: 

91 df_interp = interpolator.interp(self.x, self.df, x, "LINEAR_LOG") 

92 expected = np.exp(-0.02 * x) 

93 self.assertAlmostEqual(df_interp, expected, delta=1e-12) 

94 

95 def test_log_linear_vector_input(self): 

96 """Test list input of target x values produces correct list output.""" 

97 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR) 

98 x_targets = [0.5, 1.0, 2.5] 

99 result = interpolator.interp(self.x, self.df, x_targets, "LINEAR_LOG") 

100 expected = [np.exp(-0.02 * x) for x in x_targets] 

101 self.assertEqual(len(result), len(expected)) 

102 for r, e in zip(result, expected): 

103 self.assertAlmostEqual(r, e, delta=1e-12) 

104 

105 def test_log_linear_extrapolation_linear_mode(self): 

106 """Check extrapolation using LINEAR mode reproduces reasonable continuation.""" 

107 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR) 

108 x_extrap = 4.0 

109 df_interp = interpolator.interp(self.x, self.df, x_extrap, "LINEAR_LOG") 

110 expected = np.exp(-0.02 * 4.0) 

111 self.assertAlmostEqual(df_interp, expected, delta=1e-12) 

112 

113 def test_log_linear_requires_positive_y(self): 

114 """Log-linear interpolation must raise an error if any y <= 0.""" 

115 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR) 

116 bad_y = [1.0, 0.5, 0.0, -0.5] 

117 with self.assertRaises(ValueError): 

118 interpolator.interp(self.x, bad_y, 1.0, "LINEAR_LOG") 

119 

120 def test_log_linear_constant_extrapolation(self): 

121 """Verify constant extrapolation on left/right boundaries.""" 

122 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.CONSTANT) 

123 result_left = interpolator.interp(self.x, self.df, -1.0, "CONSTANT") 

124 result_right = interpolator.interp(self.x, self.df, 4.0, "CONSTANT") 

125 self.assertAlmostEqual(result_left, self.df[0], delta=1e-12) 

126 self.assertAlmostEqual(result_right, self.df[-1], delta=1e-12) 

127 

128class TestHaganInterpolator(unittest.TestCase): 

129 

130 def setUp(self): 

131 # Simple synthetic data — exponential discount curve 

132 # DF(x) = exp(-r*x), with r = 0.05 constant forward rate 

133 self.x = [0.0, 1.0, 2.0, 3.0, 5.0] 

134 self.r = 0.05 

135 self.df = [np.exp(-self.r * t) for t in self.x] 

136 

137 # Forward rates between grid points should all be ~0.05 

138 self.fwd = (np.log(self.df[:-1]) - np.log(self.df[1:])) / (np.diff(self.x)) 

139 

140 

141 def test_hagan_polynomials_shapes(self): 

142 """Ensure polynomial coefficient arrays have consistent lengths. 

143 """ 

144 x_vals, a0, a1, a2 = Interpolator._hagan_polynomials(self.x, self.fwd) 

145 self.assertEqual(len(a0), len(a1)) 

146 self.assertEqual(len(a1), len(a2)) 

147 self.assertTrue(len(x_vals) >= 2) 

148 

149 

150 def test_hagan_constant_forward_rate(self): 

151 """Flat forward rate curve -> all interpolated points must equal 0.05. 

152 """ 

153 for x in np.linspace(0.1, 4.9, 9): 

154 f_interp = Interpolator.hagan(self.x, self.fwd, x, "CONSTANT") 

155 self.assertAlmostEqual(f_interp, self.r, delta=1e-10) 

156 

157 

158 def test_hagan_integrate_matches_analytical(self): 

159 """ 

160 For constant forward rate = 0.05, integral from 0→x should be 0.05 * x. 

161 """ 

162 for x in [0.5, 1.0, 2.5, 4.0]: 

163 integral = Interpolator.hagan_integrate(self.x, self.fwd, x) 

164 expected = self.r * x 

165 self.assertAlmostEqual(integral, expected, delta=1e-10) 

166 

167 

168 def test_hagan_df_inside_grid(self): 

169 """Inside grid, DF(x) = exp(-0.05 * x). 

170 """ 

171 for x in [0.5, 1.0, 2.0, 3.5]: 

172 df_interp = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF") 

173 expected = np.exp(-self.r * x) 

174 self.assertAlmostEqual(df_interp, expected, delta=1e-10) 

175 

176 

177 def test_hagan_df_extrapolation_constant_df(self): 

178 """Extrapolated DF should follow exp(-r_avg * x) rule. 

179 """ 

180 # Compute "average rate" up to last point 

181 y = Interpolator.hagan_integrate(self.x, self.fwd, self.x[-1]) 

182 r_avg = y / self.x[-1] 

183 

184 for x in [6.0, 7.5, 10.0]: 

185 df_extrap = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF") 

186 expected = np.exp(-r_avg * x) 

187 self.assertAlmostEqual(df_extrap, expected, delta=1e-10) 

188 

189 def test_hagan_df_extrapolation_below_first(self): 

190 """Check extrapolation below grid for CONSTANT_DF.""" 

191 y = Interpolator.hagan_integrate(self.x, self.fwd, self.x[1]) 

192 r_avg = y / self.x[1] 

193 

194 for x in [-0.5, -1.0]: 

195 df_extrap = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF") 

196 expected = np.exp(-r_avg * x) 

197 self.assertAlmostEqual(df_extrap, expected, delta=1e-10) 

198 

199 

200 def test_hagan_df_derivative_relation(self): 

201 """ 

202 DF'(x) = -f(x) * DF(x). Test numerically for constant forward rate. 

203 """ 

204 for x in [0.5, 1.0, 2.5, 4.0]: 

205 df_interp = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF") 

206 f_interp = Interpolator.hagan(self.x, self.fwd, x, "CONSTANT_DF") 

207 df_prime = Interpolator.hagan_df_derivative(self.x, self.df, x, "CONSTANT_DF") 

208 

209 expected = -f_interp * df_interp 

210 self.assertAlmostEqual(df_prime, expected, delta=1e-10) 

211 

212 # -------------------------- 

213 # Error cases 

214 def test_hagan_df_raises_for_short_input(self): 

215 """Require at least 2 discount factors.""" 

216 with self.assertRaises(ValueError): 

217 Interpolator.hagan_df([1.0], [0.99], 0.5, "CONSTANT_DF") 

218 

219 def test_hagan_extrapolation_raises_for_none(self): 

220 """Extrapolation NONE should raise.""" 

221 with self.assertRaises(ValueError): 

222 Interpolator.hagan_df(self.x, self.df, 10.0, "NONE") 

223 

224 def test_hagan_forward_out_of_bounds_raises(self): 

225 """Extrapolation type not allowed for forward interpolation.""" 

226 with self.assertRaises(ValueError): 

227 Interpolator.hagan(self.x, self.fwd, -1.0, "NONE") 

228 

229 

230 

231if __name__ == "__main__": 

232 unittest.main()