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

  1. evaluation of univariate and multivatiate basis functions, including Smolyak combinations,
  2. transformed to the relevant domains of interest, eg $[a,b] × [0,∞)$,
  3. (partial) derivatives, with correct limits at endpoints,
  4. allocation-free, thread safe linear combinations for the above with a given set of coefficients,
  5. 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 is not ideal 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,

  1. A basis is a finite family of functions for approximating other functions. The dimension of a basis tells you how many functions are in there, while domain can be used to query its domain.

  2. A grid is vector of suggested gridpoints for evaluating the function to be approximated that has useful theoretical properties. You can contruct a collocation_matrix using 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.

  3. basis_at returns 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_combination is a convenience wrapper for obtaining a linear combination of basis functions at a given point.

A basis is constructed using

  1. a family on a fixed domain, eg Chebyshev,

  2. a grid specification like InteriorGrid,

  3. a number of parameters that determine the number of basis functions.

A set of coordinates for a particular basis can be augmented for a wider basis with augment_coefficients.

Bases have a “canonical” domain, eg $[-1,1]$ or $[-1,1]^n$ for Chebyshev polynomials. Use transformations for mapping to other domains.

Examples

Univariate family on [-1,1]

using SpectralKit
basis = Chebyshev(EndpointGrid(), 5)        # 5 Chebyshev polynomials
is_function_basis(basis)                    # ie we support the interface below
dimension(basis)                            # number of basis functions
domain(basis)                               # domain
grid(basis)                                 # Gauss-Lobatto grid
collect(basis_at(basis, 0.41))              # iterator for basis functions at 0.41
collect(basis_at(basis, derivatives(0.41))) # values and 1st derivatives
θ = [1, 0.5, 0.2, 0.3, 0.001]               # a vector of coefficients
linear_combination(basis, θ, 0.41)          # combination at some value
linear_combination(basis, θ)(0.41)          # also as a callable
basis2 = Chebyshev(EndpointGrid(), 8)       # 8 Chebyshev polynomials
is_subset_basis(basis, basis2)              # we could augment θ …
augment_coefficients(basis, basis2, θ)      # … so let's do it
8-element Vector{Float64}:
 1.0
 0.5
 0.2
 0.3
 0.001
 0.0
 0.0
 0.0

Smolyak approximation on a transformed domain

using SpectralKit, StaticArrays
function f2(x)                  # bivariate function we approximate
    x1, x2 = x                  # takes vectors
    exp(x1) + exp(-abs2(x2))
end
basis = smolyak_basis(Chebyshev, InteriorGrid2(), SmolyakParameters(3), 2)
ct = coordinate_transformations(BoundedLinear(-1, 2.0), SemiInfRational(-3.0, 3.0))
basis_t = basis ∘ ct
x = grid(basis_t)
θ = collocation_matrix(basis_t) \ f2.(x)  # find the coefficients
z = (0.5, 0.7)                            # evaluate at this point
isapprox(f2(z), linear_combination(basis_t, θ)(z), rtol = 0.005)
true

Note how the transformation can be combined with to a callable that evaluates a transformed linear combination at z.

Constructing bases

Grid specifications

SpectralKit.EndpointGridType
struct EndpointGrid <: SpectralKit.AbstractGrid

Grid that includes endpoints (eg Gauss-Lobatto).

Note

For small dimensions may fall back to a grid that does not contain endpoints.

source
SpectralKit.InteriorGrid2Type
struct InteriorGrid2 <: SpectralKit.AbstractGrid

Grid with interior points that results in smaller grids than InteriorGrid when nested. Equivalent to an EndpointGrid with endpoints dropped.

source

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_toFunction

transform_to(domain, transformation, x)

Transform x to domain using transformation.

!!! FIXME document, especially differentiability requirements at infinite endpoints

source
SpectralKit.transform_fromFunction

transform_from(domain, transformation, x)

Transform x from domain using transformation.

!!! FIXME document, especially differentiability requirements at infinite endpoints

source
SpectralKit.domainFunction

