Skip to content

Commit 2c33451

Browse files
committed
feat(numeric): formalize Space hierarchy, Approximator operators, and DecisionRule.fit() API
- Introduce Space hierarchy (CartesianSpace, DiscreteSpace/GridSpace, ProductSpace, EmptySpace) separating mathematical domain from discretization grids - Introduce Approximator hierarchy (LinearApproximator, CubicSplineApproximator, SmolyakApproximator, ProductApproximator) with canonical get_approximator() factory - Add DecisionRule.fit() returning self for fluent chaining (parity with Julia fit!), aliasing set_values - Make CartesianDomain inherit from CartesianSpace, connecting model.domain to Space - Expose dmodel.space, dmodel.approximator, and dmodel.decision_rule() on DiscretizedModel - Update documentation in docs/model_representation.md and docs/api.md - Add comprehensive test suite in tests/test_spaces_approximators.py (9/9 passed)
1 parent dbbd6aa commit 2c33451

9 files changed

Lines changed: 1099 additions & 89 deletions

File tree

‎docs/api.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ The numeric core provides the grid, transition kernel, and discrete field abstra
3434
::: dolo.numeric.garray
3535

3636
::: dolo.numeric.tau
37+
38+
::: dolo.numeric.spaces
39+
40+
::: dolo.numeric.approximators

‎docs/model_representation.md‎

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,51 @@ layered abstractions that perform this conversion:
2525

2626
## 1. Spaces and Grids
2727

28-
### The notion of a *space*
28+
### The `Space` Hierarchy
29+
30+
In economic modeling, variables live on a **mathematical space** (the continuous or discrete domain) rather than a computational grid. The **`Space`** hierarchy in `dolo.numeric.spaces` provides first-class objects for these domains:
31+
32+
- **`CartesianSpace`**: A continuous bounded hypercube $[\underline{s}, \overline{s}] \subset \mathbb{R}^d$.
33+
- In a `CartesianSpace`, location and continuous coordinates coincide (`loc = val = coords`).
34+
- Supports domain membership checks (`pt in space`), random sampling (`space.draw()`), and discretization (`space.discretize(n=20)`).
35+
- Preserves dict-like indexing `space['k']` for backward compatibility with `model.domain`.
36+
- **`DiscreteSpace`** (alias **`GridSpace`**): A finite discrete set of points $\{p_1, \dots, p_N\}$ (e.g. Markov states).
37+
- In a `DiscreteSpace`, `loc` is the integer regime index $i \in \{0, \dots, N-1\}$, while `val = points[i]`.
38+
- Discretizes to an `UnstructuredGrid`.
39+
- **`ProductSpace`**: The Cartesian product of multiple spaces (e.g. $S_{exo} \times S_{endo}$).
40+
- `loc` is a composite tuple `(loc_exo, loc_endo)`, and `val` is concatenated $[m, s]$.
41+
- Discretizes to a `ProductGrid`.
42+
- **`EmptySpace`**: The neutral empty space for models without discrete exogenous states (discretizes to `EmptyGrid`).
2943

30-
Almost every economic model solved by Dolo mixes two kinds of dimensions:
44+
```python
45+
from dolo.numeric import CartesianSpace, DiscreteSpace, ProductSpace
46+
47+
# Continuous state space
48+
endo_space = CartesianSpace(min=[0.1, 1.0], max=[0.5, 5.0], variables=["z", "k"])
49+
assert [0.2, 2.0] in endo_space
50+
51+
# Discrete regime space
52+
exo_space = DiscreteSpace([[-0.01], [0.01]], variables=["e"])
3153

32-
- a **discrete** part, capturing exogenous regimes: the nodes of a Markov
33-
chain, the quadrature nodes of an i.i.d. shock, or a mixed product of these;
34-
- a **continuous** part, the endogenous state variables (e.g. capital,
35-
productivity, wealth), which are typically bounded to an interval.
54+
# Product space
55+
space = exo_space * endo_space
56+
grid = space.discretize(endo={"n": [20, 40]}) # -> ProductGrid
57+
```
58+
59+
### Grids (Discretizations of Spaces)
3660

37-
The *space* of the model is the product of these two parts. In the current
38-
implementation the space is **not** a standalone object: it is concretely
39-
realized by a **grid** — more precisely a `ProductGrid`, the Cartesian product
40-
of an *exogenous* grid and an *endogenous* grid:
61+
A **`Grid`** represents a concrete, discrete set of nodes sampling a `Space`:
4162

