Coverage for /home/runner/work/AutoDiff/AutoDiff/autodiff/node.py: 100%


Generated by Amelia Li for AutoDiff. (GitHub Profile)

92 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-07 04:22 +0000

1import numpy as np 

2 

3class Node: 

4 """Node implementation for reversed mode.""" 

5 

6 _supported_scalars = (int, float) 

7 

8 def __init__(self, val, gradients=()) -> None: 

9 """ 

10 Initialize a node with its value and local gradients. 

11 

12 Parameters 

13 ---------- 

14 val : integer or float 

15 Value of a node. 

16  

17 gradients : tuple 

18 Local gradients of a node. 

19 Consists of 1 or 2 tuples of (`child node`, `local gradient value`) 

20 

21 """ 

22 self.val = val 

23 self.gradients = gradients 

24 

25 ### Elementary Functions ### 

26 def __add__(self, other): 

27 """ 

28 Compute addition of two nodes or a node and a constant. 

29 

30 Parameters 

31 ---------- 

32 other : Node, constant 

33 Input object which is added to a node. 

34 

35 Returns 

36 ------- 

37 node 

38 The method returns a new node initialized with its value and gradients resulting from the addition. 

39  

40 Raises 

41 ------ 

42 TypeError 

43 This method raises a `TypeError` if the type of input number other is not supported. 

44 

45 """ 

46 # check if other is of a supported type 

47 if not isinstance(other, (*self._supported_scalars, Node)): 

48 raise TypeError(f"Unsupported type '{type(other)}'") 

49 

50 if isinstance(other, self._supported_scalars): 

51 # scalar 

52 return Node(other + self.val, ((self, 1),)) 

53 else: 

54 # node 

55 return Node(self.val + other.val, ((self, 1),(other, 1))) 

56 

57 def __neg__(self): 

58 """ 

59 Compute the negation of one node. 

60 

61 Returns 

62 ------- 

63 node 

64 The method returns a new node initialized with its value and gradients resulting from the negation. 

65 

66 """ 

67 return Node(-self.val, ((self, -1),)) 

68 

69 def __radd__(self, other): 

70 """ 

71 Compute addition of two constants. 

72 

73 Parameters 

74 ---------- 

75 other : Constant 

76 Input constant which is added to another constant. 

77 

78 Returns 

79 ------- 

80 node 

81 The method returns a new node initialized with its value and gradients resulting from the addition. 

82 

83 """ 

84 return self.__add__(other) 

85 

86 def __sub__(self, other): 

87 """ 

88 Compute subtraction of one node from another node or a constant from a node. 

89 

90 

91 Parameters 

92 ---------- 

93 other : Node, Constant 

94 Input object which is subtracted from a node. 

95 

96 Returns 

97 ------- 

98 node 

99 The method returns a new node initialized with its value and gradients resulting from the subtraction. 

100 

101 """ 

102 # check if other is of a supported type 

103 if not isinstance(other, (*self._supported_scalars, Node)): 

104 raise TypeError(f"Unsupported type '{type(other)}'") 

105 

106 if isinstance(other, self._supported_scalars): 

107 # scalar 

108 return Node(self.val - other, ((self, 1),)) 

109 else: 

110 # node 

111 return Node(self.val - other.val, ((self, 1),(other, -1))) 

112 

113 

114 def __rsub__(self, other): 

115 """ 

116 Compute subtraction of one constant from another constant. 

117 

118 Parameters 

119 ---------- 

120 other : Node, Constant 

121 Input object which is subtracted by a node. 

122 

123 Returns 

124 ------- 

125 node 

126 The method returns a new node initialized with its value and gradients resulting from the subtraction. 

127 

128 """ 

129 # check if other is of a supported type 

130 if not isinstance(other, (*self._supported_scalars, Node)): 

131 raise TypeError(f"Unsupported type '{type(other)}'") 

132 

133 return Node(other - self.val, ((self, -1),)) 

134 

135 def __mul__(self, other): 

136 """ 

137 Compute multiplication of two nodes or a node and a constant. 

138 

139 Parameters 

140 ---------- 

141 other : Node, Constant 

142 Input object which is multiplied by a node. 

143 

144 Returns 

145 ------- 

146 node 

147 The method returns a new node initialized with its value and gradients resulting from the multiplication. 

148  

149 Raises 

150 ------ 

151 TypeError 

152 This method raises a `TypeError` if the type of input number other is not supported. 

153 

154 """ 

155 # check if other is of a supported type 

156 if not isinstance(other, (*self._supported_scalars, Node)): 

157 raise TypeError(f"Unsupported type '{type(other)}'") 

158 if isinstance(other, self._supported_scalars): 

159 # scalar 

160 return Node(other * self.val, ((self, other),)) 

161 else: 

162 # node 

163 return Node(self.val * other.val, ((self, other.val),(other, self.val))) 

164 

165 def __rmul__(self, other): 

166 """ 

167 Compute multiplication of two constants. 

168 

169 Parameters 

170 ---------- 

171 other : Constant 

172 Input constant which is multiplied by another constant. 

173 

174 Returns 

175 ------- 

176 node 

177 The method returns a new node initialized with its value and gradients resulting from the multiplication. 

178 

179 """ 

