Coverage for /usr/local/lib/python3.10/dist-packages/Adifpy/differentiate/dual_number.py: 100%

116 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-07 00:47 -0500

1from __future__ import annotations 

2 

3import numpy as np 

4 

5 

6class DualNumber: 

7 """Tuple-like object for storing the value and directional derivatives during AD passes 

8 

9 This class is our implementation of dual numbers that will be used 

10 to calculate function and first-order derivative values at a point specified 

11 by the user in our auto-differentiation program. 

12 

13 >>> foo = DualNumber(1, 2) 

14 >>> bar = DualNumber(3, 4) 

15 >>> foo * bar 

16 3.0, 10.0 

17 """ 

18 

19 def __init__(self, real : float | int, dual: float = 1.0): 

20 self.real = real 

21 self.dual = dual 

22 

23 def __str__(self): 

24 return f"Dual Number object: ({float(self.real)}, {float(self.dual)})" 

25 

26 def __repr__(self): 

27 return f"{float(self.real)}, {float(self.dual)}" 

28 

29 def __add__(self, other: DualNumber | int | float) -> DualNumber: 

30 """Add a scalar or another dual number""" 

31 if isinstance(other, DualNumber): 

32 return DualNumber(self.real + other.real, self.dual + other.dual) 

33 try: 

34 return DualNumber(self.real + float(other), self.dual) 

35 except ValueError: 

36 raise TypeError("Operand must be of type int, float, or DualNumber.") 

37 

38 def __radd__(self, other: int | float) -> DualNumber: 

39 """Add this dual number to a scalar""" 

40 return self.__add__(other) 

41 

42 def __sub__(self, other: DualNumber | int | float) -> DualNumber: 

43 """Subtract a scalar or another dual number from this dual number""" 

44 if isinstance(other, DualNumber): 

45 return DualNumber(self.real - other.real, self.dual - other.dual) 

46 try: 

47 return DualNumber(self.real - float(other), self.dual) 

48 except ValueError: 

49 raise TypeError("Operand must be of type int, float, or DualNumber.") 

50 

51 def __rsub__(self, other: int | float) -> DualNumber: 

52 """Subtract a dual number from a scalar""" 

53 try: 

54 return DualNumber(float(other) - self.real, -self.dual) 

55 except ValueError: 

56 raise TypeError("Operand must be of type int, float, or DualNumber.") 

57 

58 def __mul__(self, other: DualNumber | int | float) -> DualNumber: 

59 """Multiply this dual number by another dual number or a scalar""" 

60 if isinstance(other, DualNumber): 

61 return DualNumber(self.real * other.real, self.real * other.dual + self.dual * other.real) 

62 try: 

63 other = float(other) 

64 return DualNumber(self.real * other, self.dual * other) 

65 except ValueError: 

66 raise TypeError("Operand must be of type int, float, or DualNumber.") 

67 

68 def __rmul__(self, other: int | float) -> DualNumber: 

69 """Multiply a scalar by this dual number""" 

70 return self.__mul__(other) 

71 

72 def __truediv__(self, other: DualNumber | int | float) -> DualNumber: 

73 """Divide this dual number by another dual number or a scalar""" 

74 if isinstance(other, DualNumber): 

75 return(DualNumber(self.real / other.real, (self.dual * other.real - other.dual * self.real) / (other.real**2))) 

76 try: 

77 other = float(other) 

78 return(DualNumber(self.real / other, self.dual / other)) 

79 except ValueError: 

80 raise TypeError("Operand must be of type int, float, or DualNumber.") 

81 

82 def __rtruediv__(self, other: int | float) -> DualNumber: 

83 """Divide a scalar by a dual number""" 

84 try: 

85 other = float(other) 

86 return(DualNumber(other / self.real, - self.dual * other / pow(self.real, 2))) 

87 except ValueError: 

88 raise TypeError("Operand must be of type int, float, or DualNumber.") 

89 

90 def __pow__(self, other: DualNumber | int | float) -> DualNumber: 

91 """Raise this dual number to the power of another dual number or a scalar""" 

92 if isinstance(other, DualNumber): 

93 return(DualNumber(pow(self.real, other.real), other.dual * np.log(self.real) * pow(self.real, other.real) + self.dual * other.real * pow(self.real, other.real - 1))) 

94 try: 

95 other = float(other) 

96 return(DualNumber(pow(self.real, other), self.dual * other * pow(self.real, other - 1))) 

