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

163 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-22 21:50 +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 TYPE_CHECKING, Any, cast 

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 if TYPE_CHECKING: 

48 # The algebra/utility methods below are attached to ``Classicfun`` at 

49 # import time by the ``setattr`` blocks further down (they delegate to 

50 # the underlying ``onefun``). They satisfy the abstract methods declared 

51 # on :class:`Fun`; declaring them here lets static type checkers see the 

52 # concrete implementations, so subclasses such as ``Bndfun`` are 

53 # treated as instantiable and ``super().__op__()`` calls resolve safely. 

54 def __add__(self, other: Any) -> Fun: 

55 """Add another function or scalar (dynamically attached).""" 

56 ... 

57 

58 def __sub__(self, other: Any) -> Fun: 

59 """Subtract another function or scalar (dynamically attached).""" 

60 ... 

61 

62 def __mul__(self, other: Any) -> Fun: 

63 """Multiply by another function or scalar (dynamically attached).""" 

64 ... 

65 

66 def __pow__(self, power: Any) -> Fun: 

67 """Raise to a power (dynamically attached).""" 

68 ... 

69 

70 def __radd__(self, other: Any) -> Fun: 

71 """Right-hand addition (dynamically attached).""" 

72 ... 

73 

74 def __rsub__(self, other: Any) -> Fun: 

75 """Right-hand subtraction (dynamically attached).""" 

76 ... 

77 

78 def __rmul__(self, other: Any) -> Fun: 

79 """Right-hand multiplication (dynamically attached).""" 

80 ... 

81 

82 def __neg__(self) -> Fun: 

83 """Negate this function (dynamically attached).""" 

84 ... 

85 

86 def __pos__(self) -> Fun: 

87 """Return this function unchanged (dynamically attached).""" 

88 ... 

89 

90 def copy(self) -> Fun: 

91 """Return a deep copy (dynamically attached).""" 

92 ... 

93 

94 def simplify(self) -> Fun: 

95 """Return a simplified representation (dynamically attached).""" 

96 ... 

97 

98 def values(self) -> np.ndarray: 

99 """Return the function values at the representation points (dynamically attached).""" 

100 ... 

101 

102 # -------------------------- 

103 # alternative constructors 

104 # -------------------------- 

105 @classmethod 

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

107 """Initialize an empty function. 

108 

109 This constructor creates an empty function representation, which is 

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

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

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

113 

114 Returns: 

115 Classicfun: A new empty instance. 

116 """ 

117 interval = Interval() 

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

119 return cls(onefun, interval) 

120 

121 @classmethod 

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

123 """Initialize a constant function. 

124 

125 This constructor creates a function that represents a constant value 

126 on the specified interval. 

127 

128 Args: 

129 c: The constant value. 

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

131 

132 Returns: 

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

134 """ 

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

136 return cls(onefun, interval) 

137 

138 @classmethod 

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

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

141 

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

143 on the specified interval. 

144 

145 Args: 

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

147 

148 Returns: 

149 Classicfun: A new instance representing the identity function. 

150 """ 

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

152 return cls(onefun, interval) 

153 

154 @classmethod 

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

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

157 

158 This constructor determines the appropriate number of points needed to 

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

160 

161 Args: 

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

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

164 

165 Returns: 

166 Classicfun: A new instance representing the function f. 

167 """ 

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

169 return cls(onefun, interval) 

170 

171 @classmethod 

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

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

174 

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

176 rather than determining the number adaptively. 

177 

178 Args: 

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

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

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

182 

183 Returns: 

184 Classicfun: A new instance representing the function f. 

185 """ 

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

187 return cls(onefun, interval) 

188 

189 # ------------------- 

190 # 'private' methods 

191 # ------------------- 

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

193 """Evaluate the function at points x. 

194 

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

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

197 

198 Args: 

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

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

201 

202 Returns: 

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

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

205 """ 

206 y = self.map.invmap(x) 

207 return self.onefun(y, how) 

208 

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

210 """Initialize a new Classicfun instance. 

211 

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

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

214 

215 Args: 

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

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

218 """ 

219 self.onefun = onefun 

220 self._interval = interval 

221 

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

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

224 

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

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

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

228 preserve that metadata. 

229 

230 Args: 

231 onefun: The replacement Onefun object. 

232 

233 Returns: 

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

235 """ 

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

237 

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

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

240 

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

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

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

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

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

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

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

248 compare maps. 

249 """ 

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

251 

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

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

254 

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

256 dominant operand's representation. Subclasses with extra metadata 

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

258 """ 

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

260 

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

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

263 

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

265 the class name, support interval, and size. 

266 