180 return self.__mul__(other) 

181 

182 def __truediv__(self, other): 

183 """ 

184 Compute division of 'self' node by 'other' node. 

185  

186 Parameters 

187 ---------- 

188 other : Node, Constant 

189 Input object which divides a node. 

190 

191 Returns 

192 ------- 

193 node 

194 The method returns a new node initialized with its value and gradients resulting from the division. 

195 

196 """ 

197 # check if other is of a supported type 

198 if not isinstance(other, (*self._supported_scalars, Node)): 

199 raise TypeError(f"Unsupported type '{type(other)}'") 

200 if isinstance(other, self._supported_scalars): 

201 # scalar 

202 return Node(self.val/other, ((self, 1/other),)) 

203 else: 

204 # node 

205 return Node(self.val/other.val, ((self, 1/other.val),(other, -self.val*other.val**-2))) 

206 

207 def __rtruediv__(self, other): 

208 """ 

209 Compute division of 'other' node by 'self' node. 

210 

211 Parameters 

212 ---------- 

213 other : Node, Constant 

214 Input object which is divided by a node. 

215  

216 Returns 

217 ------- 

218 node 

219 The method returns a new node initialized with its value and gradients resulting from the division. 

220 

221 """ 

222 # check if other is of a supported type 

223 if not isinstance(other, (*self._supported_scalars, Node)): 

224 raise TypeError(f"Unsupported type '{type(other)}'") 

225 return Node(other/self.val, ((self, -other*self.val**-2),)) 

226 

227 def __pow__(self, other): 

228 """ 

229 Compute the exponentiation of raising one node value to the power of another node value or of one constant. 

230 

231 Parameters 

232 ---------- 

233 other : Node, Constant 

234 Input object to whose power the node will be raised. 

235 

236 Returns 

237 ------- 

238 node 

239 The method returns a new node initialized with its value and gradients resulting from the exponentiation. 

240  

241 Raises 

242 ------ 

243 TypeError 

244 This method raises a `TypeError` if the type of input number other is not supported. 

245 

246 """ 

247 # check if other is of supported type 

248 if not isinstance(other, (*self._supported_scalars, Node)): 

249 raise TypeError(f"Unsupported type '{type(other)}'") 

250 if isinstance(other, self._supported_scalars): 

251 # scalar 

252 return Node(self.val**other, ((self, other*self.val**(other-1)),)) 

253 else: 

254 # node 

255 return Node(self.val**other.val, ((self, other.val*self.val**(other.val-1)), 

256 (other, self.val**other.val*np.log(self.val)))) 

257 

258 def __rpow__(self, other): 

259 """ 

260 Compute the exponentiation of raising 'other' node value to the power of 'self' node value. 

261 

262 Parameters 

263 ---------- 

264 other : Node, Constant 

265 Input object which will be raised to the power of the 'self' node value. 

266 

267 Returns 

268 ------- 

269 node 

270 The method returns a new node initialized with its value and gradients resulting from the exponentiation. 

271  

272 Raises 

273 ------ 

274 TypeError 

275 This method raises a `TypeError` if the type of input number other is not supported. 

276 

277 """ 

278 # check if other is of supported type 

279 if not isinstance(other, (*self._supported_scalars, Node)): 

280 raise TypeError(f"Unsupported type '{type(other)}'") 

281 return Node(other**self.val, ((self, other**self.val*np.log(other)),)) 

282 

283 ### Square Root Function ### 

284 def sqrt(self): 

285 """ 

286 Compute the square root of one node. 

287 

288 Returns 

289 ------- 

290 Dual 

291 The method returns a new node initialized with its value and gradients resulting from the square root. 

292  

293 Raises 

294 ------ 

295 ValueError 

296 This method raises a `ValueError` if the value of input node value is less than zero. 

297 

298 """ 

299 if self.val < 0: 

300 raise ValueError("Cannot square root: value of node is less than 0.") 

301 return Node(self.val ** (1/2), ((self, 1/2*(self.val**(-1/2))),)) 

302 

303 ### Exponential Function ### 

304 def exp(self): 

305 """ 

306 Compute the exponentiation of raising the natural number to the power of a node value. 

307 

308 Returns 

309 ------- 

310 Node 

311 The method returns a new node initialized with its value and gradients resulting from the exponentiation. 

312 

313 """ 

314 return Node(np.exp(self.val), ((self, np.exp(self.val)),)) 

315 

316 ### Logarithmic Function ### 

317 def log(self, base): 

318 """ 

319 Compute the logarithm to find the power to which the input base must be raised to yield the given node value. 

320 

321 Parameters 

322 ---------- 

323 base : Node, Constant 

324 Input base which is raised to yield a given node value. 

325 

326 Returns 

327 ------- 

328 Node 

329 The method returns a new node initialized with its value and gradients resulting from the logarithm. 

330  

331 Raises 

332 ------ 

333 TypeError 

334 This method raises a `TypeError` if the type of input base number is not supported. 

335  

336 ValueError 

337 This method raises a `ValueError` if the value of node or the value of input base is less than zero. 

338 

339 """ 

