SpectralKit
This is a very simple package for building blocks of spectral methods. Its intended audience is users who are familiar with the theory and practice of these methods, and prefer to assemble their code from modular building blocks, potentially reusing calculations. If you need an introduction, a book like Boyd (2001): Chebyshev and Fourier spectral methods is a good place to start.
This package was designed primarily for solving functional equations, as usually encountered in economics when solving discrete and continuous-time problems. Key features include
- evaluation of univariate and multivatiate basis functions, including Smolyak combinations,
- transformed to the relevant domains of interest, eg $[a,b] × [0,∞)$,
- (partial) derivatives, with correct limits at endpoints,
- allocation-free, thread safe linear combinations for the above with a given set of coefficients,
- using static arrays extensively to avoid allocation and unroll some loops.
While there is some functionality in this package to fit approximations to existing functions, it does not use optimized algorithms (DCT) for that, as it was optimized for mapping a set of coefficients to residuals of functional equations at gridpoints.
Also, while the package should interoperate seamlessly with most AD frameworks, only the derivative API (explained below) is guaranteed to have correct derivatives of limits near infinity.
Concepts
In this package,
A basis is a finite family of functions for approximating other functions. The
dimensionof a basis tells you how many functions are in there, whiledomaincan be used to query its domain.A
gridis vector of suggested gridpoints for evaluating the function to be approximated that has useful theoretical properties. You can contruct acollocation_matrixusing this grid (or any other set of points). Grids are associated with bases at the time of their construction: a basis with the same set of functions can have different grids.basis_atreturns an iterator for evaluating basis functions at an arbitrary point inside their domain. This iterator is meant to be heavily optimized and non-allocating.linear_combinationis a convenience wrapper for obtaining a linear combination of basis functions at a given point.
Univariate basis example
We construct a basis of Chebyshev polynomials on $[0, 4]$. This requires a transformation since their canonical domain is $[-1,1]$. Other transformations include SemiInfRational for $[A, \infty]$ intervals and InfRational for $[-\infty, \infty]$ intervals.
We display the domian and the dimension (number of basis functions).
julia> using SpectralKitjulia> basis = Chebyshev(InteriorGrid(), 5) ∘ BoundedLinear(0, 4)SpectralKit.TransformedBasis{Chebyshev{InteriorGrid}, BoundedLinear{Float64}}(Chebyshev polynomials (1st kind), InteriorGrid(), dimension: 5, (0.0,4.0) ↔ domain [linear transformation])julia> domain(basis)[0.0,4.0]julia> dimension(basis)5
We have chosen an interior grid, shown below. We collect the result for the purpose of this tutorial, since grid returns an iterable to avoid allocations.
julia> collect(grid(basis))5-element Vector{Float64}: 3.9021130325903073 3.1755705045849463 2.0 0.8244294954150537 0.09788696740969272
We can show evaluate the basis functions at a given point. Again, it is an iterable, so we collect to show it here.
julia> collect(basis_at(basis, 0.41))5-element Vector{Float64}: 1.0 -0.795 0.2640500000000001 0.37516049999999984 -0.8605551949999999
We can evaluate linear combination as directly, or via partial application.
julia> θ = [1, 0.5, 0.2, 0.3, 0.001]; # a vector of coefficientsjulia> x = 0.410.41julia> linear_combination(basis, θ, x) # combination at some value0.7669975948050001julia> linear_combination(basis, θ)(x) # also as a callable0.7669975948050001
We can also evaluate derivatives of either the basis or the linear combination at a given point. Here we want the derivatives up to order 3.
julia> dx = (𝑑^2)(x)0.41 + 1.0⋅Δ + 0.0⋅Δ²julia> collect(basis_at(basis, dx))5-element Vector{SpectralKit.𝑑Expansion{3, Float64}}: 1.0 + 0.0⋅Δ + 0.0⋅Δ² -0.795 + 0.5⋅Δ + 0.0⋅Δ² 0.2640500000000001 + -1.59⋅Δ + 1.0⋅Δ² 0.37516049999999984 + 2.2921500000000004⋅Δ + -4.7700000000000005⋅Δ² -0.8605551949999999 + -1.6793580000000008⋅Δ + 11.168600000000001⋅Δ²julia> fdx = linear_combination(basis, θ, dx)0.7669975948050001 + 0.617965642⋅Δ + -1.2198314000000001⋅Δ²julia> fdx[0] # the value0.7669975948050001julia> fdx[1] # the first derivative0.617965642
Having an approximation, we can embed it in a larger basis, extending the coefficients accordingly.
julia> basis2 = Chebyshev(EndpointGrid(), 8) ∘ transformation(basis) # 8 Chebyshev polynomialsSpectralKit.TransformedBasis{Chebyshev{EndpointGrid}, BoundedLinear{Float64}}(Chebyshev polynomials (1st kind), EndpointGrid(), dimension: 8, (0.0,4.0) ↔ domain [linear transformation])julia> is_subset_basis(basis, basis2) # we could augment θ …truejulia> augment_coefficients(basis, basis2, θ) # … so let's do it8-element Vector{Float64}: 1.0 0.5 0.2 0.3 0.001 0.0 0.0 0.0
Multivariate (Smolyak) approximation example
We set up a Smolyak basis to approximate functions on $[-1,2] \times [-3, \infty]$, where the second dimension has a scaling of $3$.
using SpectralKit, StaticArrays
basis = smolyak_basis(Chebyshev, InteriorGrid2(), SmolyakParameters(3), 2)
ct = coordinate_transformations(BoundedLinear(-1, 2.0), SemiInfRational(-3.0, 3.0))
basis_t = basis ∘ ctSpectralKit.TransformedBasis{SpectralKit.SmolyakBasis{SpectralKit.SmolyakIndices{2, 15, 3, 3, 4}, Chebyshev{InteriorGrid2}}, SpectralKit.CoordinateTransformations{Tuple{BoundedLinear{Float64}, SemiInfRational{Float64}}}}(Sparse multivariate basis on ℝ²
Smolyak indexing, ∑bᵢ ≤ 3, all bᵢ ≤ 3, dimension 49
using Chebyshev polynomials (1st kind), InteriorGrid2(), dimension: 15, coordinate transformations
(-1.0,2.0) ↔ domain [linear transformation]
(-3.0,∞) ↔ domain [rational transformation with scale 3.0])Note how the basis can be combined with a transformation using ∘.
We will approximate the following function:
f2((x1, x2)) = exp(x1) + exp(-abs2(x2))f2 (generic function with 1 method)We find the coefficients by solving with the collocation matrix.
θ = collocation_matrix(basis_t) \ f2.(grid(basis_t))49-element Vector{Float64}:
2.7793220217324706
3.2369886430572143
1.1139902424577899
0.2663479965030996
0.04859825644525028
0.007157295461750643
0.0008829533666788979
9.366852829718765e-5
8.713768913578302e-6
7.216598510789816e-7
⋮
-2.8429038809505643e-16
0.03010775018319918
0.048411759340962084
-0.03222472955157677
-0.16530952363028062
0.01639663318432438
0.012259562724398244
-0.01591087240587917
-0.14085219047438763Finally, we check the approximation at a point.
z = (0.5, 0.7) # evaluate at this point
isapprox(f2(z), linear_combination(basis_t, θ)(z), rtol = 0.005)trueInfinite endpoints
Values and derivatives at $\pm\infty$ should provide the correct limits.
julia> using SpectralKitjulia> basis = Chebyshev(InteriorGrid(), 4) ∘ InfRational(0.0, 1.0)SpectralKit.TransformedBasis{Chebyshev{InteriorGrid}, InfRational{Float64}}(Chebyshev polynomials (1st kind), InteriorGrid(), dimension: 4, (-∞,∞) ↔ domain [rational transformation with center 0.0, scale 1.0])julia> collect(basis_at(basis, 𝑑(Inf)))4-element Vector{SpectralKit.𝑑Expansion{2, Float64}}: 1.0 + 0.0⋅Δ 1.0 + 0.0⋅Δ 1.0 + 0.0⋅Δ 1.0 + 0.0⋅Δjulia> collect(basis_at(basis, 𝑑(-Inf)))4-element Vector{SpectralKit.𝑑Expansion{2, Float64}}: 1.0 + 0.0⋅Δ -1.0 + 0.0⋅Δ 1.0 + -0.0⋅Δ -1.0 + 0.0⋅Δ
Constructing bases
Grid specifications
SpectralKit.EndpointGrid — Typestruct EndpointGrid <: SpectralKit.AbstractGridGrid that includes endpoints (eg Gauss-Lobatto).
For small dimensions may fall back to a grid that does not contain endpoints.
SpectralKit.InteriorGrid — Typestruct InteriorGrid <: SpectralKit.AbstractGridGrid with interior points (eg Gauss-Chebyshev).
SpectralKit.InteriorGrid2 — Typestruct InteriorGrid2 <: SpectralKit.AbstractGridGrid with interior points that results in smaller grids than InteriorGrid when nested. Equivalent to an EndpointGrid with endpoints dropped.
Domains and transformations
A transformation maps values between a domain, usually specified by the basis, and the (co)domain that is specified by a transformation. Transformations are not required to be subtypes of anything, but need to support
SpectralKit.transform_to — Functiontransform_to(domain, transformation, x)
Transform x to domain using transformation.
domain can be replaced by basis for a shortcut which uses domain(basis).
Transformations to infinity make sure that $\pm\infty$ is mapped to the limit for values and derivatives.
SpectralKit.transform_from — Functiontransform_from(domain, transformation, x)
Transform x from domain using transformation.
domain can be replaced by basis for a shortcut which uses domain(basis).
Transformations to infinity make sure that $\pm\infty$ is mapped to the limit for values and derivatives.
SpectralKit.domain — Functiondomain(basis)
The domain of a function basis.
domain(transformation)
The (co)domain of a transformation. The “other” domain (codomain, depending on the mapping) is provided explicitly for transformations, and should be compatible with thedomain of the basis.
See domain_kind for the interface supported by domains.
In most cases you do not need to specify a domain directly: transformations specify their domains (eg from $(0, ∞)$), and the codomain is determined by a basis. However, the following can be used to construct and query some concrete domains.
SpectralKit.domain_kind — Functiondomain_kind(x)
Return the kind of a domain type (preferred) or value. Errors for objects/types which are not domains. Also works for domains of transformations.
The following return values are possible:
:univariate, the bounds of which can be accessed usingminimum,maximum, and
extrema,
:multivariate, which supportslength,getindex([]), and conversion withTuple.
SpectralKit.coordinate_domains — Functioncoordinate_domains(domains)
Create domains which are the product of univariate domains. The result support length, indexing with integers, and Tuple for conversion.
coordinate_domains(domains)
coordinate_domains(_, domain)
Create a coordinate domain which is the product of domain repeated N times.
coordinate_domains(N, domain)
Bases are defined on a canonical domain, such as $[-1, 1]$ for Chebyshev polynomials. Transformations map other uni- and multivariate sets into these domains.
SpectralKit.BoundedLinear — Typestruct BoundedLinear{T<:Real} <: SpectralKit.AbstractUnivariateTransformationTransform the domain to y ∈ (a, b), using $y = x ⋅ s + m$.
m and s are calculated and checked by the constructor; a < b is enforced.
SpectralKit.InfRational — TypeInfRational(A, L)
The domain transformed to (-Inf, Inf) using $y = A + L ⋅ x / √(1 - x^2)$, with L > 0.
Example mappings (for domain $(-1,1)$)
- $0 ↦ A$
- $±0.5 ↦ A ± L / √3$
SpectralKit.SemiInfRational — TypeSemiInfRational(A, L)
The domian transformed to [A, Inf) (when L > 0) or (-Inf,A] (when L < 0) using $y = A + L ⋅ (1 + x) / (1 - x)$.
When used with Chebyshev polynomials, also known as a “rational Chebyshev” basis.
Example mappings for the domain $(-1,1)$
- $-1/2 ↦ A + L / 3$
- $0 ↦ A + L$
- $1/2 ↦ A + 3 ⋅ L$
SpectralKit.coordinate_transformations — Functioncoordinate_transformations(transformations)
Wrapper for coordinate-wise transformations. To extract components, convert to Tuple.
julia> using StaticArrays
julia> ct = coordinate_transformations(BoundedLinear(0, 2), SemiInfRational(2, 3))
coordinate transformations
(0.0,2.0) ↔ domain [linear transformation]
(2,∞) ↔ domain [rational transformation with scale 3]
julia> d1 = domain(Chebyshev(InteriorGrid(), 5))
[-1,1]
julia> dom = coordinate_domains(d1, d1)
[-1,1]²
julia> x = transform_from(dom, ct, (0.4, 0.5))
(1.4, 11.0)
julia> y = transform_to(dom, ct, x)
(0.3999999999999999, 0.5)Univariate bases
Currently, only Chebyshev polynomials are implemented. Univariate bases operate on real numbers.
SpectralKit.Chebyshev — Typestruct Chebyshev{K<:SpectralKit.AbstractGrid} <: SpectralKit.UnivariateBasisThe first N Chebyhev polynomials of the first kind, defined on [-1,1].
Multivariate bases
Multivariate bases operate on tuples or vectors (StaticArrays.SVector is preferred for performance, but all <:AbstractVector types should work).
SpectralKit.SmolyakParameters — TypeSmolyakParameters(B)
SmolyakParameters(B, M)
Parameters for Smolyak grids that are independent of the dimension of the domain.
Polynomials are organized into blocks (of eg 1, 2, 2, 4, 8, 16, …) polynomials (and corresponding gridpoints), indexed with a block index b that starts at 0. B ≥ ∑ bᵢ and 0 ≤ bᵢ ≤ M constrain the number of blocks along each dimension i.
M > B is not an error, but will be normalized to M = B with a warning.
SpectralKit.smolyak_basis — Functionsmolyak_basis(
univariate_family,
grid_kind,
smolyak_parameters,
_
)
Create a sparse Smolyak basis.
Arguments
univariate_family: should be a callable that takes agrid_kindand adimensionparameter, egChebyshev.grid_kind: the grid kind, egInteriorGrid()etc.smolyak_parameters: the Smolyak grid specification parameters, seeSmolyakParameters.N: the dimension. wrapped in aValfor type stability, a convenience constructor also takes integers.
Example
julia> basis = smolyak_basis(Chebyshev, InteriorGrid(), SmolyakParameters(3), 2)
Sparse multivariate basis on ℝ²
Smolyak indexing, ∑bᵢ ≤ 3, all bᵢ ≤ 3, dimension 81
using Chebyshev polynomials (1st kind), InteriorGrid(), dimension: 27
julia> dimension(basis)
81
julia> domain(basis)
[-1,1]²Properties
Grids nest: increasing arguments of SmolyakParameters result in a refined grid that contains points of the cruder grid.
Using bases
Introspection
SpectralKit.is_function_basis — Functionis_function_basis(::Type{F})
is_function_basis(f::F)
Test if the argument (value or type) is a function basis, supporting the following interface:
domainfor querying the domain,dimensionfor the dimension,basis_atfor function evaluation,gridto obtain collocation points.lengthandgetindexfor multivariate bases(domain_kind(domain(basis)) == :multivariate), getindex returns a compatible marginal basis
linear_combination and collocation_matrix are also supported, building on the above.
Can be used on both types (preferred) and values (for convenience).
SpectralKit.dimension — Functiondimension(basis)
Return the dimension of basis, a positive Int.
SpectralKit.transformation — Functiontransformation(basis)
Return the transformation of transformed bases, or nothing it not applicable.
See also domain.
Evaluation
SpectralKit.basis_at — Functionbasis_at(basis, x)
Return an iterable with known element type and length (Base.HasEltype(), Base.HasLength()) of basis functions in basis evaluated at x.
Univariate bases operate on real numbers, while for multivariate bases, Tuples or StaticArrays.SVector are preferred for performance, though all <:AbstractVector types should work.
Methods are type stable.
Consequences are undefined when evaluating outside the domain.
SpectralKit.linear_combination — Functionlinear_combination(basis, θ, x)
Evaluate the linear combination of $∑ θₖ⋅fₖ(x)$ of function basis $f₁, …$ at x, for the given order.
The length of θ should equal dimension(θ).
linear_combination(basis, θ)
Return a callable that calculates linear_combination(basis, θ, x) when called with x.
You can use linear_combination(basis, θ) ∘ transformation for domain transformations, though working with basis ∘ transformation may be preferred.
Grids and collocation
SpectralKit.grid — Functiongrid([T], basis)
Return an iterator for the grid recommended for collocation, with dimension(basis) elements.
T for the element type of grid coordinates, and defaults to Float64. Methods are type stable.
SpectralKit.collocation_matrix — Functioncollocation_matrix(basis)
collocation_matrix(basis, x)
Convenience function to obtain a “collocation matrix” at points x, which is assumed to have a concrete eltype. The default is x = grid(basis), specialized methods may exist for this when it makes sense.
The collocation matrix may not be an AbstractMatrix, all it needs to support is C \ y for compatible vectors y = f.(x).
Methods are type stable. The elements of x can be derivatives, see 𝑑.
Augment coordinates for a wider basis
SpectralKit.augment_coefficients — Functionaugment_coefficients(basis1, basis2, θ1)
Return a set of coefficients θ2 for basis2 such that
linear_combination(basis1, θ1, x) == linear_combination(basis2, θ2, x)for any x in the domain. In practice this means padding with zeros.
Throw a ArgumentError if the bases are incompatible with each other or x, or this is not possible. Methods may not be defined for incompatible bases, compatibility between bases can be checked with is_subset_basis.
SpectralKit.is_subset_basis — Functionis_subset_basis(basis1, basis2)
Return a Bool indicating whether coefficients in basis1 can be augmented to basis2 with augment_coefficients.
true does not mean that coefficients from basis1 can just be padded with zeros, since they may be in different positions. Always use augment_coefficients.
Derivatives
API for derivatives is still experimental and subject to change.
For univariate functions, use 𝑑. For multivariate functions, use partial derivatives with ∂.
SpectralKit.𝑑 — ConstantA callable that calculates the value and the derivative of the argument. Higher-order derivatives can be obtained by using an exponent, or multiplication.
julia> 𝑑(2.0)
2.0 + 1.0⋅Δ
julia> (𝑑^3)(2.0)
2.0 + 1.0⋅Δ + 0.0⋅Δ² + 0.0⋅Δ³Note that non-literal exponentiation requires ^Val(y), for type stability.
See linear_combination for examples of evaluating derivatives of basis functions and linear combinations.
SpectralKit.∂ — Function∂(I)
Partial derivatives along the given coordinates.
!!! NOTE Partial derivatives are currently experimental and not heavily tested. API may change at any point without prior notice or deprecation.
The following are equivalent, and represent $\partial_1 \partial^2_2$, ie the first derivative along the first axis, and the second partial derivative along the second axis.
julia> ∂(1, 2)
∂(1, 2)
julia> ∂((1, 2))
∂(1, 2)Only the vararg form allows trailing zeros, which are stripped:
julia> ∂(1, 0)
∂(1)
julia> ∂((1, 0))
ERROR: ArgumentError: I ≡ () || last(I) ≠ 0 must hold.Use the empty form for no derivatives:
julia> ∂()
∂()Combine derivatives using union or ∪:
julia> ∂(1, 2) ∪ ∂(2, 1)
union(∂(2, 1), ∂(1, 2))Internals
This section of the documentation is probably only relevant to contributors and others who want to understand the internals.
Type hierarchies
Generally, the abstract types below are not part of the exposed API, and new types don't have to subtype them (unless they want to rely on the existing convenience methods). They are merely for code organization.
SpectralKit.AbstractUnivariateDomain — TypeUnivariate domain representation. Supports extrema, minimum, maximum.
Implementations only need to define extrema.
Grid internals
SpectralKit.gridpoint — Functiongridpoint(_, basis, i)
Return a gridpoint for collocation, with 1 ≤ i ≤ dimension(basis).
T is used as a hint for the element type of grid coordinates. The actual type can be broadened as required. Methods are type stable.
Not all grids have this method defined, especially if it is impractical. See grid, which is part of the API, this function isn't.