Special Points in the Core API¶
Available since v0.12.0.
Declare domain kinks at construction time on ChebyshevApproximation.
PyChebyshev routes the build to a piecewise ChebyshevSpline under the
hood -- the call site stays simple.
Motivation: kinks break spectral convergence¶
Chebyshev polynomial approximation of an analytic function converges exponentially in the number of nodes (Trefethen 2013, Ch. 8):
where \(\rho > 1\) depends on the analyticity of \(f\). When \(f\) has a kink -- a point where \(f\) is continuous but its derivative is not -- this rate collapses to algebraic:
for a \(C^{k-1}\) function (Trefethen 2013, Ch. 6). The classic example is \(f(x) = |x|\) on \([-1, 1]\): even at \(N = 100\) nodes, the supremum error stays near \(10^{-2}\).
The fix is piecewise construction: split the domain at each kink so every piece is analytic, then interpolate each piece with Chebyshev. This restores exponential convergence per piece. MoCaX calls these "special points"; PyChebyshev now does too.
Cross-reference
This page is the ergonomic front door to
Chebyshev Splines. That page explains why piecewise
construction restores spectral convergence (Bernstein ellipses,
Gibbs phenomenon); this page covers how to declare kinks on the
main ChebyshevApproximation API without reaching for the spline
class directly.
API¶
from pychebyshev import ChebyshevApproximation
def f(x, _):
return abs(x[0])
cheb = ChebyshevApproximation(
f, 1, [[-1, 1]],
n_nodes=[[11, 11]], # per-piece Ns: 11 nodes each side
special_points=[[0.0]], # kink at origin
)
cheb.build()
special_points: list[list[float]] | None = None-- one inner list per dimension. Points must be strictly interior to the domain, sorted, and distinct. Empty lists mean "no kinks on this dim".n_nodesmust be nested (list[list[int | None]]) when any dim has special points. For each dim \(d\),len(n_nodes[d]) == len(special_points[d]) + 1.- When
special_pointsdeclares any kink,ChebyshevApproximation(...)returns aChebyshevSplineinstance (transparent dispatch; precedent:pathlib.Path()). All spline features --eval,integrate,roots, algebra (+ - * /),extrude/slice,save/load-- work identically.
Worked example 1: abs(x)¶
Without special points, the error plateaus:
cheb = ChebyshevApproximation(lambda x, _: abs(x[0]), 1, [[-1, 1]], [31])
cheb.build()
# max |cheb.eval(x) - |x|| ~ 0.03 across [-1, 1]
With special points, each linear piece is interpolated exactly:
cheb = ChebyshevApproximation(
lambda x, _: abs(x[0]), 1, [[-1, 1]],
n_nodes=[[11, 11]],
special_points=[[0.0]],
)
cheb.build()
# max error ~ 1e-14
Worked example 2: Barrier option payoff¶
A down-and-out call has a payoff with kinks at the barrier \(B\) and strike \(K\):
Two kinks (\(B\) and \(K\) assuming \(B < K\)), one smooth piece on each side:
def barrier_call(x, params):
S, K, B = x[0], params['K'], params['B']
if S <= B:
return 0.0
return max(S - K, 0.0)
cheb = ChebyshevApproximation(
lambda x, _: barrier_call(x, {'K': 100.0, 'B': 95.0}),
1, [[90.0, 150.0]],
n_nodes=[[7, 7, 15]],
special_points=[[95.0, 100.0]], # kinks at barrier and strike
)
cheb.build()
Piece 1 is identically zero, piece 2 is zero, piece 3 is linear in \(S\) -- all three recover exactly.
Per-piece auto-N with error_threshold¶
Combine kink declaration with v0.11 error-driven construction:
cheb = ChebyshevApproximation(
lambda x, _: abs(x[0]) ** 1.5, 1, [[-1, 1]],
special_points=[[0.0]],
error_threshold=1e-8,
)
cheb.build()
Each piece runs its own doubling loop until its DCT last-coefficient
drops below error_threshold. Pieces with sharper curvature land at
larger \(N\); smooth pieces terminate early. error_estimate() returns
the \(\max\) across pieces -- the global bound.
Relationship to ChebyshevSpline¶
ChebyshevSpline remains the direct API for power users and is what
ChebyshevApproximation(..., special_points=...) dispatches to:
from pychebyshev import ChebyshevSpline
sp = ChebyshevSpline(f, 1, [[-1, 1]], n_nodes=[[11, 11]], knots=[[0.0]])
special_points on the main constructor is an alias for knots on the
spline -- choose whichever reads better at your call site.
Current limitation: nodes() / from_values() + nested Ns¶
ChebyshevSpline.nodes() and ChebyshevSpline.from_values() (the
"precompute values, then build" workflow introduced in v0.10) accept
knots but not per-sub-interval nested n_nodes -- they require a
single flat n_nodes shared across all pieces in a dim. If you need
per-sub-interval Ns via that workflow, build via the __init__ path
instead:
# Works: per-sub-interval Ns via __init__
cheb = ChebyshevApproximation(
f, 1, [[-1, 1]], n_nodes=[[11, 15]], special_points=[[0.0]]
)
# Not yet supported: per-sub-interval Ns via nodes() + from_values()
# Nested n_nodes here raises NotImplementedError with a link back to
# this page.
# grids = ChebyshevSpline.nodes(1, [[-1, 1]], n_nodes=[[11, 15]], knots=[[0.0]])
# ChebyshevSpline.from_values(..., n_nodes=[[11, 15]], ...)
Extending the precompute workflow to nested Ns is tracked for a future release.
See Also¶
- Chebyshev Splines -- the underlying piecewise machinery, the Bernstein-ellipse argument for why splitting at kinks restores spectral convergence, and the Gibbs phenomenon for true discontinuities.
- Error-Driven Construction -- the
doubling loop reused per-piece when
error_thresholdis set. - Mathematical Concepts -- Bernstein ellipses and the exponential convergence rate for analytic functions.
References¶
- Trefethen, L. N. (2013). Approximation Theory and Approximation Practice. SIAM. Chapters 6 and 8.
- Ruiz, G. & Zeron, M. (2021). Machine Learning for Risk Calculations. Wiley Finance. Section 3.8.
- MoCaX Intelligence 4.3.1 manual,
MocaxSpecialPointsclass reference.