Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 60 additions & 18 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Always reference these instructions first and fallback to search or bash command

### Building and Testing
- Build the package: `julia --project=. -e "using Pkg; Pkg.build()"`
- Run all tests: `julia --project=. -e "using Pkg; Pkg.test()"` -- takes 60 seconds. NEVER CANCEL. Set timeout to 120+ seconds.
- Run all tests: `julia --project=. -e "using Pkg; Pkg.test()"` -- takes 60-120 seconds. NEVER CANCEL. Set timeout to 240+ seconds.
- Run basic functionality test: Create a simple script importing `ContinuousDPs`, `QuantEcon`, and `BasisMatrices` to verify the package works

### Development Workflow
Expand All @@ -30,9 +30,14 @@ Always test new code with this basic workflow:
using ContinuousDPs
using BasisMatrices: Basis, ChebParams

# Approximation domain
s_min, s_max = 0.1, 2.0

# Define reward and transition functions
f(s, x) = log(x) # reward function
g(s, x, e) = s^0.3 * x^0.7 + e # state transition function
# IMPORTANT: keep the next state within the approximation domain (see
# "Out-of-domain next states" below)
g(s, x, e) = clamp(s^0.3 * x^0.7 + e, s_min, s_max) # state transition

# Setup problem parameters
discount = 0.9
Expand All @@ -43,7 +48,6 @@ x_ub(s) = s - 0.01 # upper bound on action

# Create basis for approximation
n = 10
s_min, s_max = 0.1, 2.0
basis = Basis(ChebParams(n, s_min, s_max))

