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
« 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.
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"""
7from abc import ABC
8from typing import TYPE_CHECKING, Any, cast
10import matplotlib.pyplot as plt
11import numpy as np
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
22techdict = {
23 "Chebtech": Chebtech,
24 "Trigtech": Trigtech,
25}
28class Classicfun(Fun, ABC):
29 """Abstract base class for functions defined on arbitrary intervals using a mapped representation.
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.
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.
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]:
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
54 The integral of sin over [0, pi] is 2:
56 >>> bool(abs(f.sum() - 2.0) < 1e-14)
57 True
59 The representation is delegated to a Onefun, which the interval map
60 wraps:
62 >>> from chebpy.chebtech import Chebtech
63 >>> isinstance(f.onefun, Chebtech)
64 True
65 """
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
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 ...
85 def __sub__(self, other: Any) -> Fun:
86 """Subtract another function or scalar (dynamically attached)."""
87 ...
89 def __mul__(self, other: Any) -> Fun:
90 """Multiply by another function or scalar (dynamically attached)."""
91 ...
93 def __pow__(self, power: Any) -> Fun:
94 """Raise to a power (dynamically attached)."""
95 ...
97 def __radd__(self, other: Any) -> Fun:
98 """Right-hand addition (dynamically attached)."""
99 ...
101 def __rsub__(self, other: Any) -> Fun:
102 """Right-hand subtraction (dynamically attached)."""
103 ...
105 def __rmul__(self, other: Any) -> Fun:
106 """Right-hand multiplication (dynamically attached)."""
107 ...
109 def __neg__(self) -> Fun:
110 """Negate this function (dynamically attached)."""
111 ...
113 def __pos__(self) -> Fun:
114 """Return this function unchanged (dynamically attached)."""
115 ...
117 def copy(self) -> Fun:
118 """Return a deep copy (dynamically attached)."""
119 ...
121 def simplify(self) -> Fun:
122 """Return a simplified representation (dynamically attached)."""
123 ...
125 def values(self) -> np.ndarray:
126 """Return the function values at the representation points (dynamically attached)."""
127 ...
129 # --------------------------
130 # alternative constructors
131 # --------------------------
132 @classmethod
133 def initempty(cls) -> "Classicfun":
134 """Initialize an empty function.
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].
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)
148 @classmethod
149 def initconst(cls, c: Any, interval: Any) -> "Classicfun":
150 """Initialize a constant function.
152 This constructor creates a function that represents a constant value
153 on the specified interval.
155 Args:
156 c: The constant value.
157 interval: The interval on which to define the function.
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)
165 @classmethod
166 def initidentity(cls, interval: Any) -> "Classicfun":
167 """Initialize the identity function f(x) = x.
169 This constructor creates a function that represents f(x) = x
170 on the specified interval.
172 Args:
173 interval: The interval on which to define the identity function.
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)
181 @classmethod
182 def initfun_adaptive(cls, f: Any, interval: Any) -> "Classicfun":
183 """Initialize from a callable function using adaptive sampling.
185 This constructor determines the appropriate number of points needed to
186 represent the function to the specified tolerance using an adaptive algorithm.
188 Args:
189 f (callable): The function to be approximated.
190 interval: The interval on which to define the function.
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)
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.
202 This constructor uses a specified number of points to represent the function,
203 rather than determining the number adaptively.
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.
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)
216 # -------------------
217 # 'private' methods
218 # -------------------
219 def __call__(self, x: Any, how: str = "clenshaw") -> Any:
220 """Evaluate the function at points x.
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.
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".
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)
236 def __init__(self, onefun: Any, interval: Any) -> None:
237 """Initialize a new Classicfun instance.
239 This method initializes a new function representation on the specified interval
240 using the provided onefun object for the standard domain representation.
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
249 def _rebuild(self, onefun: Any) -> "Classicfun":
250 """Construct a new instance of this class with a replacement ``onefun``.
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.
257 Args:
258 onefun: The replacement Onefun object.
260 Returns:
261 Classicfun: A new instance of ``type(self)``.
262 """
263 return self.__class__(onefun, self._interval)
265 def _can_share_onefun_with(self, other: "Classicfun") -> bool:
266 """Return True if ``self`` and ``other`` represent functions on the same t-grid.
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
279 def _rebuild_from_callable(self, f: Any) -> "Classicfun":
280 """Adaptively rebuild a fun of this type evaluating callable ``f``.
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)
288 def __repr__(self) -> str:
289 """Return a string representation of the function.
291 This method returns a string representation of the function that includes
292 the class name, support interval, and size.
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
300 # ------------
301 # properties
302 # ------------
303 @property
304 def coeffs(self) -> Any:
305 """Get the coefficients of the function representation.
307 This property returns the coefficients used in the function representation,
308 delegating to the underlying onefun object.
310 Returns:
311 array-like: The coefficients of the function representation.
312 """
313 return self.onefun.coeffs
315 @property
316 def endvalues(self) -> Any:
317 """Get the values of the function at the endpoints of its interval.
319 This property evaluates the function at the endpoints of its interval
320 of definition.
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)
328 @property
329 def interval(self) -> Any:
330 """Get the interval on which this function is defined.
332 This property returns the interval object representing the domain
333 of definition for this function.
335 Returns:
336 Interval: The interval on which this function is defined.
337 """
338 return self._interval
340 @property
341 def map(self) -> IntervalMap:
342 """Return the bijective map between [-1, 1] and the function's interval.
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.
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)
356 @property
357 def isconst(self) -> Any:
358 """Check if this function represents a constant.
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.
364 Returns:
365 bool: True if the function is constant, False otherwise.
366 """
367 return self.onefun.isconst
369 @property
370 def iscomplex(self) -> Any:
371 """Check if this function has complex values.
373 This property determines whether the function has complex values or is
374 purely real-valued, delegating to the underlying onefun object.
376 Returns:
377 bool: True if the function has complex values, False otherwise.
378 """
379 return self.onefun.iscomplex
381 @property
382 def isempty(self) -> Any:
383 """Check if this function is empty.
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.
389 Returns:
390 bool: True if the function is empty, False otherwise.
391 """
392 return self.onefun.isempty
394 @property
395 def size(self) -> Any:
396 """Get the size of the function representation.
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.
402 Returns:
403 int: The size of the function representation.
404 """
405 return self.onefun.size
407 @property
408 def support(self) -> Any:
409 """Get the support interval of this function.
411 This property returns the interval on which this function is defined,
412 represented as a numpy array with two elements [a, b].
414 Returns:
415 numpy.ndarray: Array containing the endpoints of the interval.
416 """
417 return np.asarray(self.interval)
419 @property
420 def vscale(self) -> Any:
421 """Get the vertical scale of the function.
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.
427 Returns:
428 float: The vertical scale of the function.
429 """
430 return self.onefun.vscale
432 # -----------
433 # utilities
434 # -----------
436 def imag(self) -> "Classicfun":
437 """Get the imaginary part of this function.
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.
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)
450 def real(self) -> "Classicfun":
451 """Get the real part of this function.
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.
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
464 def restrict(self, subinterval: Any) -> "Classicfun":
465 """Restrict this function to a subinterval.
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.
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.
475 Returns:
476 Classicfun: A new function representing the restriction of this function to the subinterval.
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)
488 def translate(self, c: float) -> "Classicfun":
489 """Translate this function by a constant c.
491 This method creates a new function g(x) = f(x-c), which is the original
492 function translated horizontally by c.
494 Args:
495 c (float): The amount by which to translate the function.
497 Returns:
498 Classicfun: A new function representing g(x) = f(x-c).
499 """
500 return self.__class__(self.onefun, self.interval + c)
502 # -------------
503 # rootfinding
504 # -------------
505 def roots(self) -> Any:
506 """Find the roots (zeros) of the function on its interval of definition.
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.
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)
519 # ----------
520 # calculus
521 # ----------
522 def cumsum(self) -> "Classicfun":
523 """Compute the indefinite integral of the function.
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.
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)
536 def diff(self) -> "Classicfun":
537 """Compute the derivative of the function.
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.
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)
550 def sum(self) -> Any:
551 """Compute the definite integral of the function over its interval of definition.
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].
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()
563 # ----------
564 # plotting
565 # ----------
566 def plot(self, ax: Any = None, **kwds: Any) -> Any:
567 """Plot the function over its interval of definition.
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.
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.
577 Returns:
578 matplotlib.axes.Axes: The axes on which the plot was created.
579 """
580 return plotfun(self, self.support, ax=ax, **kwds)
583# ----------------------------------------------------------------
584# methods that execute the corresponding onefun method as is
585# ----------------------------------------------------------------
587methods_onefun_other = ("values", "plotcoeffs")
590def add_utility(methodname: str) -> None:
591 """Add a utility method to the Classicfun class.
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.
596 Args:
597 methodname (str): The name of the method to add.
599 Note:
600 The created method will have the same name and signature as the
601 corresponding method in the onefun object.
602 """
604 def method(self: Any, *args: Any, **kwds: Any) -> Any:
605 """Delegate to the corresponding method of the underlying onefun object.
607 This method calls the same-named method on the underlying onefun object
608 and returns its result.
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.
615 Returns:
616 The return value from the corresponding onefun method.
617 """
618 return getattr(self.onefun, methodname)(*args, **kwds)
620 method.__name__ = methodname
621 method.__doc__ = method.__doc__
622 setattr(Classicfun, methodname, method)
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)
631# -----------------------------------------------------------------------
632# unary operators and zero-argument utlity methods returning a onefun
633# -----------------------------------------------------------------------
635methods_onefun_zeroargs = ("__pos__", "__neg__", "copy", "simplify")
638def add_zero_arg_op(methodname: str) -> None:
639 """Add a zero-argument operation method to the Classicfun class.
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.
645 Args:
646 methodname (str): The name of the method to add.
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 """
654 def method(self: Any, *args: Any, **kwds: Any) -> Any:
655 """Apply a zero-argument operation and return a new Classicfun.
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.
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.
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)
671 method.__name__ = methodname
672 method.__doc__ = method.__doc__
673 setattr(Classicfun, methodname, method)
676for methodname in methods_onefun_zeroargs:
677 add_zero_arg_op(methodname)
679# -----------------------------------------
680# binary operators returning a onefun
681# -----------------------------------------
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}
702def _classicfun_mixed_binop(self: "Classicfun", other: "Classicfun", methodname: str) -> "Classicfun":
703 """Reconstruct a same-interval, mixed-subclass binary op on the dominant operand.
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
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))
718 return owner._rebuild_from_callable(combined)
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)
738def add_binary_op(methodname: str) -> None:
739 """Add a binary operation method to the Classicfun class.
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.
746 Args:
747 methodname (str): The name of the binary operation method to add.
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 """
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.
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.
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.
770 Returns:
771 Classicfun: A new Classicfun instance with the result of the operation.
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)
792 method.__name__ = methodname
793 method.__doc__ = method.__doc__
794 setattr(Classicfun, methodname, method)
797for methodname in methods_onefun_binary:
798 add_binary_op(methodname)
800# ---------------------------
801# numpy universal functions
802# ---------------------------
805def add_ufunc(op: Any) -> None:
806 """Add a NumPy universal function method to the Classicfun class.
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.
811 Args:
812 op (callable): The NumPy universal function to apply.
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 """
819 @self_empty()
820 def method(self: Any) -> Any:
821 """Apply a NumPy universal function to this function.
823 This method applies a NumPy universal function (ufunc) to the values
824 of this function and returns a new function representing the result.
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)
831 name = op.__name__
832 method.__name__ = name
833 method.__doc__ = method.__doc__
834 setattr(Classicfun, name, method)
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)
864for op in ufuncs:
865 add_ufunc(op)