4263
$$\mathcal{G} = \mathcal{M} \times \mathcal{S}$$
4364

44-
- $\mathcal{M}$ is the grid of the discretized exogenous process (regimes /
45-
shock nodes);
46-
- $\mathcal{S}$ is the grid of endogenous states (a Cartesian product of
47-
intervals).
65+
- $\mathcal{M}$ is the grid of the discretized exogenous process (regimes / shock nodes);
66+
- $\mathcal{S}$ is the grid of endogenous states (a Cartesian product of intervals).
4867

4968
A `ProductGrid` bundles the two sub-grids and supports lookup by name:
5069

5170
```python
5271
grid.grids # -> [exo_grid, endo_grid]
53-
grid["states"] # -> the endogenous sub-grid
72+
grid["endo"] # -> the endogenous sub-grid
5473
```
5574

5675
### Grid types
@@ -255,54 +274,52 @@ x0.norm() # L_infinity norm
255274

256275
## 6. Approximators
257276

258-
The solution algorithms do not return raw point values on the grid; they
259-
return a decision rule $x = \varphi(S)$ that can be evaluated *anywhere*. The
260-
objects that interpolate (or otherwise approximate) the policy over the space
261-
are collectively the **approximators**. In the current implementation these
262-
live in `dolo.numeric.decision_rule`:
277+
The solution algorithms do not return raw point values on the grid; they return a decision rule $x = \varphi(S)$ that can be evaluated *anywhere* in the continuous space. The objects that interpolate or approximate the policy are collectively the **approximators** in `dolo.numeric.approximators`:
263278

264-
- **`DecisionRule`** — interpolates a control field stored on an
265-
`(exo_grid, endo_grid)` product grid, with an interpolation method selected
266-
from `interp_methods`:
279+
- **`LinearApproximator`**: Piecewise multilinear interpolation on Cartesian grids.
280+
- **`CubicSplineApproximator`**: Prefiltered cubic spline interpolation on Cartesian grids.
281+
- **`SmolyakApproximator`**: Multivariate Chebyshev polynomial approximation on Smolyak grids.
282+
- **`ProductApproximator`**: Regime-wise composite approximator over `ProductGrid` / `ProductSpace` (delegating to continuous approximators per discrete regime).
267283

268-
| key | interp method |
269-
| --- | --- |
270-
| `linear` / `multilinear` | piecewise-linear (multilinear) interpolation |
271-
| `cubic` | cubic spline (prefiltered) |
272-
| `chebychev` | Chebyshev polynomials (reserved) |
284+
An `Approximator` provides:
285+
- `app.grid`: The compatible discretization grid.
286+
- `app.fit(values)`: Fits coefficients from field values on the grid.
287+
- `app.evaluate_coords(coeffs, s)`: Evaluates approximation at continuous coordinate matrix.
288+
- `app.evaluate(coeffs, qp)`: Evaluates approximation at a `QualifiedPoint`.
289+
- `app.evaluate_batch(coeffs, batch)`: Evaluates approximation over a `QualifiedBatch` / `SliceQP`.
273290

274-
- **`ConstantDecisionRule`** — a trivial constant policy (used as a starting
275-
point).
276-
- **`CustomDR`** — a user-supplied closed-form decision rule.
291+
### DecisionRule and `fit()`
277292

278-
A `DecisionRule` is callable like a `gufunc`, accepting either a `QP` /
279-
`SliceQP`, `(m, s)` economic coordinates, or `(i, s)` regime indexing:
293+
A **`DecisionRule`** bundles the space, grid, approximator, and fitted coefficients into a callable object:
280294

281295
```python
282-
dr = DecisionRule(exo_grid, endo_grid, interp_method="cubic")
283-
dr.set_values(x) # x : GArray or raw array of control values
284-
dr(i_m, s) # evaluate in regime i_m at states s
296+
from dolo.numeric import DecisionRule
297+
298+
# Fluent construction and fitting (matching Julia's fit!):
299+
dr = DecisionRule(exo_grid, endo_grid, interp_method="cubic").fit(controls_ga)
300+
301+
# dr.set_values is preserved as an alias for backwards compatibility:
302+
dr.set_values(controls_ga)
303+
304+
# Callable over QP, SliceQP, or regime + continuous coordinates:
305+
dr(qp)
306+
dr(slice_qp)
307+
dr(i_m, s)
285308
```
286309

