Coverage for chebpy/core/classicfun.py: 99%

143 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-07 10:30 +0000

1"""Implementation of the Classicfun class for functions on arbitrary intervals. 

2 

3This module provides the Classicfun class, which represents functions on arbitrary intervals 

4by mapping them to a standard domain [-1, 1] and using a Onefun representation. 

5""" 

6 

7from abc import ABC 

8 

9import matplotlib.pyplot as plt 

10import numpy as np 

11 

12from .chebtech import Chebtech 

13from .decorators import self_empty 

14from .exceptions import IntervalMismatch, NotSubinterval 

15from .fun import Fun 

16from .plotting import plotfun 

17from .settings import _preferences as prefs 

18from .utilities import Interval 

19 

20techdict = { 

21 "Chebtech": Chebtech, 

22} 

23 

24 

25class Classicfun(Fun, ABC): 

26 """Abstract base class for functions defined on arbitrary intervals using a mapped representation. 

27 

28 This class implements the Fun interface for functions defined on arbitrary intervals 

29 by mapping them to a standard domain [-1, 1] and using a Onefun representation 

30 (such as Chebtech) on that standard domain. 

31 

32 The Classicfun class serves as a base class for specific implementations like Bndfun. 

33 It handles the mapping between the arbitrary interval and the standard domain, 

34 delegating the actual function representation to the underlying Onefun object. 

35 """ 

36 

37 # -------------------------- 

38 # alternative constructors 

39 # -------------------------- 

40 @classmethod 

41 def initempty(cls): 

42 """Initialize an empty function. 

43 

44 This constructor creates an empty function representation, which is 

45 useful as a placeholder or for special cases. The interval has no 

46 relevance to the emptiness status of a Classicfun, so we arbitrarily 

47 set it to be the default interval [-1, 1]. 

48 

49 Returns: 

50 Classicfun: A new empty instance. 

51 """ 

52 interval = Interval() 

53 onefun = techdict[prefs.tech].initempty(interval=interval) 

54 return cls(onefun, interval) 

55 

56 @classmethod 

57 def initconst(cls, c, interval): 

58 """Initialize a constant function. 

59 

60 This constructor creates a function that represents a constant value 

61 on the specified interval. 

62 

63 Args: 

64 c: The constant value. 

65 interval: The interval on which to define the function. 

66 

67 Returns: 

68 Classicfun: A new instance representing the constant function f(x) = c. 

69 """ 

70 onefun = techdict[prefs.tech].initconst(c, interval=interval) 

71 return cls(onefun, interval) 

72 

73 @classmethod 

74 def initidentity(cls, interval): 

75 """Initialize the identity function f(x) = x. 

76 

77 This constructor creates a function that represents f(x) = x 

78 on the specified interval. 

79 

80 Args: 

81 interval: The interval on which to define the identity function. 

82 

83 Returns: 

84 Classicfun: A new instance representing the identity function. 

85 """ 

86 onefun = techdict[prefs.tech].initvalues(np.asarray(interval), interval=interval) 

87 return cls(onefun, interval) 

88 

89 @classmethod 

90 def initfun_adaptive(cls, f, interval): 

91 """Initialize from a callable function using adaptive sampling. 

92 

93 This constructor determines the appropriate number of points needed to 

94 represent the function to the specified tolerance using an adaptive algorithm. 

95 

96 Args: 

97 f (callable): The function to be approximated. 

98 interval: The interval on which to define the function. 

99 

100 Returns: 

101 Classicfun: A new instance representing the function f. 

102 """ 

103 onefun = techdict[prefs.tech].initfun(lambda y: f(interval(y)), interval=interval) 

104 return cls(onefun, interval) 

105 

106 @classmethod 

107 def initfun_fixedlen(cls, f, interval, n): 

108 """Initialize from a callable function using a fixed number of points. 

109 

110 This constructor uses a specified number of points to represent the function, 

111 rather than determining the number adaptively. 

112 

113 Args: 

114 f (callable): The function to be approximated. 

115 interval: The interval on which to define the function. 

116 n (int): The number of points to use. 

117 

118 Returns: 

119 Classicfun: A new instance representing the function f. 

120 """ 

121 onefun = techdict[prefs.tech].initfun(lambda y: f(interval(y)), n, interval=interval) 

122 return cls(onefun, interval) 

123 

124 # ------------------- 

125 # 'private' methods 

126 # ------------------- 

