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

163 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 07:22 +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 

8from typing import Any 

9 

10import matplotlib.pyplot as plt 

11import numpy as np 

12 

13from .chebtech import Chebtech 

14from .decorators import self_empty 

15from .exceptions import IntervalMismatch, NotSubinterval 

16from .fun import Fun 

17from .plotting import plotfun 

18from .settings import _preferences as prefs 

19from .trigtech import Trigtech 

20from .utilities import Interval, IntervalMap 

21 

22techdict = { 

23 "Chebtech": Chebtech, 

24 "Trigtech": Trigtech, 

25} 

26 

27 

28class Classicfun(Fun, ABC): 

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

30 

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

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

33 (such as Chebtech) on that standard domain. 

34 

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

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

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

38 """ 

39 

40 # ``_singularity_priority`` lets mixed-type binary operations dispatch 

41 # to the more "singular" representation when two ``Classicfun`` 

42 # subclasses meet on the same interval. Higher wins. ``Bndfun`` and 

43 # ``CompactFun`` use the default of ``0``; ``Singfun`` overrides to 

44 # ``10`` so that ``Singfun + Bndfun`` yields a ``Singfun``. 

45 _singularity_priority: int = 0 

46 

47 # -------------------------- 

48 # alternative constructors 

49 # -------------------------- 

50 @classmethod 

51 def initempty(cls) -> "Classicfun": 

52 """Initialize an empty function. 

53 

54 This constructor creates an empty function representation, which is 

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

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

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

58 

59 Returns: 

60 Classicfun: A new empty instance. 

61 """ 

62 interval = Interval() 

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

64 return cls(onefun, interval) 

65 

66 @classmethod 

67 def initconst(cls, c: Any, interval: Any) -> "Classicfun": 

68 """Initialize a constant function. 

69 

70 This constructor creates a function that represents a constant value 

71 on the specified interval. 

72 

73 Args: 

74 c: The constant value. 

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

76 

77 Returns: 

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

79 """ 

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

81 return cls(onefun, interval) 

82 

83 @classmethod 

84 def initidentity(cls, interval: Any) -> "Classicfun": 

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

86 

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

88 on the specified interval. 

89 

90 Args: 

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

92 

93 Returns: 

94 Classicfun: A new instance representing the identity function. 

95 """ 

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

97 return cls(onefun, interval) 

98 

99 @classmethod 

100 def initfun_adaptive(cls, f: Any, interval: Any) -> "Classicfun": 

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

102 

103 This constructor determines the appropriate number of points needed to 

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

105 

106 Args: 

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

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

109 

110 Returns: 

111 Classicfun: A new instance representing the function f. 

112 """ 

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

114 return cls(onefun, interval) 

115 

116 @classmethod 

117 def initfun_fixedlen(cls, f: Any, interval: Any, n: int) -> "Classicfun": 

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

119 

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

121 rather than determining the number adaptively. 

122 

123 Args: 

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

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

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

127 

128 Returns: 

129 Classicfun: A new instance representing the function f. 

130 """ 

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

132 return cls(onefun, interval) 

133 

134 # ------------------- 

135 # 'private' methods 

136 # ------------------- 

137 def __call__(self, x: Any, how: str = "clenshaw") -> Any: 

138 """Evaluate the function at points x. 

139 

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

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

142 

143 Args: 

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

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

146 

147 Returns: 

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

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

150 """ 

151 y = self.map.invmap(x) 

152 return self.onefun(y, how) 

153 

154 def __init__(self, onefun: Any, interval: Any) -> None: 

155 """Initialize a new Classicfun instance. 

156 

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

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

159 

160 Args: 

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

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

163 """ 

164 self.onefun = onefun 

165 self._interval = interval 

166 

167 def _rebuild(self, onefun: Any) -> "Classicfun": 

168 """Construct a new instance of this class with a replacement ``onefun``. 

169 

170 Subclasses that carry additional metadata beyond ``onefun`` and 

171 ``interval`` (e.g. :class:`CompactFun`'s logical interval) should 

172 override this method so that operations defined on the parent class 

173 preserve that metadata. 

174 

175 Args: 

176 onefun: The replacement Onefun object. 

177 

178 Returns: 

179 Classicfun: A new instance of ``type(self)``. 

180 """ 

181 return self.__class__(onefun, self._interval) 

182 

183 def _can_share_onefun_with(self, other: "Classicfun") -> bool: 

