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

170 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-01 13:43 +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 Examples: 

40 ``Classicfun`` is abstract; construct through a concrete subclass such 

41 as :class:`~chebpy.bndfun.Bndfun`. The interval is carried by the 

42 object, so evaluation and calculus happen in the user's coordinates 

43 rather than on [-1, 1]: 

44 

45 >>> import numpy as np 

46 >>> from chebpy.bndfun import Bndfun 

47 >>> from chebpy.utilities import Interval 

48 >>> f = Bndfun.initfun_adaptive(np.sin, Interval(0.0, np.pi)) 

49 >>> f.support.tolist() 

50 [0.0, 3.141592653589793] 

51 >>> bool(abs(f(np.pi / 2) - 1.0) < 1e-14) 

52 True 

53 

54 The integral of sin over [0, pi] is 2: 

55 

56 >>> bool(abs(f.sum() - 2.0) < 1e-14) 

57 True 

58 

59 The representation is delegated to a Onefun, which the interval map 

60 wraps: 

61 

62 >>> from chebpy.chebtech import Chebtech 

63 >>> isinstance(f.onefun, Chebtech) 

64 True 

65 """ 

66 

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

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

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

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

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

72 _singularity_priority: int = 0 

73 

74 if TYPE_CHECKING: 

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

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

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

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

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

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

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

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

83 ... 

84 

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

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

87 ... 

88 

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

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

91 ... 

92 

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

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

95 ... 

96 

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

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

99 ... 

100 

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

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

103 ... 

104 

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

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

107 ... 

108 

109 def __neg__(self) -> Fun: 

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

111 ... 

112 

113 def __pos__(self) -> Fun: 

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

115 ... 

116 

117 def copy(self) -> Fun: 

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

119 ... 

120 

121 def simplify(self) -> Fun: 

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

123 ... 

124 

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

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

127 ... 

128 

129 # -------------------------- 

130 # alternative constructors 

131 # -------------------------- 

132 @classmethod 

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

134 """Initialize an empty function. 

135 

136 This constructor creates an empty function representation, which is 

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

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

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

140 

141 Returns: 

142 Classicfun: A new empty instance. 

143 """ 

144 interval = Interval() 

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

146 return cls(onefun, interval) 

147 

148 @classmethod 

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

150 """Initialize a constant function. 

151 

152 This constructor creates a function that represents a constant value 

153 on the specified interval. 

154 

155 Args: 

156 c: The constant value. 

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

158 

159 Returns: 

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

161 """ 

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

163 return cls(onefun, interval) 

164 

165 @classmethod 

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

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

168 

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

170 on the specified interval. 

171 

172 Args: 

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

174 

175 Returns: 

176 Classicfun: A new instance representing the identity function. 

177 """ 

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

179 return cls(onefun, interval) 

180 

181 @classmethod 

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

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

184 

185 This constructor determines the appropriate number of points needed to 

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

187 

188 Args: 

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

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

191 

192 Returns: 

193 Classicfun: A new instance representing the function f. 

194 """ 

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

196 return cls(onefun, interval) 

197 

198 @classmethod 

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

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

201 

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

203 rather than determining the number adaptively. 

204 

205 Args: 

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

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

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

209 

210 Returns: 

211 Classicfun: A new instance representing the function f. 

212 """ 

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

214 return cls(onefun, interval) 

215 

216 # ------------------- 

217 # 'private' methods 

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

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

220 """Evaluate the function at points x. 

221 

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

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

224 

225 Args: 

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

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

228 

229 Returns: 

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

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

232 """ 

233 y = self.map.invmap(x) 

234 return self.onefun(y, how) 

235 

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

237 """Initialize a new Classicfun instance. 

238 

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

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

241 

242 Args: 

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

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

245 """ 

246 self.onefun = onefun 

247 self._interval = interval 

248 

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

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

251 

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

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

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

255 preserve that metadata. 

256 

257 Args: 

258 onefun: The replacement Onefun object. 

259 

260 Returns: 

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

262 """ 

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

264 

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

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

267 

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

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

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

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

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

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

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

275 compare maps. 

276 """ 

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

278 

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

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

281 

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

283 dominant operand's representation. Subclasses with extra metadata 

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

285 """ 

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

287 

288 def __repr__(self) -> str: 

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

290 

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

292 the class name, support interval, and size. 

293 

294 Returns: 

295 str: A string representation of the function. 

296 """ 

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

298 return out 

299 

300 # ------------ 

301 # properties 

302 # ------------ 

303 @property 

304 def coeffs(self) -> Any: 

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

