Skip to content

Commit 902399d

Browse files
committed
Refactor DecisionRule to Approximator strategy and remove multipledispatch
1 parent 2c33451 commit 902399d

9 files changed

Lines changed: 956 additions & 453 deletions

File tree

‎pixi.lock‎

Lines changed: 0 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pixi.toml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ matplotlib = ">=3.6.0,<4"
4646
xarray = ">=2024.1.0,<2025"
4747
quantecon = ">=0.7.2,<0.8"
4848
interpolation = ">=2.2.7,<3"
49-
multipledispatch = ">=0.6.0,<0.7"
5049
numba = ">=0.60.0,<0.70"
5150
dolang = ">=0.0.21,<0.1"
5251
# conda-forge packages the `dynopy` project (the PyPI name pyproject.toml
@@ -128,7 +127,6 @@ matplotlib = "==3.8.4"
128127
xarray = "==2024.7.0"
129128
quantecon = "==0.7.2"
130129
interpolation = "==2.2.7"
131-
multipledispatch = "==0.6.0"
132130
numba = "==0.60.0"
133131
dolang = "==0.0.21"
134132
dyno = "==0.1.15"

‎pyproject.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ dependencies = [
2828
"jax>=0.4.38,<0.5",
2929
"matplotlib>=3.6.0,<4",
3030
"numba>=0.60.0,<0.70",
31-
"multipledispatch>=0.6.0,<0.7",
3231
"numpy>=2.0.0,<3",
3332
"pandas>=2.2.2,<4",
3433
"pyyaml>=6.0,<7",

‎src/dolo/algos/ergodic.py‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import numpy as np
22

3-
# doesn't seem to support keyword arguments yet
4-
# from multipledispatch import dispatch
5-
# multimethod = dispatch()
63
from dolo.misc.multimethod import multimethod
74

85
# from numba import generated_jit

‎src/dolo/numeric/__init__.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
ProductApproximator,
2525
get_approximator,
2626
)
27+
from dolo.numeric.interpolation_jax import (
28+
prefilter_cubic_jax,
29+
eval_cubic_jax,
30+
eval_cubic_point_jax,
31+
eval_linear_jax,
32+
eval_linear_point_jax,
33+
)
2734

2835
__all__ = [
2936
"GArray",
@@ -47,4 +54,9 @@
4754
"SmolyakApproximator",
4855
"ProductApproximator",
4956
"get_approximator",
57+
"prefilter_cubic_jax",
58+
"eval_cubic_jax",
59+
"eval_cubic_point_jax",
60+
"eval_linear_jax",
61+
"eval_linear_point_jax",
5062
]

‎src/dolo/numeric/approximators.py‎

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,26 @@ def evaluate_batch(self, coeffs: Any, batch: QualifiedBatch) -> np.ndarray:
6161
class LinearApproximator(Approximator):
6262
"""Piecewise multilinear interpolation on CartesianGrid."""
6363

64-
def __init__(self, grid: CartesianGrid, space: Optional[CartesianSpace] = None):
64+
def __init__(
65+
self,
66+
grid: CartesianGrid,
67+
space: Optional[CartesianSpace] = None,
68+
backend: str = "jax",
69+
):
6570
self.grid = grid
6671
self.space = space
72+
self.backend = backend
6773

6874
def fit(self, values: Union[GArray, np.ndarray]) -> np.ndarray:
6975
data = values.data if isinstance(values, GArray) else np.asarray(values)
7076
return data.reshape(tuple(self.grid.n) + (-1,)).copy()
7177

7278
def evaluate_coords(self, coeffs: np.ndarray, coords: np.ndarray) -> np.ndarray:
79+
if self.backend == "jax":
80+
from dolo.numeric.interpolation_jax import eval_linear_jax
81+
82+
return np.asarray(eval_linear_jax(self.grid, coeffs, coords))
83+
7384
from interpolation.splines import eval_linear
7485

7586
pts = np.asarray(coords, dtype=float)
@@ -81,18 +92,34 @@ def evaluate_coords(self, coeffs: np.ndarray, coords: np.ndarray) -> np.ndarray:
8192
class CubicSplineApproximator(Approximator):
8293
"""Prefiltered cubic spline interpolation on CartesianGrid."""
8394

84-
def __init__(self, grid: CartesianGrid, space: Optional[CartesianSpace] = None):
95+
def __init__(
96+
self,
97+
grid: CartesianGrid,
98+
space: Optional[CartesianSpace] = None,
99+
backend: str = "jax",
100+
):
85101
self.grid = grid
86102
self.space = space
103+
self.backend = backend
87104