127 def __call__(self, x, how="clenshaw"): 

128 """Evaluate the function at points x. 

129 

130 This method evaluates the function at the specified points by mapping them 

131 to the standard domain [-1, 1] and evaluating the underlying onefun. 

132 

133 Args: 

134 x (float or array-like): Points at which to evaluate the function. 

135 how (str, optional): Method to use for evaluation. Defaults to "clenshaw". 

136 

137 Returns: 

138 float or array-like: The value(s) of the function at the specified point(s). 

139 Returns a scalar if x is a scalar, otherwise an array of the same size as x. 

140 """ 

141 y = self.interval.invmap(x) 

142 return self.onefun(y, how) 

143 

144 def __init__(self, onefun, interval): 

145 """Initialize a new Classicfun instance. 

146 

147 This method initializes a new function representation on the specified interval 

148 using the provided onefun object for the standard domain representation. 

149 

150 Args: 

151 onefun: The Onefun object representing the function on [-1, 1]. 

152 interval: The Interval object defining the domain of the function. 

153 """ 

154 self.onefun = onefun 

155 self._interval = interval 

156 

157 def __repr__(self): # pragma: no cover 

158 """Return a string representation of the function. 

159 

160 This method returns a string representation of the function that includes 

161 the class name, support interval, and size. 

162 

163 Returns: 

164 str: A string representation of the function. 

165 """ 

166 out = "{0}([{2}, {3}], {1})".format(self.__class__.__name__, self.size, *self.support) 

167 return out 

168 

169 # ------------ 

170 # properties 

171 # ------------ 

172 @property 

173 def coeffs(self): 

174 """Get the coefficients of the function representation. 

175 

176 This property returns the coefficients used in the function representation, 

177 delegating to the underlying onefun object. 

178 

179 Returns: 

180 array-like: The coefficients of the function representation. 

181 """ 

182 return self.onefun.coeffs 

183 

184 @property 

185 def endvalues(self): 

186 """Get the values of the function at the endpoints of its interval. 

187 

188 This property evaluates the function at the endpoints of its interval 

189 of definition. 

190 

191 Returns: 

192 numpy.ndarray: Array containing the function values at the endpoints 

193 of the interval [a, b]. 

194 """ 

195 return self.__call__(self.support) 

196 

197 @property 

198 def interval(self): 

199 """Get the interval on which this function is defined. 

200 

201 This property returns the interval object representing the domain 

202 of definition for this function. 

203 

204 Returns: 

205 Interval: The interval on which this function is defined. 

206 """ 

207 return self._interval 

208 

209 @property 

210 def isconst(self): 

211 """Check if this function represents a constant. 

212 

213 This property determines whether the function is constant (i.e., f(x) = c 

214 for some constant c) over its interval of definition, delegating to the 

215 underlying onefun object. 

216 

217 Returns: 

218 bool: True if the function is constant, False otherwise. 

219 """ 

220 return self.onefun.isconst 

221 

222 @property 

223 def iscomplex(self): 

224 """Check if this function has complex values. 

225 

226 This property determines whether the function has complex values or is 

227 purely real-valued, delegating to the underlying onefun object. 

228 

229 Returns: 

230 bool: True if the function has complex values, False otherwise. 

231 """ 

232 return self.onefun.iscomplex 

233 

234 @property 

235 def isempty(self): 

236 """Check if this function is empty. 

237 

238 This property determines whether the function is empty, which is a special 

239 state used as a placeholder or for special cases, delegating to the 

240 underlying onefun object. 

241 

242 Returns: 

243 bool: True if the function is empty, False otherwise. 

244 """ 

245 return self.onefun.isempty 

246 

247 @property 

248 def size(self): 

249 """Get the size of the function representation. 

250 

251 This property returns the number of coefficients or other measure of the 

252 complexity of the function representation, delegating to the underlying 

253 onefun object. 

254 

255 Returns: 

256 int: The size of the function representation. 

257 """ 

258 return self.onefun.size 

259 

260 @property 

261 def support(self): 

262 """Get the support interval of this function. 

263 

264 This property returns the interval on which this function is defined, 

265 represented as a numpy array with two elements [a, b]. 

266 

267 Returns: 

268 numpy.ndarray: Array containing the endpoints of the interval. 

269 """ 

270 return np.asarray(self.interval) 

271 

272 @property 

273 def vscale(self): 