306 

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

308 delegating to the underlying onefun object. 

309 

310 Returns: 

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

312 """ 

313 return self.onefun.coeffs 

314 

315 @property 

316 def endvalues(self) -> Any: 

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

318 

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

320 of definition. 

321 

322 Returns: 

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

324 of the interval [a, b]. 

325 """ 

326 return self.__call__(self.support) 

327 

328 @property 

329 def interval(self) -> Any: 

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

331 

332 This property returns the interval object representing the domain 

333 of definition for this function. 

334 

335 Returns: 

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

337 """ 

338 return self._interval 

339 

340 @property 

341 def map(self) -> IntervalMap: 

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

343 

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

345 transforms for endpoint singularities) override this to return a 

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

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

348 

349 Returns: 

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

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

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

353 """ 

354 return cast(IntervalMap, self._interval) 

355 

356 @property 

357 def isconst(self) -> Any: 

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

359 

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

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

362 underlying onefun object. 

363 

364 Returns: 

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

366 """ 

367 return self.onefun.isconst 

368 

369 @property 

370 def iscomplex(self) -> Any: 

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

372 

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

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

375 

376 Returns: 

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

378 """ 

379 return self.onefun.iscomplex 

380 

381 @property 

382 def isempty(self) -> Any: 

383 """Check if this function is empty. 

384 

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

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

387 underlying onefun object. 

388 

389 Returns: 

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

391 """ 

392 return self.onefun.isempty 

393 

394 @property 

395 def size(self) -> Any: 

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

397 

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

399 complexity of the function representation, delegating to the underlying 

400 onefun object. 

401 

402 Returns: 

403 int: The size of the function representation. 

404 """ 

405 return self.onefun.size 

406 

407 @property 

408 def support(self) -> Any: 

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

410 

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

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

413 

414 Returns: 

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

416 """ 

417 return np.asarray(self.interval) 

418 

419 @property 

420 def vscale(self) -> Any: 

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

422 

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

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

425 delegating to the underlying onefun object. 

426 

427 Returns: 

428 float: The vertical scale of the function. 

429 """ 

430 return self.onefun.vscale 

431 

432 # ----------- 

433 # utilities 

434 # ----------- 

435 

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

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

438 

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

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

441 

442 Returns: 

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

444 """ 

445 if self.iscomplex: 

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

447 else: 

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

449 

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

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

452 

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

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

455 

456 Returns: 

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

458 """ 

459 if self.iscomplex: 

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

461 else: 

462 return self 

463 

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

465 """Restrict this function to a subinterval. 

466 

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

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

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

470 

471 Args: 

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

473 Must be contained within the original interval of definition. 

474 

475 Returns: 

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

477 

478 Raises: 

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

480 """ 

481 if subinterval not in self.interval: 

482 raise NotSubinterval(self.interval, subinterval) 

483 if self.interval == subinterval: 

484 return self 

485 else: 

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

487 

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

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

490 

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

492 function translated horizontally by c. 

493 

494 Args: 

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

496 

497 Returns: 

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

499 """ 

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

501 

502 # ------------- 

503 # rootfinding 

504 # ------------- 

505 def roots(self) -> Any: 

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

507 

508 This method computes the points where the function equals zero 

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

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

511 

512 Returns: 

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

514 sorted in ascending order. 

515 """ 

516 uroots = self.onefun.roots() 

517 return self.map.formap(uroots) 

518 

519 # ---------- 

520 # calculus 

521 # ---------- 

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

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

524 

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

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

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

528 

529 Returns: 

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

531 """ 

532 a, b = self.interval 

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

534 return self._rebuild(onefun) 

535 

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

537 """Compute the derivative of the function. 

538 

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

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

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

542 

543 Returns: 

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

545 """ 

546 a, b = self.interval 

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

548 return self._rebuild(onefun) 

549 

550 def sum(self) -> Any: 

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

552 

553 This method calculates the definite integral of the function 

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

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

556 

557 Returns: 

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

559 """ 

560 a, b = self.interval 

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

562 

563 # ---------- 

564 # plotting 

565 # ---------- 

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

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

568 

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

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

571 

572 Args: 

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

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

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

576 

577 Returns: 

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

579 """ 

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

581 

582 

583# ---------------------------------------------------------------- 

584# methods that execute the corresponding onefun method as is 

585# ---------------------------------------------------------------- 

586 

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

588 

589 

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

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

592 

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

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

595 

596 Args: 

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

598 

599 Note: 

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

601 corresponding method in the onefun object. 