340 # check if base is of supported type 

341 if not isinstance(base, self._supported_scalars): 

342 raise TypeError(f"Unsupported base type '{type(base)}'") 

343 # check that the base is above 0. 

344 if base <= 0: 

345 raise ValueError("Cannot log: Base is less than or equal to 0.") 

346 # check that the value of the node is greater than 0. 

347 if self.val <= 0: 

348 raise ValueError("Cannot log: Value of node is less than or equal to 0.") 

349 return Node(np.log(self.val)/np.log(base), ((self, 1 / (np.log(base)*self.val)),)) 

350 

351 ### Logistic Function ### 

352 def standard_logistic(self): 

353 """ 

354 Compute the value of the standard logistic function with the given node as input. 

355 

356 Returns 

357 ------- 

358 node 

359 The method returns the value of the standard logistic function with the given node as input. 

360 

361 """ 

362 return Node(1 / (1 + np.exp(-self.val)),((self, 1/(1+np.exp(-self.val)) * (1-1/(1+np.exp(-self.val)))),)) 

363 

364 ### Trigonometric Functions ###  

365 def sin(self): 

366 """ 

367 Compute the sine of a node value. 

368 

369 Returns 

370 ------- 

371 Node 

372 The method returns The method returns a new node initialized with its value and gradients resulting from the sine. 

373 

374 """ 

375 return Node(np.sin(self.val), ((self, np.cos(self.val)),)) 

376 

377 def cos(self): 

378 """ 

379 Compute the cosine of a node value. 

380 

381 Returns 

382 ------- 

383 Node 

384 The method returns a new node initialized with its value and gradients resulting from the cosine. 

385  

386 """ 

387 return Node(np.cos(self.val), ((self, -np.sin(self.val)),)) 

388 

389 def tan(self): 

390 """ 

391 Compute the tangent of a node value. 

392 

393 Returns 

394 ------- 

395 Node 

396 The method returns a new node initialized with its value and gradients resulting from the tangent. 

397  

398 """ 

399 return Node(np.tan(self.val), ((self, 1 / (np.cos(self.val) ** 2)),)) 

400 

401 ### Inverse Trigonometric Functions ### 

402 def arcsin(self): 

403 """ 

404 Compute the arcsine of a node value. 

405 

406 Returns 

407 ------- 

408 Node 

409 The method returns a new node initialized with its value and gradients resulting from the arcsine. 

410  

411 Raises 

412 ------ 

413 ValueError 

414 This method raises a `ValueError` if the value of the node is smaller than -1 or greater than 1. 

415 

416 """ 

417 if self.val >= 1 or self.val <= -1: 

418 raise ValueError("Value of node is not between -1 and 1.") 

419 return Node(np.arcsin(self.val), ((self, 1 / np.sqrt(1 - self.val ** 2)),)) 

420 

421 def arccos(self): 

422 """ 

423 Compute the arccosine of a node value. 

424 

425 Returns 

426 ------- 

427 Node 

428 The method returns a new node initialized with its value and gradients resulting from the arccosine. 

429  

430 Raises 

431 ------ 

432 ValueError 

433 This method raises a `ValueError` if the value of the node is smaller than -1 or greater than 1. 

434 

435 """ 

436 if self.val >= 1 or self.val <= -1: 

437 raise ValueError("Value of node is not between -1 and 1.") 

438 return Node(np.arccos(self.val), ((self, - 1 / np.sqrt(1 - self.val ** 2)),)) 

439 

440 def arctan(self): 

441 """ 

442 Compute the arctangent of a node value. 

443 

444 Returns 

445 ------- 

446 Node 

447 The method returns a new node initialized with its value and gradients resulting from the arctangent. 

448 

449 """ 

450 return Node(np.arctan(self.val), ((self, 1 / ((self.val ** 2) + 1)),)) 

451 

452 ### Hyperbolic Functions ### 

453 def sinh(self): 

454 """ 

455 Compute the hyperbolic sine of a node value. 

456 

457 Returns 

458 ------- 

459 Node 

460 The method returns a new node initialized with its value and gradients resulting from the hyperbolic sine. 

461 

462 """ 

463 return Node(np.sinh(self.val), ((self, np.cosh(self.val)),)) 

464 

465 def cosh(self): 

466 """ 

467 Compute the hyperbolic cosine of a node value. 

468 

469 Returns 

470 ------- 

471 Node 

472 The method returns a new node initialized with its value and gradients resulting from the hyperbolic cosine. 

473 

474 """ 

475 return Node(np.cosh(self.val), ((self, np.sinh(self.val)),)) 

476 

477 def tanh(self): 

478 """ 

479 Compute the hyperbolic tangent of a node value. 

480 

481 Returns 

482 ------- 

483 Node 

484 The method returns a new node initialized with its value and gradients resulting from the hyperbolic tangent. 

485 

486 """ 

487 return Node(np.tanh(self.val), ((self, 1/np.cosh(self.val)**2),))