274 """Get the vertical scale of the function. 

275 

276 This property returns a measure of the range of function values, typically 

277 the maximum absolute value of the function on its interval of definition, 

278 delegating to the underlying onefun object. 

279 

280 Returns: 

281 float: The vertical scale of the function. 

282 """ 

283 return self.onefun.vscale 

284 

285 # ----------- 

286 # utilities 

287 # ----------- 

288 

289 def imag(self): 

290 """Get the imaginary part of this function. 

291 

292 This method returns a new function representing the imaginary part of this function. 

293 If this function is real-valued, returns a zero function. 

294 

295 Returns: 

296 Classicfun: A new function representing the imaginary part of this function. 

297 """ 

298 if self.iscomplex: 

299 return self.__class__(self.onefun.imag(), self.interval) 

300 else: 

301 return self.initconst(0, interval=self.interval) 

302 

303 def real(self): 

304 """Get the real part of this function. 

305 

306 This method returns a new function representing the real part of this function. 

307 If this function is already real-valued, returns this function. 

308 

309 Returns: 

310 Classicfun: A new function representing the real part of this function. 

311 """ 

312 if self.iscomplex: 

313 return self.__class__(self.onefun.real(), self.interval) 

314 else: 

315 return self 

316 

317 def restrict(self, subinterval): 

318 """Restrict this function to a subinterval. 

319 

320 This method creates a new function that is the restriction of this function 

321 to the specified subinterval. The output is formed using a fixed length 

322 construction with the same number of degrees of freedom as the original function. 

323 

324 Args: 

325 subinterval (array-like): The subinterval to which this function should be restricted. 

326 Must be contained within the original interval of definition. 

327 

328 Returns: 

329 Classicfun: A new function representing the restriction of this function to the subinterval. 

330 

331 Raises: 

332 NotSubinterval: If the subinterval is not contained within the original interval. 

333 """ 

334 if subinterval not in self.interval: # pragma: no cover 

335 raise NotSubinterval(self.interval, subinterval) 

336 if self.interval == subinterval: 

337 return self 

338 else: 

339 return self.__class__.initfun_fixedlen(self, subinterval, self.size) 

340 

341 def translate(self, c): 

342 """Translate this function by a constant c. 

343 

344 This method creates a new function g(x) = f(x-c), which is the original 

345 function translated horizontally by c. 

346 

347 Args: 

348 c (float): The amount by which to translate the function. 

349 

350 Returns: 

351 Classicfun: A new function representing g(x) = f(x-c). 

352 """ 

353 return self.__class__(self.onefun, self.interval + c) 

354 

355 # ------------- 

356 # rootfinding 

357 # ------------- 

358 def roots(self): 

359 """Find the roots (zeros) of the function on its interval of definition. 

360 

361 This method computes the points where the function equals zero 

362 within its interval of definition by finding the roots of the 

363 underlying onefun and mapping them to the function's interval. 

364 

365 Returns: 

366 numpy.ndarray: An array of the roots of the function in its interval of definition, 

367 sorted in ascending order. 

368 """ 

369 uroots = self.onefun.roots() 

370 return self.interval(uroots) 

371 

372 # ---------- 

373 # calculus 

374 # ---------- 

375 def cumsum(self): 

376 """Compute the indefinite integral of the function. 

377 

378 This method calculates the indefinite integral (antiderivative) of the function, 

379 with the constant of integration chosen so that the indefinite integral 

380 evaluates to 0 at the left endpoint of the interval. 

381 

382 Returns: 

383 Classicfun: A new function representing the indefinite integral of this function. 

384 """ 

385 a, b = self.support 

386 onefun = 0.5 * (b - a) * self.onefun.cumsum() 

387 return self.__class__(onefun, self.interval) 

388 

389 def diff(self): 

390 """Compute the derivative of the function. 

391 

392 This method calculates the derivative of the function with respect to x, 

393 applying the chain rule to account for the mapping between the standard 

394 domain [-1, 1] and the function's interval. 

395 

396 Returns: 

397 Classicfun: A new function representing the derivative of this function. 

398 """ 

399 a, b = self.support 

400 onefun = 2.0 / (b - a) * self.onefun.diff() 

401 return self.__class__(onefun, self.interval) 

402 

403 def sum(self): 

404 """Compute the definite integral of the function over its interval of definition. 

405 

406 This method calculates the definite integral of the function 

407 over its interval of definition [a, b], applying the appropriate 

408 scaling factor to account for the mapping from [-1, 1]. 

409 

410 Returns: 

411 float or complex: The definite integral of the function over its interval of definition. 

412 """ 