602 """ 

603 

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

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

606 

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

608 and returns its result. 

609 

610 Args: 

611 self (Classicfun): The Classicfun object. 

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

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

614 

615 Returns: 

616 The return value from the corresponding onefun method. 

617 """ 

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

619 

620 method.__name__ = methodname 

621 method.__doc__ = method.__doc__ 

622 setattr(Classicfun, methodname, method) 

623 

624 

625for methodname in methods_onefun_other: 

626 if methodname[:4] == "plot" and plt is None: # pragma: no cover - only without matplotlib 

627 continue 

628 add_utility(methodname) 

629 

630 

631# ----------------------------------------------------------------------- 

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

633# ----------------------------------------------------------------------- 

634 

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

636 

637 

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

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

640 

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

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

643 instance with the same interval. 

644 

645 Args: 

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

647 

648 Note: 

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

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

651 instance instead of an onefun instance. 

652 """ 

653 

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

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

656 

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

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

659 

660 Args: 

661 self (Classicfun): The Classicfun object. 

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

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

664 

665 Returns: 

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

667 """ 

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

669 return self._rebuild(onefun) 

670 

671 method.__name__ = methodname 

672 method.__doc__ = method.__doc__ 

673 setattr(Classicfun, methodname, method) 

674 

675 

676for methodname in methods_onefun_zeroargs: 

677 add_zero_arg_op(methodname) 

678 

679# ----------------------------------------- 

680# binary operators returning a onefun 

681# ----------------------------------------- 

682 

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

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

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

686_BINOP_OPERATORS: dict[str, Any] = { 

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

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

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

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

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

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

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

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

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

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

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

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

699} 

700 

701 

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

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

704 

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

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

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

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

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

710 """ 

711 op_fn = _BINOP_OPERATORS[methodname] 

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

713 

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

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

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

717 

718 return owner._rebuild_from_callable(combined) 

719 

720 

721# ToDo: change these to operator module methods 

722methods_onefun_binary = ( 

723 "__add__", 

724 "__div__", 

725 "__mul__", 

726 "__pow__", 

727 "__radd__", 

728 "__rdiv__", 

729 "__rmul__", 

730 "__rpow__", 

731 "__rsub__", 

732 "__rtruediv__", 

733 "__sub__", 

734 "__truediv__", 

735) 

736 

737 

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

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

740 

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

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

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

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

745 

746 Args: 

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

748 

749 Note: 

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

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

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

753 """ 

754 

755 @self_empty() 

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

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

758 

759 This method implements a binary operation between this Classicfun and 

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

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

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

763 

764 Args: 

765 self (Classicfun): The Classicfun object. 

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

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

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

769 

770 Returns: 

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

772 

773 Raises: 

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

775 """ 

776 if isinstance(f, Classicfun): 

777 if f.isempty: 

778 return f.copy() 

779 if self.interval != f.interval: 

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

781 if not self._can_share_onefun_with(f): 

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

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

784 return _classicfun_mixed_binop(self, f, methodname) 

785 g = f.onefun 

786 else: 

787 # let the lower level classes raise any other exceptions 

788 g = f 

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

790 return self._rebuild(onefun) 

791 

792 method.__name__ = methodname 

793 method.__doc__ = method.__doc__ 

794 setattr(Classicfun, methodname, method) 

795 

796 

797for methodname in methods_onefun_binary: 

798 add_binary_op(methodname) 

799 

800# --------------------------- 

801# numpy universal functions 

802# --------------------------- 

803 

804 

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

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

807 

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

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

810 

811 Args: 

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

813 

814 Note: 

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

816 and will take no arguments other than self. 

817 """ 

818 

819 @self_empty() 

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

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

822 

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

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

825 

826 Returns: 

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

828 """ 

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

830 

831 name = op.__name__ 

832 method.__name__ = name 

833 method.__doc__ = method.__doc__ 

834 setattr(Classicfun, name, method) 

835 

836 

837ufuncs = ( 

838 np.absolute, 

839 np.arccos, 

840 np.arccosh, 

841 np.arcsin, 

842 np.arcsinh, 

843 np.arctan, 

844 np.arctanh, 

845 np.ceil, 

846 np.cos, 

847 np.cosh, 

848 np.exp, 

849 np.exp2, 

850 np.expm1, 

851 np.floor, 

852 np.log, 

853 np.log2, 

854 np.log10, 

855 np.log1p, 

856 np.sign, 

857 np.sinh, 

858 np.sin, 

859 np.tan, 

860 np.tanh, 

861 np.sqrt, 

862) 

863 

864for op in ufuncs: 

865 add_ufunc(op)