97 except ValueError: 

98 raise TypeError("Operand must be of type int, float, or DualNumber.") 

99 

100 def __rpow__(self, other: int | float) -> DualNumber: 

101 """Raise a scalar to a dual number""" 

102 try: 

103 other = float(other) 

104 return(DualNumber(pow(other, self.real), self.dual * np.log(other) * pow(other, self.real))) 

105 except ValueError: 

106 raise TypeError("Operand must be of type int, float, or DualNumber.") 

107 

108 def __neg__(self: DualNumber) -> DualNumber: 

109 """Negate this dual number""" 

110 return -1 * self 

111 

112 # Other functions 

113 def exp(self): 

114 """Exponential e^x""" 

115 return DualNumber(np.exp(self.real), 

116 np.exp(self.real) * self.dual) 

117 

118 def sqrt(self): 

119 "Square root" 

120 return self ** 0.5 

121 

122 # Builtin log functions of multiple bases 

123 def log(self): 

124 "Natural log" 

125 return DualNumber(np.log(self.real), 

126 (1.0 / self.real) * self.dual) 

127 

128 def log10(self): 

129 """Log base 10""" 

130 return DualNumber(np.log10(self.real), 

131 1.0 / self.real / np.log(10) * self.dual) 

132 

133 def log2(self): 

134 """Log base 2""" 

135 return DualNumber(np.log2(self.real), 

136 1.0 / self.real / np.log(2) * self.dual) 

137 

138 # Trigonometric Functions 

139 def sin(self): 

140 return DualNumber(np.sin(self.real), 

141 np.cos(self.real) * self.dual) 

142 

143 def cos(self): 

144 return DualNumber(np.cos(self.real), 

145 -np.sin(self.real) * self.dual) 

146 

147 def tan(self): 

148 return DualNumber(np.tan(self.real), 

149 (1 / pow(np.cos(self.real), 2)) * self.dual) 

150 

151 # Inverse Trigonometric Functions 

152 def arcsin(self): 

153 return DualNumber(np.arcsin(self.real), 

154 pow(1 - pow(self.real,2), -1 / 2) * self.dual) 

155 

156 def arccos(self): 

157 return DualNumber(np.arccos(self.real), 

158 -pow(1 - pow(self.real,2), -1 / 2) * self.dual) 

159 

160 def arctan(self): 

161 return DualNumber(np.arctan(self.real), 

162 1 / (1 + pow(self.real, 2)) * self.dual) 

163 

164 # Hyperbolic Trigonometric Functions 

165 def sinh(self): 

166 return DualNumber(np.sinh(self.real), 

167 np.cosh(self.real) * self.dual) 

168 

169 def cosh(self): 

170 return DualNumber(np.cosh(self.real), 

171 np.sinh(self.real) * self.dual) 

172 

173 def tanh(self): 

174 return DualNumber(np.tanh(self.real), 

175 (1 - pow(np.tanh(self.real), 2)) * self.dual) 

176 

177 

178 # Inverse Hyperbolic Trigonometric Functions 

179 def arcsinh(self): 

180 return DualNumber(np.arcsinh(self.real), 

181 pow(pow(self.real, 2) + 1, -1 / 2) * self.dual) 

182 

183 def arccosh(self): 

184 return DualNumber(np.arccosh(self.real), 

185 pow(pow(self.real, 2) - 1, -1 / 2) * self.dual) 

186 

187 def arctanh(self): 

188 return DualNumber(np.arctanh(self.real), 

189 pow(1 - pow(self.real, 2), -1) * self.dual) 

190 

191 

192# Other functions 

193# NOTE: These functions will not be called through any instance of DualNumber: 

194# They will be called through the package via __init__.py 

195 

196def sigmoid(z): 

197 """Sigmoid activation function""" 

198 try: 

199 return 1.0 / (1.0 + np.exp(-float(z))) 

200 except TypeError: 

201 real = 1.0 / (1.0 + np.exp(-z.real)) 

202 return DualNumber(real, 

203 real * (1.0 - real) * z.dual) 

204 

205def logb(z, base: float): 

206 """Log with arbitrary base""" 

207 try: 

208 return np.log(float(z)) / np.log(base) 

209 except TypeError: 

210 return DualNumber(np.log(z.real) / np.log(base), 

211 1 / z.real / np.log(base) * z.dual)