413 a, b = self.support 

414 return 0.5 * (b - a) * self.onefun.sum() 

415 

416 # ---------- 

417 # plotting 

418 # ---------- 

419 def plot(self, ax=None, **kwds): 

420 """Plot the function over its interval of definition. 

421 

422 This method plots the function over its interval of definition using matplotlib. 

423 For complex-valued functions, it plots the real part against the imaginary part. 

424 

425 Args: 

426 ax (matplotlib.axes.Axes, optional): The axes on which to plot. If None, 

427 a new axes will be created. Defaults to None. 

428 **kwds: Additional keyword arguments to pass to matplotlib's plot function. 

429 

430 Returns: 

431 matplotlib.axes.Axes: The axes on which the plot was created. 

432 """ 

433 return plotfun(self, self.support, ax=ax, **kwds) 

434 

435 

436# ---------------------------------------------------------------- 

437# methods that execute the corresponding onefun method as is 

438# ---------------------------------------------------------------- 

439 

440methods_onefun_other = ("values", "plotcoeffs") 

441 

442 

443def add_utility(methodname): 

444 """Add a utility method to the Classicfun class. 

445 

446 This function creates a method that delegates to the corresponding method 

447 of the underlying onefun object and adds it to the Classicfun class. 

448 

449 Args: 

450 methodname (str): The name of the method to add. 

451 

452 Note: 

453 The created method will have the same name and signature as the 

454 corresponding method in the onefun object. 

455 """ 

456 

457 def method(self, *args, **kwds): 

458 """Delegate to the corresponding method of the underlying onefun object. 

459 

460 This method calls the same-named method on the underlying onefun object 

461 and returns its result. 

462 

463 Args: 

464 self (Classicfun): The Classicfun object. 

465 *args: Variable length argument list to pass to the onefun method. 

466 **kwds: Arbitrary keyword arguments to pass to the onefun method. 

467 

468 Returns: 

469 The return value from the corresponding onefun method. 

470 """ 

471 return getattr(self.onefun, methodname)(*args, **kwds) 

472 

473 method.__name__ = methodname 

474 method.__doc__ = method.__doc__ 

475 setattr(Classicfun, methodname, method) 

476 

477 

478for methodname in methods_onefun_other: 

479 if methodname[:4] == "plot" and plt is None: # pragma: no cover 

480 continue 

481 add_utility(methodname) 

482 

483 

484# ----------------------------------------------------------------------- 

485# unary operators and zero-argument utlity methods returning a onefun 

486# ----------------------------------------------------------------------- 

487 

488methods_onefun_zeroargs = ("__pos__", "__neg__", "copy", "simplify") 

489 

490 

491def add_zero_arg_op(methodname): 

492 """Add a zero-argument operation method to the Classicfun class. 

493 

494 This function creates a method that delegates to the corresponding method 

495 of the underlying onefun object and wraps the result in a new Classicfun 

496 instance with the same interval. 

497 

498 Args: 

499 methodname (str): The name of the method to add. 

500 

501 Note: 

502 The created method will have the same name and signature as the 

503 corresponding method in the onefun object, but will return a Classicfun 

504 instance instead of an onefun instance. 

505 """ 

506 

507 def method(self, *args, **kwds): 

508 """Apply a zero-argument operation and return a new Classicfun. 

509 

510 This method calls the same-named method on the underlying onefun object 

511 and wraps the result in a new Classicfun instance with the same interval. 

512 

513 Args: 

514 self (Classicfun): The Classicfun object. 

515 *args: Variable length argument list to pass to the onefun method. 

516 **kwds: Arbitrary keyword arguments to pass to the onefun method. 

517 

518 Returns: 

519 Classicfun: A new Classicfun instance with the result of the operation. 

520 """ 

521 onefun = getattr(self.onefun, methodname)(*args, **kwds) 

522 return self.__class__(onefun, self.interval) 

523 

524 method.__name__ = methodname 

525 method.__doc__ = method.__doc__ 

526 setattr(Classicfun, methodname, method) 

527 

528 

529for methodname in methods_onefun_zeroargs: 

530 add_zero_arg_op(methodname) 

531 

532# ----------------------------------------- 

533# binary operators returning a onefun 

534# ----------------------------------------- 

535 

536# ToDo: change these to operator module methods 