184 """Return True if ``self`` and ``other`` represent functions on the same t-grid. 

185 

186 Two ``Classicfun`` instances can share onefun-level arithmetic when 

187 they have the same concrete subclass, the same logical interval, and 

188 the same map (so the underlying ``Onefun`` coefficients refer to the 

189 same Chebyshev nodes in ``t``-space). The default implementation 

190 compares only the type and the interval, which is correct for the 

191 affine-mapped subclasses (:class:`Bndfun`, :class:`CompactFun`). 

192 :class:`~chebpy.singfun.Singfun` overrides this to additionally 

193 compare maps. 

194 """ 

195 return type(self) is type(other) and self._interval == other._interval 

196 

197 def _rebuild_from_callable(self, f: Any) -> "Classicfun": 

198 """Adaptively rebuild a fun of this type evaluating callable ``f``. 

199 

200 Used by mixed-type binary operations to reconstruct the result on the 

201 dominant operand's representation. Subclasses with extra metadata 

202 (e.g. :class:`~chebpy.singfun.Singfun`'s map) override this. 

203 """ 

204 return type(self).initfun_adaptive(f, self._interval) 

205 

206 def __repr__(self) -> str: # pragma: no cover 

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

208 

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

210 the class name, support interval, and size. 

211 

212 Returns: 

213 str: A string representation of the function. 

214 """ 

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

216 return out 

217 

218 # ------------ 

219 # properties 

220 # ------------ 

221 @property 

222 def coeffs(self) -> Any: 

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

224 

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

226 delegating to the underlying onefun object. 

227 

228 Returns: 

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

230 """ 

231 return self.onefun.coeffs 

232 

233 @property 

234 def endvalues(self) -> Any: 

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

236 

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

238 of definition. 

239 

240 Returns: 

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

242 of the interval [a, b]. 

243 """ 

244 return self.__call__(self.support) 

245 

246 @property 

247 def interval(self) -> Any: 

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

249 

250 This property returns the interval object representing the domain 

251 of definition for this function. 

252 

253 Returns: 

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

255 """ 

256 return self._interval 

257 

258 @property 

259 def map(self) -> IntervalMap: 

260 """Return the bijective map between [-1, 1] and the function's interval. 

261 

262 Subclasses backed by a non-affine map (e.g. endpoint-clustering 

263 transforms for endpoint singularities) override this to return a 

264 different :class:`~chebpy.utilities.IntervalMap` implementer while 

265 keeping ``self._interval`` as the logical support endpoints. 

266 

267 Returns: 

268 IntervalMap: The map used to relate reference points ``y ∈ [-1, 1]`` 

269 to logical points ``x ∈ [a, b]``. Defaults to ``self._interval``, 

270 which is the affine :class:`~chebpy.utilities.Interval` map. 

271 """ 

272 return self._interval 

273 

274 @property 

275 def isconst(self) -> Any: 

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

277 

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

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

280 underlying onefun object. 

281 

282 Returns: 

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

284 """ 

285 return self.onefun.isconst 

286 

287 @property 

288 def iscomplex(self) -> Any: 

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

290 

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

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

293 

294 Returns: 

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

296 """ 

297 return self.onefun.iscomplex 

298 

299 @property 

300 def isempty(self) -> Any: 

301 """Check if this function is empty. 

302 

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

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

305 underlying onefun object. 

306 

307 Returns: 

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

309 """ 

310 return self.onefun.isempty 

311 

312 @property 

313 def size(self) -> Any: 

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

315 

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

317 complexity of the function representation, delegating to the underlying 

318 onefun object. 

319 

320 Returns: 

321 int: The size of the function representation. 

322 """ 

323 return self.onefun.size 

324 

325 @property 

326 def support(self) -> Any: 

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

328 

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

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

331 

332 Returns: 

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

334 """ 

335 return np.asarray(self.interval) 

336 

337 @property 

338 def vscale(self) -> Any: 

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

340 

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

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

343 delegating to the underlying onefun object. 

344 

345 Returns: 

346 float: The vertical scale of the function. 

347 """ 

348 return self.onefun.vscale 

349 

350 # ----------- 

351 # utilities 

352 # ----------- 

353 

354 def imag(self) -> "Classicfun": 

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

356 

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

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

359 

360 Returns: 

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

362 """ 

363 if self.iscomplex: 

364 return self._rebuild(self.onefun.imag()) 

365 else: 

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

367 

368 def real(self) -> "Classicfun": 

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

370 

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

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

373 

374 Returns: 

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