# Create continuous DP
Expand Down Expand Up @@ -71,16 +75,25 @@ ALWAYS run these validation steps after making changes:
```
├── src/
│ ├── ContinuousDPs.jl # Main module file
│ ├── point_eval.jl # Non-allocating point-evaluation kernels
│ ├── cdp.jl # Core continuous DP functionality
│ └── lq_approx.jl # Linear quadratic approximation methods
├── docs/
│ ├── make.jl # Documenter build script
│ └── src/ # Documentation source files
├── test/
│ ├── runtests.jl # Test runner
│ ├── test_cdp.jl # Tests for core CDP functionality
│ ├── test_cdp_multidim.jl # Tests for multi-dimensional CDP
│ └── test_lq_approx.jl # Tests for LQ approximation
│ ├── runtests.jl # Test runner
│ ├── test_point_eval.jl # Agreement tests for the point kernels
│ ├── test_workspace.jl # CDPWorkspace and in-place evaluation tests
│ ├── test_foc.jl # First-order-condition inner solver tests
│ ├── test_evaluate_policy.jl # Policy-evaluation assembly tests
│ ├── test_cdp.jl # Tests for core CDP functionality
│ ├── test_cdp_multidim.jl # Tests for multi-dimensional CDP
│ └── test_lq_approx.jl # Tests for LQ approximation
├── benchmark/
│ ├── benchmarks.jl # PkgBenchmark/BenchmarkTools suite (SUITE)
│ ├── Project.toml # Benchmark environment
│ └── README.md # How to run and compare benchmarks
├── examples/
│ ├── cdp_ex_optgrowth_jl.ipynb # Optimal growth model example
│ ├── cdp_ex_MF_jl.ipynb # Miranda & Fackler examples
Expand All @@ -90,9 +103,28 @@ ALWAYS run these validation steps after making changes:

### Important Files to Check When Making Changes
- Always check `src/cdp.jl` when modifying core solving algorithms
- Always check `src/lq_approx.jl` when working with linear quadratic approximations
- Always run tests in `test/test_cdp.jl` when modifying CDP functionality
- Always check `src/point_eval.jl` when modifying interpolant evaluation; its behavior contract is exact agreement with `BasisMatrices.funeval`/`evalbase` (see below)
- Always check `src/lq_approx.jl` when working with linear quadratic approximations
- Always run tests in `test/test_point_eval.jl` when modifying `src/point_eval.jl`
- Always run tests in `test/test_cdp.jl`, `test/test_cdp_multidim.jl`, `test/test_foc.jl`, and `test/test_evaluate_policy.jl` when modifying CDP functionality
- Always run tests in `test/test_lq_approx.jl` when modifying LQ approximation
- Update `benchmark/benchmarks.jl` when internal signatures used there change

## Architecture Notes (performance-critical internals)

### Point-evaluation kernels (`src/point_eval.jl`)
- `FunEvalCache(basis)` + `funeval_point!(fec, C, x)`: non-allocating single-point evaluation of an interpolant; `DerivFunEvalCache(basis, order)` + `set_coefs!` for partial derivatives via coefficient differentiation.
- CONTRACT: these kernels must reproduce `BasisMatrices.evalbase`/`funeval` semantics exactly, including behavior outside the interpolation domain; `test/test_point_eval.jl` enforces agreement at machine precision. Any change here requires those tests.
- The file is deliberately self-contained (no DP-specific types): it is planned to move upstream to BasisMatrices.jl (issue #94). Do not add ContinuousDPs types to it, and do not make other files depend on its imports (import what you use explicitly).

### Solver workspace and inner solvers (`src/cdp.jl`)
- `CDPWorkspace(cdp; inner_solver=:foc)` holds all preallocated buffers and evaluation caches; it is created once per `solve`. Workspaces and caches are NOT thread-safe (one per thread).
- The inner maximization over actions uses the first-order condition by default (`inner_solver=:foc`; safeguarded root-finding, warm-started via `ws.X`, exact interpolant gradients, finite differences of user `f`/`g`), with automatic fallback to Brent per basis (piecewise linear bases), per state (non-finite values, exceptions), and per call (`inner_solver=:brent`).
- The sweep over states is allocation-free except for `Optim.maximize`'s small result object on Brent paths. Do not introduce per-state allocations in `_s_wise_max!`-family functions; benchmark allocation columns are watched.
- `evaluate_policy!` assembles the collocation system with the point kernels, in sparse form when `Phi` is sparse (spline and piecewise linear bases). Preserve this dense/sparse dispatch.

### Out-of-domain next states (common pitfall)
Candidate actions explored during the inner maximization can map the next state outside the interpolation domain, where the fitted value function is extrapolated. For Chebyshev bases such extrapolation is astronomically large and can silently destroy a solve. Any test or benchmark model must keep `g(s, x, e)` within the domain for all feasible actions and shocks (clamp inside `g`, or tighten `x_lb`/`x_ub`). See PR #97 for background.

## Common Tasks

Expand All @@ -102,19 +134,24 @@ ALWAYS run these validation steps after making changes:
- Key examples: optimal growth model, Miranda & Fackler chapter 9 examples

### Algorithm Types
- **PFI (Policy Function Iteration)**: Generally faster convergence
- **PFI (Policy Function Iteration)**: Generally faster convergence; per-iteration cost includes a collocation linear solve
- **VFI (Value Function Iteration)**: More robust but potentially slower
- **LQA (Linear Quadratic Approximation)**: Approximates the model around a steady state
- All algorithms available through `solve()` with method parameter
- All algorithms available through `solve()` with method parameter; `solve` also accepts `inner_solver=:foc` (default) or `:brent` for the inner maximization

### Basis Types
### Basis Types
- **Chebyshev**: `ChebParams(n, s_min, s_max)` - good general purpose choice
- **Spline**: `SplineParams(breaks, s_min, s_max, k)` - flexible, good for irregular functions
- **Linear**: `LinParams(breaks, s_min, s_max)` - simple linear interpolation

### Benchmarks
- The suite in `benchmark/benchmarks.jl` follows the PkgBenchmark convention; see `benchmark/README.md` for standalone and `judge`-based usage
- Performance-related PRs must include before/after numbers from full `PkgBenchmark.benchmarkpkg` runs on the base branch and the PR branch
- A full suite run takes a few minutes. NEVER CANCEL.

### Debugging Common Issues
- **`solve` not defined**: Ensure `using ContinuousDPs` is present
- **Method convergence issues**: Try different basis sizes or algorithm (PFI vs VFI)
- **Method convergence issues**: Try different basis sizes or algorithm (PFI vs VFI); check for out-of-domain next states (see above)
- **Simulation errors**: Check that state bounds are consistent with transition function
- **Performance issues**: Larger basis sizes increase accuracy but slow computation

Expand All @@ -132,20 +169,25 @@ ALWAYS run these validation steps after making changes:
- Check that examples in `examples/` directory still work if you modified core functionality
- No additional linting or formatting tools required - Julia has built-in code standards

### Pull Request Conventions
- PR and commit titles carry a category prefix: `ENH:`, `PERF:`, `BUG:`, `TST:`, `DOC:`, `MAINT:`
- Performance claims are backed by benchmark tables (see Benchmarks above)
- New numerical code paths are validated against a reference: either analytical solutions, `BasisMatrices` outputs, or a verbatim copy of the previous implementation

## Dependencies and Compatibility
- **Core dependencies**: QuantEcon.jl, BasisMatrices.jl, Optim.jl, FiniteDiff.jl
- **Core dependencies**: QuantEcon.jl, BasisMatrices.jl, Optim.jl, FiniteDiff.jl, SparseArrays (stdlib)
- **Julia version**: 1.10+ required (see Project.toml)
- **Platform support**: Windows, macOS, Linux (all tested in CI)
- Dependencies install automatically via `Pkg.instantiate()`

## Performance Notes
- Basis approximation size (`n`) significantly affects both accuracy and speed
- PFI generally converges faster than VFI but requires more memory
- Larger shock grids increase computational complexity
- The inner-maximization sweeps are allocation-free; VFI cost is dominated by the sweep, PFI cost by the sweep plus the collocation linear solve
- Larger shock grids increase computational complexity linearly in the sweep
- Simulation is fast once the problem is solved

## Troubleshooting
- **Long solve times**: Normal for complex problems. Increase `max_iter` if needed, but be patient
- **Convergence warnings**: Try different basis size, tolerance, or algorithm
- **Convergence warnings**: Try different basis size, tolerance, or algorithm; verify next states stay within the interpolation domain
- **Memory issues**: Reduce basis size or use simpler basis types
- **Installation issues**: Ensure Julia 1.10+ and try `Pkg.update()` first
- **Installation issues**: Ensure Julia 1.10+ and try `Pkg.update()` first
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ContinuousDPs.jl — agent instructions

The repository instructions for AI agents are maintained in [`.github/copilot-instructions.md`](.github/copilot-instructions.md). Read that file before working on this repository.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
See @.github/copilot-instructions.md for the repository instructions.
Loading