Coverage for src/chebpy/_pointwise.py: 100%
78 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-22 21:50 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-22 21:50 +0000
1"""Pointwise and step-function operations on :class:`~chebpy.chebfun.Chebfun`.
3Kept separate from :mod:`chebpy.chebfun` so the root-splitting machinery behind
4``abs``, ``sign``, ``ceil``, ``floor`` and pointwise ``maximum``/``minimum``
5does not bloat the main class. Each public Chebfun method is a thin wrapper
6around the corresponding function here.
8Every function assumes non-empty operands; the wrapping Chebfun methods handle
9the empty case (via ``@self_empty()``) before delegating.
10"""
12from __future__ import annotations
14from collections.abc import Callable
15from typing import TYPE_CHECKING, Any
17import numpy as np
19from .bndfun import Bndfun
20from .exceptions import SupportMismatch
21from .settings import _preferences as prefs
23if TYPE_CHECKING:
24 from .chebfun import Chebfun
27def absolute(f: Chebfun) -> Chebfun:
28 """Return ``|f|`` by splitting the domain at the roots of ``f``."""
29 newdom = f.domain.merge(f.roots())
30 funs = [x.absolute() for x in f._break(newdom)]
31 return f.__class__(funs)
34def sign(f: Chebfun) -> Chebfun:
35 """Return the piecewise sign of ``f``.
37 Splits the domain at the roots of ``f`` and builds a constant piece with
38 the appropriate sign on each sub-interval; breakpoints at roots are set to
39 ``0``.
40 """
41 roots = f.roots()
42 newdom = f.domain.merge(roots)
43 funs = []
44 for fun in f._break(newdom):
45 mid = fun.support[0] + 0.5 * (fun.support[-1] - fun.support[0])
46 s = float(np.sign(float(f(mid))))
47 funs.append(Bndfun.initconst(s, fun.interval))
48 result = f.__class__(funs)
49 # Set breakdata: at roots sign is 0, elsewhere use sign of function.
50 htol = max(1e2 * f.hscale * prefs.eps, prefs.eps)
51 for bp in result.breakpoints:
52 if roots.size > 0 and np.any(np.abs(bp - roots) <= htol):
53 result.breakdata[bp] = 0.0
54 else:
55 result.breakdata[bp] = float(np.sign(float(f(bp))))
56 return result
59def ceil(f: Chebfun) -> Chebfun:
60 """Return the piecewise ceiling of ``f``."""
61 return _step_at_integer_crossings(f, np.ceil)
64def floor(f: Chebfun) -> Chebfun:
65 """Return the piecewise floor of ``f``."""
66 return _step_at_integer_crossings(f, np.floor)
69def _step_at_integer_crossings(f: Chebfun, rounder: Callable[[Any], Any]) -> Chebfun:
70 """Build the piecewise-constant ``rounder(f)`` (used by :func:`ceil`/:func:`floor`).
72 Splits the domain where ``f`` crosses integer values and assigns each piece
73 the rounded value at its midpoint.
74 """
75 crossings = _integer_crossings(f)
76 newdom = f.domain.merge(crossings)
77 funs = []
78 for fun in f._break(newdom):
79 mid = fun.support[0] + 0.5 * (fun.support[-1] - fun.support[0])
80 c = float(rounder(float(f(mid))))
81 funs.append(Bndfun.initconst(c, fun.interval))
82 result = f.__class__(funs)
83 for bp in result.breakpoints:
84 result.breakdata[bp] = float(rounder(float(f(bp))))
85 return result
88def _integer_crossings(f: Chebfun) -> np.ndarray:
89 """Return the x-values where ``f`` crosses an integer value.
91 Finds the roots of ``f - n`` for each integer ``n`` in the range of ``f``.
92 """
93 all_values = np.concatenate([fun.values() for fun in f])
94 lo = int(np.floor(np.min(all_values)))
95 hi = int(np.ceil(np.max(all_values)))
96 crossings = []
97 for n in range(lo, hi + 1):
98 shifted = f - n
99 crossings.extend(shifted.roots().tolist())
100 return np.array(crossings)
103def maximum_minimum(f: Chebfun, other: Chebfun, comparator: Callable[..., bool]) -> Any:
104 """Return the pointwise maximum or minimum of ``f`` and ``other``.
106 ``comparator`` selects which operand wins on each sub-interval:
107 ``operator.ge`` gives the pointwise maximum and ``operator.lt`` the minimum.
108 The switch points are the roots of ``f - other``.
109 """
110 if f.isempty or other.isempty:
111 return f.__class__.initempty()
113 # Find the intersection of domains.
114 try:
115 # Try to use union if supports match.
116 newdom = f.domain.union(other.domain)
117 except SupportMismatch:
118 # If supports don't match, find the intersection.
119 a_min, a_max = f.support
120 b_min, b_max = other.support
122 c_min = max(a_min, b_min)
123 c_max = min(a_max, b_max)
125 # If there's no intersection, return empty.
126 if c_min >= c_max:
127 return f.__class__.initempty()
129 # Restrict both functions to the intersection and recurse.
130 f_restricted = f.restrict([c_min, c_max])
131 other_restricted = other.restrict([c_min, c_max])
132 return maximum_minimum(f_restricted, other_restricted, comparator)
134 roots = (f - other).roots()
135 newdom = newdom.merge(roots)
136 switch = newdom.support.merge(roots)
138 if switch.size == 0: # pragma: no cover
139 return f.__class__.initempty()
141 keys = 0.5 * ((-1) ** np.arange(switch.size - 1) + 1)
142 if switch.size > 0 and comparator(other(switch[0]), f(switch[0])):
143 keys = 1 - keys
144 funs = np.array([])
145 for interval, use_self in zip(switch.intervals, keys, strict=False):
146 subdom = newdom.restrict(interval)
147 subfun = f.restrict(subdom) if use_self else other.restrict(subdom)
148 funs = np.append(funs, subfun.funs)
149 return f.__class__(funs)