376 """ 

377 if self.iscomplex: 

378 return self._rebuild(self.onefun.real()) 

379 else: 

380 return self 

381 

382 def restrict(self, subinterval: Any) -> "Classicfun": 

383 """Restrict this function to a subinterval. 

384 

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

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

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

388 

389 Args: 

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

391 Must be contained within the original interval of definition. 

392 

393 Returns: 

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

395 

396 Raises: 

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

398 """ 

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

400 raise NotSubinterval(self.interval, subinterval) 

401 if self.interval == subinterval: 

402 return self 

403 else: 

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

405 

406 def translate(self, c: float) -> "Classicfun": 

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

408 

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

410 function translated horizontally by c. 

411 

412 Args: 

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

414 

415 Returns: 

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

417 """ 

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

419 

420 # ------------- 

421 # rootfinding 

422 # ------------- 

423 def roots(self) -> Any: 

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

425 

426 This method computes the points where the function equals zero 

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

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

429 

430 Returns: 

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

432 sorted in ascending order. 

433 """ 

434 uroots = self.onefun.roots() 

435 return self.map.formap(uroots) 

436 

437 # ---------- 

438 # calculus 

439 # ---------- 

440 def cumsum(self) -> "Classicfun": 

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

442 

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

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

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

446 

447 Returns: 

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

449 """ 

450 a, b = self.interval 

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

452 return self._rebuild(onefun) 

453 

454 def diff(self) -> "Classicfun": 

455 """Compute the derivative of the function. 

456 

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

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

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

460 

461 Returns: 

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

463 """ 

464 a, b = self.interval 

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

466 return self._rebuild(onefun) 

467 

468 def sum(self) -> Any: 

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

470 

471 This method calculates the definite integral of the function 

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

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

474 

475 Returns: 

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

477 """ 

478 a, b = self.interval 

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

480 

481 # ---------- 

482 # plotting 

483 # ---------- 

484 def plot(self, ax: Any = None, **kwds: Any) -> Any: 

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

486 

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

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

489 

490 Args: 

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

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

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

494 

495 Returns: 

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

497 """ 

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

499 

500 

501# ---------------------------------------------------------------- 

502# methods that execute the corresponding onefun method as is 

503# ---------------------------------------------------------------- 

504 

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

506 

507 

508def add_utility(methodname: str) -> None: 

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

510 

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

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

513 

514 Args: 

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

516 

517 Note: 

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

519 corresponding method in the onefun object. 

520 """ 

521 

522 def method(self: Any, *args: Any, **kwds: Any) -> Any: 

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

524 

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

526 and returns its result. 

527 

528 Args: 

529 self (Classicfun): The Classicfun object. 

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

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

532 

533 Returns: 

534 The return value from the corresponding onefun method. 

535 """ 

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

537 

538 method.__name__ = methodname 

539 method.__doc__ = method.__doc__ 

540 setattr(Classicfun, methodname, method) 

541 

542 

543for methodname in methods_onefun_other: 

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

545 continue 

546 add_utility(methodname) 

547 

548 

549# ----------------------------------------------------------------------- 

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

551# ----------------------------------------------------------------------- 

552 

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

554 

555 

556def add_zero_arg_op(methodname: str) -> None: 

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

558 

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

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

561 instance with the same interval. 

562 

563 Args: 

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

565 

566 Note: 

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

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

569 instance instead of an onefun instance. 

570 """ 

571 

572 def method(self: Any, *args: Any, **kwds: Any) -> Any: 

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

574 

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

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

577 

578 Args: 

579 self (Classicfun): The Classicfun object. 

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

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

582 

583 Returns: 

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

585 """ 

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

587 return self._rebuild(onefun) 

588 

589 method.__name__ = methodname 

590 method.__doc__ = method.__doc__ 

591 setattr(Classicfun, methodname, method) 

592 

593 

594for methodname in methods_onefun_zeroargs: 

595 add_zero_arg_op(methodname) 

596 

597# ----------------------------------------- 

598# binary operators returning a onefun 

599# ----------------------------------------- 

600 

601# Map from dunder method name to the corresponding callable acting on raw 

602# values. Used by the mixed-subclass binary-op fallback to reconstruct the 

603# result adaptively on the dominant operand's representation. 