267 Returns: 

268 str: A string representation of the function. 

269 """ 

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

271 return out 

272 

273 # ------------ 

274 # properties 

275 # ------------ 

276 @property 

277 def coeffs(self) -> Any: 

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

279 

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

281 delegating to the underlying onefun object. 

282 

283 Returns: 

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

285 """ 

286 return self.onefun.coeffs 

287 

288 @property 

289 def endvalues(self) -> Any: 

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

291 

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

293 of definition. 

294 

295 Returns: 

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

297 of the interval [a, b]. 

298 """ 

299 return self.__call__(self.support) 

300 

301 @property 

302 def interval(self) -> Any: 

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

304 

305 This property returns the interval object representing the domain 

306 of definition for this function. 

307 

308 Returns: 

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

310 """ 

311 return self._interval 

312 

313 @property 

314 def map(self) -> IntervalMap: 

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

316 

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

318 transforms for endpoint singularities) override this to return a 

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

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

321 

322 Returns: 

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

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

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

326 """ 

327 return cast(IntervalMap, self._interval) 

328 

329 @property 

330 def isconst(self) -> Any: 

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

332 

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

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

335 underlying onefun object. 

336 

337 Returns: 

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

339 """ 

340 return self.onefun.isconst 

341 

342 @property 

343 def iscomplex(self) -> Any: 

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

345 

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

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

348 

349 Returns: 

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

351 """ 

352 return self.onefun.iscomplex 

353 

354 @property 

355 def isempty(self) -> Any: 

356 """Check if this function is empty. 

357 

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

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

360 underlying onefun object. 

361 

362 Returns: 

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

364 """ 

365 return self.onefun.isempty 

366 

367 @property 

368 def size(self) -> Any: 

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

370 

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

372 complexity of the function representation, delegating to the underlying 

373 onefun object. 

374 

375 Returns: 

376 int: The size of the function representation. 

377 """ 

378 return self.onefun.size 

379 

380 @property 

381 def support(self) -> Any: 

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

383 

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

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

386 

387 Returns: 

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

389 """ 

390 return np.asarray(self.interval) 

391 

392 @property 

393 def vscale(self) -> Any: 

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

395 

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

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

398 delegating to the underlying onefun object. 

399 

400 Returns: 

401 float: The vertical scale of the function. 

402 """ 

403 return self.onefun.vscale 

404 

405 # ----------- 

406 # utilities 

407 # ----------- 

408 

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

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

411 

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

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

414 

415 Returns: 

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

417 """ 

418 if self.iscomplex: 

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

420 else: 

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

422 

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

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

425 

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

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

428 

429 Returns: 

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

431 """ 

432 if self.iscomplex: 

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

434 else: 

435 return self 

436 

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

438 """Restrict this function to a subinterval. 

439 

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

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

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

443 

444 Args: 

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

446 Must be contained within the original interval of definition. 

447 

448 Returns: 

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

450 

451 Raises: 

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

453 """ 

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

455 raise NotSubinterval(self.interval, subinterval) 

456 if self.interval == subinterval: 

457 return self 

458 else: 

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

460 

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

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

463 

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

465 function translated horizontally by c. 

466 

467 Args: 

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

469 

470 Returns: 

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

472 """ 

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

474 

475 # ------------- 

476 # rootfinding 

477 # ------------- 

478 def roots(self) -> Any: 

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

480 

481 This method computes the points where the function equals zero 

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

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

484 

485 Returns: 

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

487 sorted in ascending order. 

488 """ 

489 uroots = self.onefun.roots() 

490 return self.map.formap(uroots) 

491 

492 # ---------- 

493 # calculus 

494 # ---------- 

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

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

497 

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

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

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

501 

502 Returns: 

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

504 """ 

505 a, b = self.interval 

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

507 return self._rebuild(onefun) 

508 

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

510 """Compute the derivative of the function. 

511 

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

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

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

515 

516 Returns: 

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

518 """ 

519 a, b = self.interval 

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

521 return self._rebuild(onefun) 

522 

523 def sum(self) -> Any: 

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

525 

526 This method calculates the definite integral of the function 

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

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

529 

530 Returns: 

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

532 """ 

533 a, b = self.interval 

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

535 

536 # ---------- 

537 # plotting 

538 # ---------- 

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

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

541 

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

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

544 

545 Args: 

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

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

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

549 

550 Returns: 

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

552 """ 

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

554 

555 

556# ---------------------------------------------------------------- 

557# methods that execute the corresponding onefun method as is 

558# ---------------------------------------------------------------- 

559 

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

561 

562 

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

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

565 

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

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

568 

569 Args: 

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

571 

572 Note: 

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

574 corresponding method in the onefun object. 

575 """ 

