solprop.translate.chebyshev#
Adaptive piecewise Chebyshev representation of functions of z.
The solver rests on the cumulative linear operator Phi(omega, z) = int D dz: because
D is diagonal in omega it commutes with itself, so exp(Phi(z2) - Phi(z1)) is the
exact linear flow over any step. This module builds the representation that
carries it — a piecewise Chebyshev polynomial in z with array-valued coefficients, built
by Chebfun-style adaptive splitting, integrated exactly in the Chebyshev basis and
evaluated by Clenshaw recursion.
The design, the evidence behind each choice and the traps that are silent are in
docs/dev/stepper.md section 4; the manual page is
docs/guide/numerics/linear-operator.md.
Module Attributes
Default tolerance: the bound on the uniform error relative to the value scale. |
|
Default highest Chebyshev degree tried on one interval before it is bisected. |
|
Default interval budget; exceeding it sets the fallback flag rather than raising. |
|
Default narrowest interval the split may produce, as a fraction of the total span. |
|
Degree the adaptive search starts from, here and in the parameter expansion. |
|
Comparison-scale floor of the acceptance test, as a fraction of the global envelope. |
|
Margin of the local sampling-noise floor of the acceptance test (SEP 14 phase 5). |
Functions
|
Build an adaptive piecewise Chebyshev representation of |
|
Resolve the scalar paths first, then the array-valued function on their union. |
|
Evaluate one interval's Chebyshev series at one point, as NumPy's |
|
Merge a nested grid's samples with the ones a doubling added, along one axis. |
|
Transform samples at the Chebyshev-Lobatto points into Chebyshev coefficients. |
|
Return the Chebyshev-Lobatto points of |
|
Return the points a degree doubling adds to |
|
Transform one axis to Chebyshev coefficients and judge its tail. |
|
Drop the trailing coefficients of one axis that are transform noise. |
Classes
|
A piecewise Chebyshev polynomial in z with array-valued coefficients. |
- solprop.translate.chebyshev.DEFAULT_MAX_DEGREE: Final = 64#
Default highest Chebyshev degree tried on one interval before it is bisected.
- solprop.translate.chebyshev.DEFAULT_MAX_INTERVALS: Final = 512#
Default interval budget; exceeding it sets the fallback flag rather than raising.
- solprop.translate.chebyshev.DEFAULT_TOL: Final = 1e-12#
Default tolerance: the bound on the uniform error relative to the value scale.
- solprop.translate.chebyshev.ENVELOPE_FLOOR: Final = 1e-10#
Comparison-scale floor of the acceptance test, as a fraction of the global envelope.
An element whose own values are negligible beside the rest must not drive the refinement, so its comparison scale is floored here. Exposed because the round-off floor solprop.translate.linop measures has to be measured in this same norm to be comparable with the tail it is meant to bound.
- solprop.translate.chebyshev.INITIAL_DEGREE: Final = 8#
Degree the adaptive search starts from, here and in the parameter expansion.
- solprop.translate.chebyshev.MIN_WIDTH_FRACTION: Final = 1e-06#
Default narrowest interval the split may produce, as a fraction of the total span.
- class solprop.translate.chebyshev.PiecewiseChebyshev(coefficients, breakpoints, degrees, hard_kinks, budget_exceeded=False)[source]#
Bases:
objectA piecewise Chebyshev polynomial in z with array-valued coefficients.
On interval
ithe value issum_j coefficients[i, j] T_j(x)withx = (2 z - lo - hi) / (hi - lo)mapping[lo, hi]onto[-1, 1]. Intervals that need a lower degree carry zero coefficients in the high-order slots, which Clenshaw handles exactly and at no accuracy cost, so one vectorised pass evaluates every interval.coefficientsis listed first because jaxtyping resolves the symbolic axisn_intervals+1ofbreakpointsonly oncen_intervalsis bound by an earlier argument. Shape agreement between the fields is enforced by those annotations under the test session’s runtime checker; construction validates what shapes cannot express.- Attributes:
- coefficientsnumpy.ndarray
Chebyshev coefficients, shape
(n_intervals, n_deg, *value_shape), float64 or complex128. Units are those of the represented function.- breakpointsnumpy.ndarray
Interval edges in z, shape
(n_intervals + 1,), m, strictly increasing.- degreesnumpy.ndarray
Degree actually used on each interval, shape
(n_intervals,). Diagnostic: the zero padding makes it unnecessary for evaluation.- hard_kinksnumpy.ndarray
Positions in z, m, where the represented function itself jumps or kinks — stage boundaries, taper corners. Every entry is an interior breakpoint. These are tracked apart from the far more numerous edges the adaptive split introduces to resolve smooth variation, because steps must land on them and must not be capped by the others.
- budget_exceededbool, optional
True when the split ran out of its interval budget and accepted unresolved intervals. The caller falls back to per-substep Gauss-Legendre on it.
- Raises:
- TranslationError
If the interval count is zero, the breakpoints are not finite and strictly increasing, the coefficient dtype is neither float64 nor complex128, or a hard kink is not an interior breakpoint.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import build >>> phi = build(lambda z: 2.0 * z, 0.0, 1.0).integral() >>> float(phi(0.5)) 0.25
- at_positions(z, *, helpers=Helpers(name='numba'))[source]#
Evaluate at several positions, one Clenshaw pass for all of them.
Each row is what calling the table returns at that position, bit for bit: the same interval choice (an interior breakpoint belongs to the interval starting there), the same map onto
[-1, 1]and the same recursion, so a caller may batch the positions it knows without changing a value. Rows are kept as the last positions’ values, as a call of the table keeps them, so the next step’s anchor finds its weights already formed.The pass is the caller’s backend’s (helpers, SEP 29 section 6.6): a stage’s rows are evaluated by the backend that runs the stage. The kept rows are shared whichever implementation filled them, because every implementation performs the operations of a one-point call in its order under strict arithmetic, so a kept row is the value any of them would form.
- Parameters:
- znumpy.ndarray
Positions, m, float64, shape
(n_points,), each within the represented span (a few ulp outside is tolerated).- helperssolprop._helpers.Helpers, optional
The evaluating backend’s implementation; the shared Numba one by default, for callers outside a backend.
- Returns:
- numpy.ndarray
The values, shape
(n_points, *value_shape), read-only.
- Raises:
- TranslationError
If a position lies outside the represented span.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import build >>> f = build(lambda z: np.cos(z), 0.0, 1.0) >>> rows = f.at_positions(np.array([0.3, 0.7])) >>> bool(rows[0] == f(0.3)) and bool(rows[1] == f(0.7)) True
- breakpoints: Float[ndarray, 'n_intervals+1']#
- coefficients: Inexact[ndarray, 'n_intervals n_deg *value']#
- degrees: Int[ndarray, 'n_intervals']#
- derivative()[source]#
Return the derivative with respect to z.
- Returns:
- PiecewiseChebyshev
The derivative, with the same breakpoints and hard kinks and one fewer coefficient per interval (a single zero coefficient at the floor).
Examples
>>> from solprop.translate.chebyshev import build >>> f = build(lambda z: z**2, 0.0, 1.0).derivative() >>> bool(abs(f(0.5) - 1.0) < 1e-14) True
- hard_kinks: Float[ndarray, 'n_kinks']#
- integral()[source]#
Return the antiderivative, continuous across intervals and zero at the start.
Integration is a coefficient recurrence in the Chebyshev basis, not a quadrature, so it is exact for the polynomial it is given: the only approximation left in the cumulative operator is the interpolation of the integrand. The constant of integration lives entirely in the
T_0coefficient and is fixed from the left, interval by interval, so the result is continuous by construction.- Returns:
- PiecewiseChebyshev
The antiderivative with respect to z, vanishing at the first breakpoint, with the same breakpoints and hard kinks and one more coefficient per interval.
Examples
>>> from solprop.translate.chebyshev import build >>> phi = build(lambda z: 3.0 * z**2, 0.0, 2.0).integral() >>> bool(abs(phi(2.0) - 8.0) < 1e-14) True
- interval_containing(z)[source]#
Return the index of the interval that contains
z.A position sitting exactly on an interior breakpoint belongs to the interval starting there, and the right end of the span resolves to the last interval.
- Parameters:
- zfloat
Position, m. Positions up to
1e-10of the span outside are tolerated, because step schedules land on the ends and rounding puts them a few ulp out.
- Returns:
- int
Interval index in
[0, n_intervals - 1].
- Raises:
- TranslationError
If
zlies further outside the span than that tolerance.
Examples
>>> from solprop.translate.chebyshev import build >>> f = build(lambda z: z, 0.0, 1.0, mandatory_breakpoints=(0.5,)) >>> f.interval_containing(0.5) 1
- interval_width(z)[source]#
Return the width of the interval containing
z.This is the local step cap of
docs/dev/stepper.mdsection 3. The narrowest interval anywhere is not a cap: the split resolves a square-root entrance with intervals four orders of magnitude below the median, and capping globally on those would cost four orders of magnitude in step count.- Parameters:
- zfloat
Position, m.
- Returns:
- float
Width of the containing interval, m.
Examples
>>> from solprop.translate.chebyshev import build >>> f = build(lambda z: z, 0.0, 1.0, mandatory_breakpoints=(0.25,)) >>> f.interval_width(0.1) 0.25
- next_hard_kink(z)[source]#
Return the first hard kink beyond
z, or None if there is none.Strictly beyond, with the same edge tolerance as
interval_containing(), so a step starting on a kink clears it instead of being capped to zero length.- Parameters:
- zfloat
Position, m.
- Returns:
- float or None
Position of the next hard kink, m, or None when none remains.
Examples
>>> from solprop.translate.chebyshev import build >>> f = build(lambda z: z, 0.0, 1.0, mandatory_breakpoints=(0.4,)) >>> f.next_hard_kink(0.0), f.next_hard_kink(0.4) (0.4, None)
- solprop.translate.chebyshev.SAMPLING_NOISE_FACTOR: Final = 4.0#
Margin of the local sampling-noise floor of the acceptance test (SEP 14 phase 5).
An interval is also accepted when twice its coefficient tail is within this factor of the relative change its interpolant makes across the rounding of its own node positions,
eps |z| max|f'| / |f|: below that the sampled function carries no information about its own variation. Near a choked gas end, where the pressure goes as the square root of the distance to the exit, that floor rises above the requested tolerance within half a millimetre of a 4.2 m exit, and without it the split tiled that region at its width floor.
- solprop.translate.chebyshev.build(f, z0, z1, *, tol=1e-12, mandatory_breakpoints=(), n_max=64, min_width=None, max_intervals=512)[source]#
Build an adaptive piecewise Chebyshev representation of
fon[z0, z1].The split is Chebfun-style: sample at Chebyshev-Lobatto points, accept when the coefficient tail is negligible, otherwise double the degree up to
n_max, otherwise bisect. Declared breakpoints are mandatory edges and become the hard kinks, so a jump that is known in advance is never crossed; a jump that is not declared is located by the bisection down tomin_width.- Parameters:
- fcallable
Sampled vectorised:
f(z)takes positions of shape(n_z,)in m and returns an array of shape(n_z, *value_shape), float64 or complex128. Sampling is vectorised because the operator is evaluated over a whole omega grid at once.- z0, z1float
Ends of the domain, m, with
z1 > z0.- tolfloat, optional
Target bound on the uniform error relative to the value scale,
> 0. An interval is accepted when twice the relative coefficient tail is at mosttol; the factor two is the Chebyshev aliasing bound, sotolbounds the error itself. Use1e-12when the frame quantities are analytic,1e-8when differenced.- mandatory_breakpointsarray_like, optional
Positions in z, m, inside
[z0, z1]that must be interval edges: stage boundaries, taper corners. The interior ones become thePiecewiseChebyshevhard kinks.- n_maxint, optional
Highest degree tried on one interval before bisecting,
>= 3.- min_widthfloat or None, optional
Narrowest interval the split may produce, m. None means
MIN_WIDTH_FRACTION * (z1 - z0). It is absolute and computed once from the whole span: derived per call it lets an already tiny segment recurse several further decades and exhaust the budget without getting closer to resolving anything.- max_intervalsint, optional
Interval budget,
>= 1. Exceeding it stops the split, accepts what is left and sets thePiecewiseChebyshevfallback flag rather than raising. Mandatory breakpoints are honoured whatever the budget, so a domain declaring more of them than the budget allows still gets one interval per segment.
- Returns:
- PiecewiseChebyshev
The representation of
f. CallPiecewiseChebyshev.integral()for its cumulative integral.
- Raises:
- TranslationError
If the domain, the tolerance, the degree or the budget is invalid, if a mandatory breakpoint lies outside the domain, or if
freturns the wrong leading length, an empty value shape, or a non-finite sample.
Notes
What the build reproduces exactly, to round-off, whatever
tolis:any polynomial in z of degree at most
n_max - 3on each declared segment. The interpolation at the Chebyshev-Lobatto points is exact for it and the tail test then sees three vanishing coefficients. Degreesn_max - 2ton_maxare still represented exactly but are not recognised as resolved, so the split bisects them until it reachesmin_width;a constant or piecewise-constant function, as one interval of degree 0 per declared segment, whose integral is one interval of degree 1;
the round trip
build(f).integral().derivative(), which returns the original coefficients up to the round-off of the two recurrences.
The other round trip, the one the interaction picture needs –
exp(-dPhi)as the exact inverse ofexp(+dPhi)– is the caller’s: negate one evaluated exponent rather than evaluating the table twice.Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import build >>> f = build(lambda z: np.exp(-z), 0.0, 1.0) >>> int(f.degrees.size) 1 >>> bool(abs(f(0.25) - np.exp(-0.25)) < 1e-14) True
- solprop.translate.chebyshev.build_from_paths(paths, f_of_params, z0, z1, *, tol=1e-12, mandatory_breakpoints=(), n_max=64, min_width=None, max_intervals=512)[source]#
Resolve the scalar paths first, then the array-valued function on their union.
The scalar parameter paths — density, core radius, any other profile of z — are cheap to evaluate and carry the structure that makes the operator hard: they are resolved first, within each declared segment separately so that a declared kink is never crossed by a path fit, and the union of all the edges they produce becomes the mandatory split of the expensive array-valued function.
- Parameters:
- pathssequence of callable
Scalar paths, each mapping positions of shape
(n_z,)in m to values of shape(n_z,).- f_of_paramscallable
Called as
f_of_params(z, *values)with the positions and one array of shape(n_z,)per path, returning an array of shape(n_z, *value_shape). The positions come first so that an operator which also depends on z directly – a co-moving frame quantity, say – needs no path of its own, and so that a build with no paths at all is exactlybuild().- z0, z1float
Ends of the domain, m, with
z1 > z0.- tolfloat, optional
Target bound on the uniform error relative to the value scale; see
build().- mandatory_breakpointsarray_like, optional
Positions in z, m, that must be interval edges; the interior ones become the hard kinks.
- n_maxint, optional
Highest degree tried on one interval before bisecting,
>= 3.- min_widthfloat or None, optional
Narrowest interval the split may produce, m; see
build().- max_intervalsint, optional
Interval budget for each stage of the build,
>= 1.
- Returns:
- PiecewiseChebyshev
The representation of
f_of_paramsevaluated along the paths.
- Raises:
- TranslationError
For the same reasons as
build().
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import build_from_paths >>> density = lambda z: 1.0 + z >>> f = build_from_paths([density], lambda z, rho: rho**2, 0.0, 1.0) >>> bool(abs(f(0.5) - 2.25) < 1e-14) True
- solprop.translate.chebyshev.clenshaw(x, coefficients)[source]#
Evaluate one interval’s Chebyshev series at one point, as NumPy’s
chebval.The same values bit for bit, compiled: a degree-64 series of 18 values costs under a microsecond against NumPy’s 36 µs. Until numba is loaded NumPy evaluates instead: a run on the native backend needs numba for nothing else, and importing it costs more than a set-up’s thousand NumPy evaluations.
- Parameters:
- xfloat
Point in the interval’s own coordinate, in [-1, 1].
- coefficientsnumpy.ndarray
The series’ coefficients, lowest degree first, shape
(n_terms, *value), float64 or complex128.
- Returns:
- numpy.ndarray
The value, shape
value, of the coefficients’ dtype.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import clenshaw >>> float(clenshaw(0.5, np.array([1.0, 2.0, 3.0]))) 0.5
- solprop.translate.chebyshev.interleave_axis(coarse, fine, axis)[source]#
Merge a nested grid’s samples with the ones a doubling added, along one axis.
- Parameters:
- coarsenumpy.ndarray
Samples on the degree
dgrid, withd + 1entries along axis.- finenumpy.ndarray
Samples at the
dpointsrefinement_points()added, same shape otherwise.- axisint
The axis the doubling ran along.
- Returns:
- numpy.ndarray
Samples on the degree
2 dgrid, with2 d + 1entries along axis.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import interleave_axis >>> coarse = np.array([[0.0, 2.0]]) >>> interleave_axis(coarse, np.array([[1.0]]), 1).tolist() [[0.0, 1.0, 2.0]]
- solprop.translate.chebyshev.lobatto_coefficients(values)[source]#
Transform samples at the Chebyshev-Lobatto points into Chebyshev coefficients.
The discrete Chebyshev transform is the type-I DCT of the samples up to the end weights; taking it as the FFT of the mirror extension lets complex-valued integrands go through the same path as real ones. The coefficients are those
numpy.polynomial.chebyshev.chebval()andPiecewiseChebyshevevaluate, so one transform serves the adaptive builder here and the fixed-section expansions ofsolprop.translate.coupled.- Parameters:
- valuesnumpy.ndarray
Samples at
lobatto_points()in increasing position, shape(n_deg, *value_shape).
- Returns:
- numpy.ndarray
Coefficients of
sum_j c_j T_j, shape(n_deg, *value_shape), real when the samples are real.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import lobatto_coefficients, lobatto_points >>> nodes = lobatto_points(4, -1.0, 1.0) >>> lobatto_coefficients(nodes**2).round(12) + 0.0 array([0.5, 0. , 0.5, 0. , 0. ])
- solprop.translate.chebyshev.lobatto_points(degree, lo, hi)[source]#
Return the Chebyshev-Lobatto points of
degreemapped onto[lo, hi].Unlike the z sampler these are not inset at the ends: a parameter range has no discontinuity sitting on its own boundary, and its ends are values the paths actually reach, where the function must be evaluated as it stands.
- Parameters:
- degreeint
Polynomial degree,
>= 1.- lo, hifloat
Ends of the range, in the parameter’s own units, with
hi > lo.
- Returns:
- numpy.ndarray
The
degree + 1points, increasing from lo to hi.
Examples
>>> from solprop.translate.chebyshev import lobatto_points >>> [round(value, 12) for value in lobatto_points(2, 0.0, 1.0).tolist()] [0.0, 0.5, 1.0]
- solprop.translate.chebyshev.refinement_points(degree, lo, hi)[source]#
Return the points a degree doubling adds to
lobatto_points().- Parameters:
- degreeint
Current degree; the result completes the grid of degree
2 * degree.- lo, hifloat
Ends of the range, in the parameter’s own units.
- Returns:
- numpy.ndarray
The
degreenew points, increasing, all interior.
Examples
>>> from solprop.translate.chebyshev import refinement_points >>> [round(value, 6) for value in refinement_points(1, 0.0, 1.0).tolist()] [0.5]
- solprop.translate.chebyshev.resolve_axis(values, axis, *, tol)[source]#
Transform one axis to Chebyshev coefficients and judge its tail.
The samples along the other axes are left as they are, so the acceptance test measures the tail per element against a genuine function value, as it does for the state in the one-dimensional build, rather than against a coefficient that may itself be negligible.
- Parameters:
- valuesnumpy.ndarray
Samples at the Chebyshev-Lobatto points of axis, of any shape.
- axisint
The axis to transform.
- tolfloat
Target bound on the uniform error relative to the value scale,
> 0. The criterion isbuild()’s: twice the relative coefficient tail, the factor two being the Chebyshev aliasing bound.
- Returns:
- numpy.ndarray
The same array with axis holding Chebyshev coefficients.
- bool
Whether that axis is resolved to tol.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import lobatto_points, resolve_axis >>> x = lobatto_points(8, -1.0, 1.0) >>> coefficients, resolved = resolve_axis(2.0 * x**2 - 1.0, 0, tol=1e-12) >>> resolved, [abs(round(value, 12)) for value in coefficients[:3].tolist()] (True, [0.0, 0.0, 1.0])
- solprop.translate.chebyshev.trim_axis(coefficients, axis)[source]#
Drop the trailing coefficients of one axis that are transform noise.
- Parameters:
- coefficientsnumpy.ndarray
Chebyshev coefficients along axis.
- axisint
The axis to trim.
- Returns:
- numpy.ndarray
The same array with axis shortened to the coefficients that carry signal; at least one is always kept.
Examples
>>> import numpy as np >>> from solprop.translate.chebyshev import trim_axis >>> trim_axis(np.array([[1.0, 0.0, 0.0]]), 1).shape (1, 1)