604_BINOP_OPERATORS: dict[str, Any] = { 

605 "__add__": lambda a, b: a + b, 

606 "__sub__": lambda a, b: a - b, 

607 "__mul__": lambda a, b: a * b, 

608 "__truediv__": lambda a, b: a / b, 

609 "__div__": lambda a, b: a / b, 

610 "__pow__": lambda a, b: a**b, 

611 "__radd__": lambda a, b: b + a, 

612 "__rsub__": lambda a, b: b - a, 

613 "__rmul__": lambda a, b: b * a, 

614 "__rtruediv__": lambda a, b: b / a, 

615 "__rdiv__": lambda a, b: b / a, 

616 "__rpow__": lambda a, b: b**a, 

617} 

618 

619 

620def _classicfun_mixed_binop(self: "Classicfun", other: "Classicfun", methodname: str) -> "Classicfun": 

621 """Reconstruct a same-interval, mixed-subclass binary op on the dominant operand. 

622 

623 When two :class:`Classicfun` instances of different subclasses (or 

624 same subclass but with maps that disagree) meet on the same logical 

625 interval, neither's onefun-level arithmetic is correct. Pick the 

626 operand with higher ``_singularity_priority`` and rebuild the 

627 composition adaptively in its representation. Ties go to ``self``. 

628 """ 

629 op_fn = _BINOP_OPERATORS[methodname] 

630 owner = self if self._singularity_priority >= other._singularity_priority else other 

631 

632 def combined(x: Any) -> Any: 

633 return op_fn(self(x), other(x)) 

634 

635 return owner._rebuild_from_callable(combined) 

636 

637 

638# ToDo: change these to operator module methods 

639methods_onefun_binary = ( 

640 "__add__", 

641 "__div__", 

642 "__mul__", 

643 "__pow__", 

644 "__radd__", 

645 "__rdiv__", 

646 "__rmul__", 

647 "__rpow__", 

648 "__rsub__", 

649 "__rtruediv__", 

650 "__sub__", 

651 "__truediv__", 

652) 

653 

654 

655def add_binary_op(methodname: str) -> None: 

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

657 

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

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

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

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

662 

663 Args: 

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

665 

666 Note: 

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

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

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

670 """ 

671 

672 @self_empty() 

673 def method(self: Any, f: Any, *args: Any, **kwds: Any) -> Any: 

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

675 

676 This method implements a binary operation between this Classicfun and 

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

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

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

680 

681 Args: 

682 self (Classicfun): The Classicfun object. 

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

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

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

686 

687 Returns: 

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

689 

690 Raises: 

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

692 """ 

693 if isinstance(f, Classicfun): 

694 if f.isempty: 

695 return f.copy() 

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

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

698 if not self._can_share_onefun_with(f): 

699 # Mixed subclasses (or same subclass with disagreeing maps): 

700 # rebuild adaptively on the dominant operand's representation. 

701 return _classicfun_mixed_binop(self, f, methodname) 

702 g = f.onefun 

703 else: 

704 # let the lower level classes raise any other exceptions 

705 g = f 

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

707 return self._rebuild(onefun) 

708 

709 method.__name__ = methodname 

710 method.__doc__ = method.__doc__ 

711 setattr(Classicfun, methodname, method) 

712 

713 

714for methodname in methods_onefun_binary: 

715 add_binary_op(methodname) 

716 

717# --------------------------- 

718# numpy universal functions 

719# --------------------------- 

720 

721 

722def add_ufunc(op: Any) -> None: 

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

724 

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

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

727 

728 Args: 

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

730 

731 Note: 

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

733 and will take no arguments other than self. 

734 """ 

735 

736 @self_empty() 

737 def method(self: Any) -> Any: 

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

739 

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

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

742 

743 Returns: 

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

745 """ 

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

747 

748 name = op.__name__ 

749 method.__name__ = name 

750 method.__doc__ = method.__doc__ 

751 setattr(Classicfun, name, method) 

752 

753 

754ufuncs = ( 

755 np.absolute, 

756 np.arccos, 

757 np.arccosh, 

758 np.arcsin, 

759 np.arcsinh, 

760 np.arctan, 

761 np.arctanh, 

762 np.ceil, 

763 np.cos, 

764 np.cosh, 

765 np.exp, 

766 np.exp2, 

767 np.expm1, 

768 np.floor, 

769 np.log, 

770 np.log2, 

771 np.log10, 

772 np.log1p, 

773 np.sign, 

774 np.sinh, 

775 np.sin, 

776 np.tan, 

777 np.tanh, 

778 np.sqrt, 

779) 

780 

781for op in ufuncs: 

782 add_ufunc(op)