domain(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.

source

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_kindFunction
domain_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:

  1. :univariate, the bounds of which can be accessed using minimum, maximum, and

extrema,

  1. :multivariate, which supports length, getindex ([]), and conversion with Tuple.
source
SpectralKit.coordinate_domainsFunction
coordinate_domains(domains)

Create domains which are the product of univariate domains. The result support length, indexing with integers, and Tuple for conversion.

source
coordinate_domains(domains)
source
coordinate_domains(_, domain)

Create a coordinate domain which is the product of domain repeated N times.

source
coordinate_domains(N, domain)
source

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.BoundedLinearType
struct BoundedLinear{T<:Real} <: SpectralKit.AbstractUnivariateTransformation

Transform 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.

source
SpectralKit.InfRationalType
InfRational(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$
source
SpectralKit.SemiInfRationalType
SemiInfRational(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$
source
SpectralKit.coordinate_transformationsFunction
coordinate_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)
source

Univariate bases

Currently, only Chebyshev polynomials are implemented. Univariate bases operate on real numbers.

SpectralKit.ChebyshevType
struct Chebyshev{K<:SpectralKit.AbstractGrid} <: SpectralKit.UnivariateBasis

The first N Chebyhev polynomials of the first kind, defined on [-1,1].

source

Multivariate bases

Multivariate bases operate on tuples or vectors (StaticArrays.SVector is preferred for performance, but all <:AbstractVector types should work).

SpectralKit.SmolyakParametersType
SmolyakParameters(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.

source
SpectralKit.smolyak_basisFunction
smolyak_basis(
    univariate_family,
    grid_kind,
    smolyak_parameters,
    _
)

Create a sparse Smolyak basis.

Arguments

  • univariate_family: should be a callable that takes a grid_kind and a dimension parameter, eg Chebyshev.

  • grid_kind: the grid kind, eg InteriorGrid() etc.

  • smolyak_parameters: the Smolyak grid specification parameters, see SmolyakParameters.

  • N: the dimension. wrapped in a Val for 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.

source

Using bases

Introspection

SpectralKit.is_function_basisFunction

is_function_basis(::Type{F})

is_function_basis(f::F)

Test if the argument (value or type) is a function basis, supporting the following interface:

  • domain for querying the domain,

  • dimension for the dimension,

  • basis_at for function evaluation,

  • grid to obtain collocation points.

  • length and getindex for 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).

source

See also domain.

Evaluation

SpectralKit.basis_atFunction

basis_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.

Note

Consequences are undefined when evaluating outside the domain.

source
SpectralKit.linear_combinationFunction
linear_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(θ).

source
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.

source

Grids and collocation

SpectralKit.gridFunction

grid([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.

source
SpectralKit.collocation_matrixFunction
collocation_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.

source

Augment coordinates for a wider basis

SpectralKit.augment_coefficientsFunction

augment_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.

source

Derivatives

Note

API for derivatives is still experimental and subject to change.

For univariate functions, use derivatives. For multivariate functions, use partial derivatives with .

SpectralKit.derivativesFunction
derivatives(x, ::Val(N) = Val(1))

Obtain N derivatives (and the function value) at a scalar x. The ith derivative can be accessed with [i] from results, with [0] for the function value.

Important note about transformations

Always use derivatives before a transformation for correct results. For example, for some transformation t and value x in the transformed domain,

# right
linear_combination(basis, θ, transform_to(domain(basis), t, derivatives(x)))
# right (convenience form)
(linear_combination(basis, θ) ∘ t)(derivatives(x))

instead of

# WRONG
linear_combination(basis, θ, derivatives(transform_to(domain(basis), t, x)))

For multivariate calculations, use the interface.

Example

julia> basis = Chebyshev(InteriorGrid(), 3)
Chebyshev polynomials (1st kind), InteriorGrid(), dimension: 3

julia> C = collect(basis_at(basis, derivatives(0.1)))
3-element Vector{SpectralKit.Derivatives{2, Float64}}:
 1.0 + 0.0⋅Δ
 0.1 + 1.0⋅Δ
 -0.98 + 0.4⋅Δ

julia> C[1][1]                         # 1st derivative of the linear term is 1
0.0
source
SpectralKit.∂Function
∂(_, partials)

Partial derivative specification. The first argument is Val(::Int) or simply an Int (for convenience, using constant folding), determining the dimension of the argument.

Subsequent arguments are indices of the input variable.

julia> ∂(3, (), (1, 1), (2, 3))
partial derivatives
[1] f
[2] ∂²f/∂²x₁
[3] ∂²f/∂x₂∂x₃
source
∂(∂specification, x)

Input wrappert type for evaluating partial derivatives ∂specification at x.

julia> using StaticArrays

julia> s = ∂(Val(2), (), (1,), (2,), (1, 2))
partial derivatives
[1] f
[2] ∂f/∂x₁
[3] ∂f/∂x₂
[4] ∂²f/∂x₁∂x₂

julia> ∂(s, SVector(1, 2))
partial derivatives
[1] f
[2] ∂f/∂x₁
[3] ∂f/∂x₂
[4] ∂²f/∂x₁∂x₂
at [1, 2]
source
∂(x, partials)

Shorthand for ∂(x, ∂(Val(length(x)), partials...)). Ideally needs an SVector or a Tuple so that size information can be obtained statically.

source

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.

Grid internals

SpectralKit.gridpointFunction
gridpoint(_, 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.

Note

Not all grids have this method defined, especially if it is impractical. See grid, which is part of the API, this function isn't.

source