QuantEcon / QuantEcon/ContinuousDPs.jl
Move the point-evaluation kernel (src/point_eval.jl) upstream to BasisMatrices.jl
Nobody has claimed this yet.
- Dominant language
- Julia
- Stars
- 17
- Forks
- 11
- PR merge metrics
- No merged PRs in 30d
Description
Background
#93 added src/point_eval.jl: a non-allocating single-point interpolant evaluation kernel used in the inner loop of _s_wise_max!. Previously, each objective-function call inside Optim.maximize invoked funeval, which has no fast path for a single point: the scalar method wraps the point in a matrix (fill(x, 1, 1)) and constructs a full BasisMatrix. Replacing those calls with the kernel reduced the memory footprint of a 50-iteration VFI from 700 MiB / 5.8M allocations to 434 KiB / 5.4K allocations and roughly halved wall-clock time (see the benchmark table in #93; addresses #73 and most of the cost behind #74).
The file is deliberately self-contained — it contains no DP-specific types and speaks only BasisMatrices' language — because this functionality conceptually belongs upstream: it is generic interpolation machinery, useful to anyone evaluating a fitted function point-wise in a loop (optimizers, solvers, simulations). This issue tracks moving it upstream to BasisMatrices.jl.
What the kernel provides (order 0, implemented)
PointEvalCache(p::BasisParams): per-dimension cache with a preallocated buffer for basis-function values at a point; specialized kernels forChebParams(Chebyshev recurrence on the unscaled point),SplineParams(de Boor recurrence on the augmented knot sequence, replicatinglookup(augbreaks, x, 3)semantics), andLinParams(two-weight linear interpolation, both theevennumfast path and thelookuppath); other basis families fall back to a generic (allocating)evalbase-based implementation.point_evalbase!(cache, x::Real) -> (first, nvals): writes the values of the basis functions with nonzero support atxintocache.vals, exploiting sparsity for splines (k+1values) and piecewise linear (2 values).FunEvalCache(basis::Basis{N})andfuneval_point!(cache, c, x) -> Float64: tensor-product contraction over the per-dimension kernels for anN-dimensional point; agrees withfuneval(c, basis, x)to machine precision, including for points outside the interpolation domain (each family reproducesevalbase's extrapolation behavior exactly).- Agreement tests in
test/test_point_eval.jl: 260 cases across basis families (Cheb; splines of degree 1–3 with even and uneven breaks; Lin withevennumzero and nonzero), mixed 2-D/3-D tensor bases, points at/inside/outside domain boundaries, plus zero-allocation assertions. These tests would transplant to BasisMatrices.jl essentially as-is, and until the move happens they guard against divergence from upstreamfunevalconventions.
Design decisions on record
- The caches are
Float64-only andfuneval_point!restricts coefficients toAbstractVector{Float64}, so that use with other numeric types (BigFloat, AD dual numbers) fails at dispatch instead of silently downcasting. Generic-eltype support would mean parameterizing the caches; whether that is worth the complexity is an API question best settled upstream. - Only derivative order 0 is implemented. The
(first, nvals)-into-buffer contract extends directly to derivative orders via the standard Chebyshev/B-spline derivative recurrences (e.g.point_evalbase!(cache, x, order)); this is what derivative-based inner optimization (#7) needs. Since the interpolant's derivative is known in closed form, the intended pattern there is chain-rule composition — AD applied to the user'sfandgonly, with the interpolant gradient supplied by an order-1 kernel — rather than running AD through the basis recurrences. FunEvalCacheis mutable scratch and not thread-safe; parallel state loops need one cache per thread (relevant to a future threading workspace, and to any user-facing result type that embeds a cache, cf. #10).
Suggested upstream shape
Beyond a direct port, two natural integrations on the BasisMatrices.jl side:
- An
Interpolandfast path, soitp(x)at a single point uses the kernel instead of building aBasisMatrix— this also connects to the old discussion in #8 about moving ContinuousDPs' interpolation machinery upstream. - Complementarity with QuantEcon/BasisMatrices.jl#60, which requests in-place batch updates of basis matrices at a fixed set of nodes: that covers the fixed-grid batch workflow, this kernel covers the scattered point-wise workflow where the evaluation points change on every call; a design accommodating both may be worth considering.
Plan
- Propose the move at BasisMatrices.jl (issue link to follow) and iterate on API naming/shape there.
- Once the functionality is available in a released BasisMatrices.jl version: delete
src/point_eval.jlandtest/test_point_eval.jlhere, import the upstream equivalents insrc/cdp.jl, and bump theBasisMatricescompat bound.
🤖 Drafted with Claude Code (Claude Fable 5)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing src/point_eval.jl and test/test_point_eval.jl, then inspect how src/cdp.jl imports and uses the kernel. Propose and iterate on the API with BasisMatrices.jl, including the Interpoland integration questions. Once an upstream release provides the functionality, remove the local implementation and tests, import the upstream equivalents, and bump the BasisMatrices compat bound.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- julia
- Domain
- backend
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100