576 

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

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

579 

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

581 and returns its result. 

582 

583 Args: 

584 self (Classicfun): The Classicfun object. 

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

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

587 

588 Returns: 

589 The return value from the corresponding onefun method. 

590 """ 

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

592 

593 method.__name__ = methodname 

594 method.__doc__ = method.__doc__ 

595 setattr(Classicfun, methodname, method) 

596 

597 

598for methodname in methods_onefun_other: 

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

600 continue 

601 add_utility(methodname) 

602 

603 

604# ----------------------------------------------------------------------- 

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

606# ----------------------------------------------------------------------- 

607 

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

609 

610 

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

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

613 

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

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

616 instance with the same interval. 

617 

618 Args: 

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

620 

621 Note: 

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

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

624 instance instead of an onefun instance. 

625 """ 

626 

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

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

629 

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

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

632 

633 Args: 

634 self (Classicfun): The Classicfun object. 

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

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

637 

638 Returns: 

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

640 """ 

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

642 return self._rebuild(onefun) 

643 

644 method.__name__ = methodname 

645 method.__doc__ = method.__doc__ 

646 setattr(Classicfun, methodname, method) 

647 

648 

649for methodname in methods_onefun_zeroargs: 

650 add_zero_arg_op(methodname) 

651 

652# ----------------------------------------- 

653# binary operators returning a onefun 

654# ----------------------------------------- 

655 

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

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

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

659_BINOP_OPERATORS: dict[str, Any] = { 

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

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

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

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

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

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

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

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

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

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

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

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

672} 

673 

674 

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

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

677 

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

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

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

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

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

683 """ 

684 op_fn = _BINOP_OPERATORS[methodname] 

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

686 

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

688 """Evaluate the binary op pointwise on both operands at *x*.""" 

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

690 

691 return owner._rebuild_from_callable(combined) 

692 

693 

694# ToDo: change these to operator module methods 

695methods_onefun_binary = ( 

696 "__add__", 

697 "__div__", 

698 "__mul__", 

699 "__pow__", 

700 "__radd__", 

701 "__rdiv__", 

702 "__rmul__", 

703 "__rpow__", 

704 "__rsub__", 

705 "__rtruediv__", 

706 "__sub__", 

707 "__truediv__", 

708) 

709 

710 

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

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

713 

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

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

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

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

718 

719 Args: 

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

721 

722 Note: 

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

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

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

726 """ 

727 

728 @self_empty() 

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

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

731 

732 This method implements a binary operation between this Classicfun and 

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

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

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

736 

737 Args: 

738 self (Classicfun): The Classicfun object. 

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

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

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

742 

743 Returns: 

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

745 

746 Raises: 

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

748 """ 

749 if isinstance(f, Classicfun): 

750 if f.isempty: 

751 return f.copy() 

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

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

754 if not self._can_share_onefun_with(f): 

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

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

757 return _classicfun_mixed_binop(self, f, methodname) 

758 g = f.onefun 

759 else: 

760 # let the lower level classes raise any other exceptions 

761 g = f 

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

763 return self._rebuild(onefun) 

764 

765 method.__name__ = methodname 

766 method.__doc__ = method.__doc__ 

767 setattr(Classicfun, methodname, method) 

768 

769 

770for methodname in methods_onefun_binary: 

771 add_binary_op(methodname) 

772 

773# --------------------------- 

774# numpy universal functions 

775# --------------------------- 

776 

777 

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

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

780 

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

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

783 

784 Args: 

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

786 

787 Note: 

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

789 and will take no arguments other than self. 

790 """ 

791 

792 @self_empty() 

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

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

795 

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

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

798 

799 Returns: 

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

801 """ 

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

803 

804 name = op.__name__ 

805 method.__name__ = name 

806 method.__doc__ = method.__doc__ 

807 setattr(Classicfun, name, method) 

808 

809 

810ufuncs = ( 

811 np.absolute, 

812 np.arccos, 

813 np.arccosh, 

814 np.arcsin, 

815 np.arcsinh, 

816 np.arctan, 

817 np.arctanh, 

818 np.ceil, 

819 np.cos, 

820 np.cosh, 

821 np.exp, 

822 np.exp2, 

823 np.expm1, 

824 np.floor, 

825 np.log, 

826 np.log2, 

827 np.log10, 

828 np.log1p, 

829 np.sign, 

830 np.sinh, 

831 np.sin, 

832 np.tan, 

833 np.tanh, 

834 np.sqrt, 

835) 

836 

837for op in ufuncs: 

838 add_ufunc(op)