88105
def fit(self, values: Union[GArray, np.ndarray]) -> np.ndarray:
89-
from interpolation.splines.prefilter_cubic import prefilter_cubic
90-
91106
data = values.data if isinstance(values, GArray) else np.asarray(values)
92107
reshaped = data.reshape(tuple(self.grid.n) + (-1,))
108+
if self.backend == "jax":
109+
from dolo.numeric.interpolation_jax import prefilter_cubic_jax
110+
111+
return np.asarray(prefilter_cubic_jax(self.grid, reshaped))
112+
113+
from interpolation.splines.prefilter_cubic import prefilter_cubic
114+
93115
return prefilter_cubic(self.grid.__numba_repr__(), reshaped)
94116

95117
def evaluate_coords(self, coeffs: np.ndarray, coords: np.ndarray) -> np.ndarray:
118+
if self.backend == "jax":
119+
from dolo.numeric.interpolation_jax import eval_cubic_jax
120+
121+
return np.asarray(eval_cubic_jax(self.grid, coeffs, coords))
122+
96123
from interpolation.splines import eval_cubic
97124

98125
pts = np.asarray(coords, dtype=float)
@@ -172,6 +199,8 @@ def __init__(
172199
def fit(self, values: Union[GArray, np.ndarray]) -> Any:
173200
data = values.data if isinstance(values, GArray) else np.asarray(values)
174201
if self.is_empty_exo:
202+
if data.ndim == 3 and data.shape[0] == 1:
203+
data = data[0]
175204
return self.sub_approximator.fit(data)
176205
elif self.is_discrete_exo:
177206
n_regimes = data.shape[0]
@@ -183,6 +212,48 @@ def fit(self, values: Union[GArray, np.ndarray]) -> Any:
183212
f"Unsupported exo grid type: {type(self.exo_grid)}"
184213
)
185214

215+
def evaluate_s(self, coeffs: Any, s: np.ndarray) -> np.ndarray:
216+
"""Evaluate continuous endogenous states `s` when exogenous state is empty or single-regime."""
217+
s_arr = np.asarray(s, dtype=float)
218+
if self.is_empty_exo:
219+
return self.sub_approximator.evaluate_coords(coeffs, s_arr)
220+
if self.is_discrete_exo and self.exo_grid.n_nodes == 1:
221+
return self.sub_approximator.evaluate_coords(coeffs[0], s_arr)
222+
raise ValueError(
223+
"Cannot evaluate 's' alone when exogenous states are non-empty."
224+
)
225+
226+
def evaluate_ms(self, coeffs: Any, m: np.ndarray, s: np.ndarray) -> np.ndarray:
227+
"""Evaluate continuous endogenous states `s` and exogenous states `m`."""
228+
m_arr = np.asarray(m, dtype=float)
229+
s_arr = np.asarray(s, dtype=float)
230+
if self.is_empty_exo:
231+
return self.sub_approximator.evaluate_coords(coeffs, s_arr)
232+
elif self.is_cartesian_exo:
233+
combined = np.hstack([m_arr, s_arr])
234+
return self.joint_approximator.evaluate_coords(coeffs, combined)
235+
elif self.is_discrete_exo:
236+
if m_arr.ndim == 1:
237+
m_arr = m_arr[None, :]
238+
if s_arr.ndim == 1:
239+
s_arr = s_arr[None, :]
240+
if m_arr.shape[0] == 1 and s_arr.shape[0] > 1:
241+
m_arr = np.repeat(m_arr, s_arr.shape[0], axis=0)
242+
diffs = np.linalg.norm(
243+
self.exo_grid.nodes[:, None, :] - m_arr[None, :, :], axis=-1
244+
)
245+
regimes = np.argmin(diffs, axis=0)
246+
n_x = coeffs[0].shape[-1] if hasattr(coeffs[0], "shape") else 1
247+
out = np.empty((s_arr.shape[0], n_x), dtype=float)
248+
for r in np.unique(regimes):
249+
mask = regimes == r
250+
out[mask] = self.sub_approximator.evaluate_coords(
251+
coeffs[r], s_arr[mask]
252+
)
253+
return out
254+
else:
255+
raise NotImplementedError()
256+
186257
def evaluate_coords(self, coeffs: Any, coords: np.ndarray) -> np.ndarray:
187258
if self.is_empty_exo:
188259
return self.sub_approximator.evaluate_coords(coeffs, coords)

0 commit comments

Comments
 (0)