287-
Smolyak interpolation is available through `SmolyakGrid` (and the
288-
`interpolation` dependency); higher-dimensional and Taylor-based
289-
approximators (as in Dolo.jl) are planned but not yet wired into the solve
290-
routines.
310+
You can also construct a `DecisionRule` directly from a `DiscretizedModel`:
311+
312+
```python
313+
dr = dmodel.decision_rule(controls_ga, interp_method="cubic")
314+
```
291315

292316
---
293317

294318
## 7. Roadmap
295319

296-
The following pieces of the redesign are still outstanding; this chapter will
297-
be updated as they land:
320+
With first-class **`Space`** and **`Approximator`** hierarchies now landed, the remaining items on the redesign roadmap are:
298321

299-
- a first-class **`Space`** object (separating the *domain* from its *node
300-
set*), making non-Cartesian and mixed discrete/continuous domains
301-
first-class citizens;
302-
- a unified **`Approximator`** interface (Smolyak, spline, Chebyshev, Taylor)
303-
shared by all solution routines;
304-
- `tau`-based simulation and quadrature (currently `tau_fit` is the reference).
322+
- **`tau`-based simulation and ergodic analysis**: using $\tau$ directly inside time-series simulation loops (replacing ad-hoc process branching);
323+
- **Higher-order Taylor approximators**: local perturbation expansions around the steady state implementing the unified `Approximator` interface.
305324

306-
The stable, algorithm-facing `DiscretizedModel` / `tau` / `GArray` / `QP`
307-
abstractions are already used by the global solvers (see the "Time Iteration"
308-
chapter).
325+
The stable, algorithm-facing `DiscretizedModel` / `tau` / `GArray` / `QP` / `Space` / `Approximator` abstractions are used by the global solvers (see the "Time Iteration" chapter).

‎src/dolo/compiler/objects.py‎

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,15 @@
1414
# @language_element
1515

1616

17-
class Domain:
18-
pass
19-
20-
21-
class CartesianDomain(Domain, dict):
22-
def __init__(self, **kwargs):
23-
super().__init__()
24-
for k, w in kwargs.items():
25-
v = kwargs[k]
26-
self[k] = np.array(v, dtype=float)
17+
from dolo.numeric.spaces import Space, CartesianSpace
2718

28-
def discretize(self, n=None):
29-
if n == None:
30-
n = [10] * (len(self.min))
31-
from dolo.numeric.grids import UniformCartesianGrid
19+
Domain = Space
3220

33-
return UniformCartesianGrid(self.min, self.max, n)
3421

35-
@property
36-
def states(self):
37-
l = tuple(self.keys())
38-
return l
22+
class CartesianDomain(CartesianSpace):
23+
"""Backwards-compatible alias for CartesianSpace."""
3924

40-
@property
41-
def min(self):
42-
return np.array([self[e][0] for e in self.states])
43-
44-
@property
45-
def max(self):
46-
return np.array([self[e][1] for e in self.states])
25+
pass
4726

4827

4928
# these are dummy objects so far

‎src/dolo/numeric/__init__.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@
88
)
99
from dolo.numeric.tau import Tau, trembling_hand
1010
from dolo.numeric.discretized_model import DiscretizedModel
11+
from dolo.numeric.spaces import (
12+
Space,
13+
CartesianSpace,
14+
DiscreteSpace,
15+
GridSpace,
16+
ProductSpace,
17+
EmptySpace,
18+
)
19+
from dolo.numeric.approximators import (
20+
Approximator,
21+
LinearApproximator,
22+
CubicSplineApproximator,
23+
SmolyakApproximator,
24+
ProductApproximator,
25+
get_approximator,
26+
)
1127

1228
__all__ = [
1329
"GArray",
@@ -19,4 +35,16 @@
1935
"Tau",
2036
"trembling_hand",
2137
"DiscretizedModel",
38+
"Space",
39+
"CartesianSpace",
40+
"DiscreteSpace",
41+
"GridSpace",
42+
"ProductSpace",
43+
"EmptySpace",
44+
"Approximator",
45+
"LinearApproximator",
46+
"CubicSplineApproximator",
47+
"SmolyakApproximator",
48+
"ProductApproximator",
49+
"get_approximator",
2250
]

0 commit comments

Comments
 (0)