537methods_onefun_binary = ( 

538 "__add__", 

539 "__div__", 

540 "__mul__", 

541 "__pow__", 

542 "__radd__", 

543 "__rdiv__", 

544 "__rmul__", 

545 "__rpow__", 

546 "__rsub__", 

547 "__rtruediv__", 

548 "__sub__", 

549 "__truediv__", 

550) 

551 

552 

553def add_binary_op(methodname): 

554 """Add a binary operation method to the Classicfun class. 

555 

556 This function creates a method that implements a binary operation between 

557 two Classicfun objects or between a Classicfun and a scalar. It delegates 

558 to the corresponding method of the underlying onefun object and wraps the 

559 result in a new Classicfun instance with the same interval. 

560 

561 Args: 

562 methodname (str): The name of the binary operation method to add. 

563 

564 Note: 

565 The created method will check that both Classicfun objects have the 

566 same interval before performing the operation. If one operand is not 

567 a Classicfun, it will be passed directly to the onefun method. 

568 """ 

569 

570 @self_empty() 

571 def method(self, f, *args, **kwds): 

572 """Apply a binary operation and return a new Classicfun. 

573 

574 This method implements a binary operation between this Classicfun and 

575 another object (either another Classicfun or a scalar). It delegates 

576 to the corresponding method of the underlying onefun object and wraps 

577 the result in a new Classicfun instance with the same interval. 

578 

579 Args: 

580 self (Classicfun): The Classicfun object. 

581 f (Classicfun or scalar): The second operand of the binary operation. 

582 *args: Variable length argument list to pass to the onefun method. 

583 **kwds: Arbitrary keyword arguments to pass to the onefun method. 

584 

585 Returns: 

586 Classicfun: A new Classicfun instance with the result of the operation. 

587 

588 Raises: 

589 IntervalMismatch: If f is a Classicfun with a different interval. 

590 """ 

591 cls = self.__class__ 

592 if isinstance(f, cls): 

593 # TODO: as in ChebTech, is a decorator apporach here better? 

594 if f.isempty: 

595 return f.copy() 

596 g = f.onefun 

597 # raise Exception if intervals are not consistent 

598 if self.interval != f.interval: # pragma: no cover 

599 raise IntervalMismatch(self.interval, f.interval) 

600 else: 

601 # let the lower level classes raise any other exceptions 

602 g = f 

603 onefun = getattr(self.onefun, methodname)(g, *args, **kwds) 

604 return cls(onefun, self.interval) 

605 

606 method.__name__ = methodname 

607 method.__doc__ = method.__doc__ 

608 setattr(Classicfun, methodname, method) 

609 

610 

611for methodname in methods_onefun_binary: 

612 add_binary_op(methodname) 

613 

614# --------------------------- 

615# numpy universal functions 

616# --------------------------- 

617 

618 

619def add_ufunc(op): 

620 """Add a NumPy universal function method to the Classicfun class. 

621 

622 This function creates a method that applies a NumPy universal function (ufunc) 

623 to the values of a Classicfun and returns a new Classicfun representing the result. 

624 

625 Args: 

626 op (callable): The NumPy universal function to apply. 

627 

628 Note: 

629 The created method will have the same name as the NumPy function 

630 and will take no arguments other than self. 

631 """ 

632 

633 @self_empty() 

634 def method(self): 

635 """Apply a NumPy universal function to this function. 

636 

637 This method applies a NumPy universal function (ufunc) to the values 

638 of this function and returns a new function representing the result. 

639 

640 Returns: 

641 Classicfun: A new function representing op(f(x)). 

642 """ 

643 return self.__class__.initfun_adaptive(lambda x: op(self(x)), self.interval) 

644 

645 name = op.__name__ 

646 method.__name__ = name 

647 method.__doc__ = method.__doc__ 

648 setattr(Classicfun, name, method) 

649 

650 

651ufuncs = ( 

652 np.absolute, 

653 np.arccos, 

654 np.arccosh, 

655 np.arcsin, 

656 np.arcsinh, 

657 np.arctan, 

658 np.arctanh, 

659 np.cos, 

660 np.cosh, 

661 np.exp, 

662 np.exp2, 

663 np.expm1, 

664 np.log, 

665 np.log2, 

666 np.log10, 

667 np.log1p, 

668 np.sinh, 

669 np.sin, 

670 np.tan, 

671 np.tanh, 

672 np.sqrt, 

673) 

674 

675for op in ufuncs: 

676 add_ufunc(op)