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
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-07 00:47 -0500
1from __future__ import annotations
3import numpy as np
6class DualNumber:
7 """Tuple-like object for storing the value and directional derivatives during AD passes
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.
13 >>> foo = DualNumber(1, 2)
14 >>> bar = DualNumber(3, 4)
15 >>> foo * bar
16 3.0, 10.0
17 """
19 def __init__(self, real : float | int, dual: float = 1.0):
20 self.real = real
21 self.dual = dual
23 def __str__(self):
24 return f"Dual Number object: ({float(self.real)}, {float(self.dual)})"
26 def __repr__(self):
27 return f"{float(self.real)}, {float(self.dual)}"
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.")
38 def __radd__(self, other: int | float) -> DualNumber:
39 """Add this dual number to a scalar"""
40 return self.__add__(other)
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.")
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.")
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.")
68 def __rmul__(self, other: int | float) -> DualNumber:
69 """Multiply a scalar by this dual number"""
70 return self.__mul__(other)
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.")
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.")
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.")
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.")
108 def __neg__(self: DualNumber) -> DualNumber:
109 """Negate this dual number"""
110 return -1 * self
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)
118 def sqrt(self):
119 "Square root"
120 return self ** 0.5
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)
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)
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)
138 # Trigonometric Functions
139 def sin(self):
140 return DualNumber(np.sin(self.real),
141 np.cos(self.real) * self.dual)
143 def cos(self):
144 return DualNumber(np.cos(self.real),
145 -np.sin(self.real) * self.dual)
147 def tan(self):
148 return DualNumber(np.tan(self.real),
149 (1 / pow(np.cos(self.real), 2)) * self.dual)
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)
156 def arccos(self):
157 return DualNumber(np.arccos(self.real),
158 -pow(1 - pow(self.real,2), -1 / 2) * self.dual)
160 def arctan(self):
161 return DualNumber(np.arctan(self.real),
162 1 / (1 + pow(self.real, 2)) * self.dual)
164 # Hyperbolic Trigonometric Functions
165 def sinh(self):
166 return DualNumber(np.sinh(self.real),
167 np.cosh(self.real) * self.dual)
169 def cosh(self):
170 return DualNumber(np.cosh(self.real),
171 np.sinh(self.real) * self.dual)
173 def tanh(self):
174 return DualNumber(np.tanh(self.real),
175 (1 - pow(np.tanh(self.real), 2)) * self.dual)
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)
183 def arccosh(self):
184 return DualNumber(np.arccosh(self.real),
185 pow(pow(self.real, 2) - 1, -1 / 2) * self.dual)
187 def arctanh(self):
188 return DualNumber(np.arctanh(self.real),
189 pow(1 - pow(self.real, 2), -1) * self.dual)
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
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)
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)