Coverage for chebpy/core/classicfun.py: 99%
143 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-07 10:30 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-07 10:30 +0000
1"""Implementation of the Classicfun class for functions on arbitrary intervals.
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
9import matplotlib.pyplot as plt
10import numpy as np
12from .chebtech import Chebtech
13from .decorators import self_empty
14from .exceptions import IntervalMismatch, NotSubinterval
15from .fun import Fun
16from .plotting import plotfun
17from .settings import _preferences as prefs
18from .utilities import Interval
20techdict = {
21 "Chebtech": Chebtech,
22}
25class Classicfun(Fun, ABC):
26 """Abstract base class for functions defined on arbitrary intervals using a mapped representation.
28 This class implements the Fun interface for functions defined on arbitrary intervals
29 by mapping them to a standard domain [-1, 1] and using a Onefun representation
30 (such as Chebtech) on that standard domain.
32 The Classicfun class serves as a base class for specific implementations like Bndfun.
33 It handles the mapping between the arbitrary interval and the standard domain,
34 delegating the actual function representation to the underlying Onefun object.
35 """
37 # --------------------------
38 # alternative constructors
39 # --------------------------
40 @classmethod
41 def initempty(cls):
42 """Initialize an empty function.
44 This constructor creates an empty function representation, which is
45 useful as a placeholder or for special cases. The interval has no
46 relevance to the emptiness status of a Classicfun, so we arbitrarily
47 set it to be the default interval [-1, 1].
49 Returns:
50 Classicfun: A new empty instance.
51 """
52 interval = Interval()
53 onefun = techdict[prefs.tech].initempty(interval=interval)
54 return cls(onefun, interval)
56 @classmethod
57 def initconst(cls, c, interval):
58 """Initialize a constant function.
60 This constructor creates a function that represents a constant value
61 on the specified interval.
63 Args:
64 c: The constant value.
65 interval: The interval on which to define the function.
67 Returns:
68 Classicfun: A new instance representing the constant function f(x) = c.
69 """
70 onefun = techdict[prefs.tech].initconst(c, interval=interval)
71 return cls(onefun, interval)
73 @classmethod
74 def initidentity(cls, interval):
75 """Initialize the identity function f(x) = x.
77 This constructor creates a function that represents f(x) = x
78 on the specified interval.
80 Args:
81 interval: The interval on which to define the identity function.
83 Returns:
84 Classicfun: A new instance representing the identity function.
85 """
86 onefun = techdict[prefs.tech].initvalues(np.asarray(interval), interval=interval)
87 return cls(onefun, interval)
89 @classmethod
90 def initfun_adaptive(cls, f, interval):
91 """Initialize from a callable function using adaptive sampling.
93 This constructor determines the appropriate number of points needed to
94 represent the function to the specified tolerance using an adaptive algorithm.
96 Args:
97 f (callable): The function to be approximated.
98 interval: The interval on which to define the function.
100 Returns:
101 Classicfun: A new instance representing the function f.
102 """
103 onefun = techdict[prefs.tech].initfun(lambda y: f(interval(y)), interval=interval)
104 return cls(onefun, interval)
106 @classmethod
107 def initfun_fixedlen(cls, f, interval, n):
108 """Initialize from a callable function using a fixed number of points.
110 This constructor uses a specified number of points to represent the function,
111 rather than determining the number adaptively.
113 Args:
114 f (callable): The function to be approximated.
115 interval: The interval on which to define the function.
116 n (int): The number of points to use.
118 Returns:
119 Classicfun: A new instance representing the function f.
120 """
121 onefun = techdict[prefs.tech].initfun(lambda y: f(interval(y)), n, interval=interval)
122 return cls(onefun, interval)
124 # -------------------
125 # 'private' methods
126 # -------------------
127 def __call__(self, x, how="clenshaw"):
128 """Evaluate the function at points x.
130 This method evaluates the function at the specified points by mapping them
131 to the standard domain [-1, 1] and evaluating the underlying onefun.
133 Args:
134 x (float or array-like): Points at which to evaluate the function.
135 how (str, optional): Method to use for evaluation. Defaults to "clenshaw".
137 Returns:
138 float or array-like: The value(s) of the function at the specified point(s).
139 Returns a scalar if x is a scalar, otherwise an array of the same size as x.
140 """
141 y = self.interval.invmap(x)
142 return self.onefun(y, how)
144 def __init__(self, onefun, interval):
145 """Initialize a new Classicfun instance.
147 This method initializes a new function representation on the specified interval
148 using the provided onefun object for the standard domain representation.
150 Args:
151 onefun: The Onefun object representing the function on [-1, 1].
152 interval: The Interval object defining the domain of the function.
153 """
154 self.onefun = onefun
155 self._interval = interval
157 def __repr__(self): # pragma: no cover
158 """Return a string representation of the function.
160 This method returns a string representation of the function that includes
161 the class name, support interval, and size.
163 Returns:
164 str: A string representation of the function.
165 """
166 out = "{0}([{2}, {3}], {1})".format(self.__class__.__name__, self.size, *self.support)
167 return out
169 # ------------
170 # properties
171 # ------------
172 @property
173 def coeffs(self):
174 """Get the coefficients of the function representation.
176 This property returns the coefficients used in the function representation,
177 delegating to the underlying onefun object.
179 Returns:
180 array-like: The coefficients of the function representation.
181 """
182 return self.onefun.coeffs
184 @property
185 def endvalues(self):
186 """Get the values of the function at the endpoints of its interval.
188 This property evaluates the function at the endpoints of its interval
189 of definition.
191 Returns:
192 numpy.ndarray: Array containing the function values at the endpoints
193 of the interval [a, b].
194 """
195 return self.__call__(self.support)
197 @property
198 def interval(self):
199 """Get the interval on which this function is defined.
201 This property returns the interval object representing the domain
202 of definition for this function.
204 Returns:
205 Interval: The interval on which this function is defined.
206 """
207 return self._interval
209 @property
210 def isconst(self):
211 """Check if this function represents a constant.
213 This property determines whether the function is constant (i.e., f(x) = c
214 for some constant c) over its interval of definition, delegating to the
215 underlying onefun object.
217 Returns:
218 bool: True if the function is constant, False otherwise.
219 """
220 return self.onefun.isconst
222 @property
223 def iscomplex(self):
224 """Check if this function has complex values.
226 This property determines whether the function has complex values or is
227 purely real-valued, delegating to the underlying onefun object.
229 Returns:
230 bool: True if the function has complex values, False otherwise.
231 """
232 return self.onefun.iscomplex
234 @property
235 def isempty(self):
236 """Check if this function is empty.
238 This property determines whether the function is empty, which is a special
239 state used as a placeholder or for special cases, delegating to the
240 underlying onefun object.
242 Returns:
243 bool: True if the function is empty, False otherwise.
244 """
245 return self.onefun.isempty
247 @property
248 def size(self):
249 """Get the size of the function representation.
251 This property returns the number of coefficients or other measure of the
252 complexity of the function representation, delegating to the underlying
253 onefun object.
255 Returns:
256 int: The size of the function representation.
257 """
258 return self.onefun.size
260 @property
261 def support(self):
262 """Get the support interval of this function.
264 This property returns the interval on which this function is defined,
265 represented as a numpy array with two elements [a, b].
267 Returns:
268 numpy.ndarray: Array containing the endpoints of the interval.
269 """
270 return np.asarray(self.interval)
272 @property
273 def vscale(self):
274 """Get the vertical scale of the function.
276 This property returns a measure of the range of function values, typically
277 the maximum absolute value of the function on its interval of definition,
278 delegating to the underlying onefun object.
280 Returns:
281 float: The vertical scale of the function.
282 """
283 return self.onefun.vscale
285 # -----------
286 # utilities
287 # -----------
289 def imag(self):
290 """Get the imaginary part of this function.
292 This method returns a new function representing the imaginary part of this function.
293 If this function is real-valued, returns a zero function.
295 Returns:
296 Classicfun: A new function representing the imaginary part of this function.
297 """
298 if self.iscomplex:
299 return self.__class__(self.onefun.imag(), self.interval)
300 else:
301 return self.initconst(0, interval=self.interval)
303 def real(self):
304 """Get the real part of this function.
306 This method returns a new function representing the real part of this function.
307 If this function is already real-valued, returns this function.
309 Returns:
310 Classicfun: A new function representing the real part of this function.
311 """
312 if self.iscomplex:
313 return self.__class__(self.onefun.real(), self.interval)
314 else:
315 return self
317 def restrict(self, subinterval):
318 """Restrict this function to a subinterval.
320 This method creates a new function that is the restriction of this function
321 to the specified subinterval. The output is formed using a fixed length
322 construction with the same number of degrees of freedom as the original function.
324 Args:
325 subinterval (array-like): The subinterval to which this function should be restricted.
326 Must be contained within the original interval of definition.
328 Returns:
329 Classicfun: A new function representing the restriction of this function to the subinterval.
331 Raises:
332 NotSubinterval: If the subinterval is not contained within the original interval.
333 """
334 if subinterval not in self.interval: # pragma: no cover
335 raise NotSubinterval(self.interval, subinterval)
336 if self.interval == subinterval:
337 return self
338 else:
339 return self.__class__.initfun_fixedlen(self, subinterval, self.size)
341 def translate(self, c):
342 """Translate this function by a constant c.
344 This method creates a new function g(x) = f(x-c), which is the original
345 function translated horizontally by c.
347 Args:
348 c (float): The amount by which to translate the function.
350 Returns:
351 Classicfun: A new function representing g(x) = f(x-c).
352 """
353 return self.__class__(self.onefun, self.interval + c)
355 # -------------
356 # rootfinding
357 # -------------
358 def roots(self):
359 """Find the roots (zeros) of the function on its interval of definition.
361 This method computes the points where the function equals zero
362 within its interval of definition by finding the roots of the
363 underlying onefun and mapping them to the function's interval.
365 Returns:
366 numpy.ndarray: An array of the roots of the function in its interval of definition,
367 sorted in ascending order.
368 """
369 uroots = self.onefun.roots()
370 return self.interval(uroots)
372 # ----------
373 # calculus
374 # ----------
375 def cumsum(self):
376 """Compute the indefinite integral of the function.
378 This method calculates the indefinite integral (antiderivative) of the function,
379 with the constant of integration chosen so that the indefinite integral
380 evaluates to 0 at the left endpoint of the interval.
382 Returns:
383 Classicfun: A new function representing the indefinite integral of this function.
384 """
385 a, b = self.support
386 onefun = 0.5 * (b - a) * self.onefun.cumsum()
387 return self.__class__(onefun, self.interval)
389 def diff(self):
390 """Compute the derivative of the function.
392 This method calculates the derivative of the function with respect to x,
393 applying the chain rule to account for the mapping between the standard
394 domain [-1, 1] and the function's interval.
396 Returns:
397 Classicfun: A new function representing the derivative of this function.
398 """
399 a, b = self.support
400 onefun = 2.0 / (b - a) * self.onefun.diff()
401 return self.__class__(onefun, self.interval)
403 def sum(self):
404 """Compute the definite integral of the function over its interval of definition.
406 This method calculates the definite integral of the function
407 over its interval of definition [a, b], applying the appropriate
408 scaling factor to account for the mapping from [-1, 1].
410 Returns:
411 float or complex: The definite integral of the function over its interval of definition.
412 """
413 a, b = self.support
414 return 0.5 * (b - a) * self.onefun.sum()
416 # ----------
417 # plotting
418 # ----------
419 def plot(self, ax=None, **kwds):
420 """Plot the function over its interval of definition.
422 This method plots the function over its interval of definition using matplotlib.
423 For complex-valued functions, it plots the real part against the imaginary part.
425 Args:
426 ax (matplotlib.axes.Axes, optional): The axes on which to plot. If None,
427 a new axes will be created. Defaults to None.
428 **kwds: Additional keyword arguments to pass to matplotlib's plot function.
430 Returns:
431 matplotlib.axes.Axes: The axes on which the plot was created.
432 """
433 return plotfun(self, self.support, ax=ax, **kwds)
436# ----------------------------------------------------------------
437# methods that execute the corresponding onefun method as is
438# ----------------------------------------------------------------
440methods_onefun_other = ("values", "plotcoeffs")
443def add_utility(methodname):
444 """Add a utility method to the Classicfun class.
446 This function creates a method that delegates to the corresponding method
447 of the underlying onefun object and adds it to the Classicfun class.
449 Args:
450 methodname (str): The name of the method to add.
452 Note:
453 The created method will have the same name and signature as the
454 corresponding method in the onefun object.
455 """
457 def method(self, *args, **kwds):
458 """Delegate to the corresponding method of the underlying onefun object.
460 This method calls the same-named method on the underlying onefun object
461 and returns its result.
463 Args:
464 self (Classicfun): The Classicfun object.
465 *args: Variable length argument list to pass to the onefun method.
466 **kwds: Arbitrary keyword arguments to pass to the onefun method.
468 Returns:
469 The return value from the corresponding onefun method.
470 """
471 return getattr(self.onefun, methodname)(*args, **kwds)
473 method.__name__ = methodname
474 method.__doc__ = method.__doc__
475 setattr(Classicfun, methodname, method)
478for methodname in methods_onefun_other:
479 if methodname[:4] == "plot" and plt is None: # pragma: no cover
480 continue
481 add_utility(methodname)
484# -----------------------------------------------------------------------
485# unary operators and zero-argument utlity methods returning a onefun
486# -----------------------------------------------------------------------
488methods_onefun_zeroargs = ("__pos__", "__neg__", "copy", "simplify")
491def add_zero_arg_op(methodname):
492 """Add a zero-argument operation method to the Classicfun class.
494 This function creates a method that delegates to the corresponding method
495 of the underlying onefun object and wraps the result in a new Classicfun
496 instance with the same interval.
498 Args:
499 methodname (str): The name of the method to add.
501 Note:
502 The created method will have the same name and signature as the
503 corresponding method in the onefun object, but will return a Classicfun
504 instance instead of an onefun instance.
505 """
507 def method(self, *args, **kwds):
508 """Apply a zero-argument operation and return a new Classicfun.
510 This method calls the same-named method on the underlying onefun object
511 and wraps the result in a new Classicfun instance with the same interval.
513 Args:
514 self (Classicfun): The Classicfun object.
515 *args: Variable length argument list to pass to the onefun method.
516 **kwds: Arbitrary keyword arguments to pass to the onefun method.
518 Returns:
519 Classicfun: A new Classicfun instance with the result of the operation.
520 """
521 onefun = getattr(self.onefun, methodname)(*args, **kwds)
522 return self.__class__(onefun, self.interval)
524 method.__name__ = methodname
525 method.__doc__ = method.__doc__
526 setattr(Classicfun, methodname, method)
529for methodname in methods_onefun_zeroargs:
530 add_zero_arg_op(methodname)
532# -----------------------------------------
533# binary operators returning a onefun
534# -----------------------------------------
536# ToDo: change these to operator module methods
537methods_onefun_binary = (
538 "__add__",
539 "__div__",
540 "__mul__",
541 "__pow__",
542 "__radd__",
543 "__rdiv__",
544 "__rmul__",
545 "__rpow__",
546 "__rsub__",
547 "__rtruediv__",
548 "__sub__",
549 "__truediv__",
550)
553def add_binary_op(methodname):
554 """Add a binary operation method to the Classicfun class.
556 This function creates a method that implements a binary operation between
557 two Classicfun objects or between a Classicfun and a scalar. It delegates
558 to the corresponding method of the underlying onefun object and wraps the
559 result in a new Classicfun instance with the same interval.
561 Args:
562 methodname (str): The name of the binary operation method to add.
564 Note:
565 The created method will check that both Classicfun objects have the
566 same interval before performing the operation. If one operand is not
567 a Classicfun, it will be passed directly to the onefun method.
568 """
570 @self_empty()
571 def method(self, f, *args, **kwds):
572 """Apply a binary operation and return a new Classicfun.
574 This method implements a binary operation between this Classicfun and
575 another object (either another Classicfun or a scalar). It delegates
576 to the corresponding method of the underlying onefun object and wraps
577 the result in a new Classicfun instance with the same interval.
579 Args:
580 self (Classicfun): The Classicfun object.
581 f (Classicfun or scalar): The second operand of the binary operation.
582 *args: Variable length argument list to pass to the onefun method.
583 **kwds: Arbitrary keyword arguments to pass to the onefun method.
585 Returns:
586 Classicfun: A new Classicfun instance with the result of the operation.
588 Raises:
589 IntervalMismatch: If f is a Classicfun with a different interval.
590 """
591 cls = self.__class__
592 if isinstance(f, cls):
593 # TODO: as in ChebTech, is a decorator apporach here better?
594 if f.isempty:
595 return f.copy()
596 g = f.onefun
597 # raise Exception if intervals are not consistent
598 if self.interval != f.interval: # pragma: no cover
599 raise IntervalMismatch(self.interval, f.interval)
600 else:
601 # let the lower level classes raise any other exceptions
602 g = f
603 onefun = getattr(self.onefun, methodname)(g, *args, **kwds)
604 return cls(onefun, self.interval)
606 method.__name__ = methodname
607 method.__doc__ = method.__doc__
608 setattr(Classicfun, methodname, method)
611for methodname in methods_onefun_binary:
612 add_binary_op(methodname)
614# ---------------------------
615# numpy universal functions
616# ---------------------------
619def add_ufunc(op):
620 """Add a NumPy universal function method to the Classicfun class.
622 This function creates a method that applies a NumPy universal function (ufunc)
623 to the values of a Classicfun and returns a new Classicfun representing the result.
625 Args:
626 op (callable): The NumPy universal function to apply.
628 Note:
629 The created method will have the same name as the NumPy function
630 and will take no arguments other than self.
631 """
633 @self_empty()
634 def method(self):
635 """Apply a NumPy universal function to this function.
637 This method applies a NumPy universal function (ufunc) to the values
638 of this function and returns a new function representing the result.
640 Returns:
641 Classicfun: A new function representing op(f(x)).
642 """
643 return self.__class__.initfun_adaptive(lambda x: op(self(x)), self.interval)
645 name = op.__name__
646 method.__name__ = name
647 method.__doc__ = method.__doc__
648 setattr(Classicfun, name, method)
651ufuncs = (
652 np.absolute,
653 np.arccos,
654 np.arccosh,
655 np.arcsin,
656 np.arcsinh,
657 np.arctan,
658 np.arctanh,
659 np.cos,
660 np.cosh,
661 np.exp,
662 np.exp2,
663 np.expm1,
664 np.log,
665 np.log2,
666 np.log10,
667 np.log1p,
668 np.sinh,
669 np.sin,
670 np.tan,
671 np.tanh,
672 np.sqrt,
673)
675for op in ufuncs:
676 add_ufunc(op)