Skip to content

Commit 574ae7a

Browse files
committed
Unify DecisionRule and Approximator evaluation and deprecate eval_* methods
1 parent 902399d commit 574ae7a

7 files changed

Lines changed: 206 additions & 60 deletions

File tree

‎src/dolo/algos/newton.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ def newton(
428428

429429
if dr0 is not None:
430430
for i_m in range(n_m):
431-
x0[i_m, :, :] = dr0.eval_is(i_m, s)
431+
x0[i_m, :, :] = dr0(i_m, s)
432432
ddr.set_values(x0)
433433

434434
if x_lb is not None and complementarities:

‎src/dolo/algos/simulations.py‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,15 @@ def simulate(
131131
sim_type = "continuous"
132132
if m0 is None:
133133
m0 = model.calibration["exogenous"]
134-
x_simul[0, :, :] = dr.eval_ms(m0[None, :], s0[None, :])[0, :]
134+
x_simul[0, :, :] = dr(m0[None, :], s0[None, :])[0, :]
135135
elif len(driving_process.shape) == 2:
136136
i_simul = driving_process
137137
nodes = dr.exo_grid.nodes
138138
m_simul = nodes[i_simul]
139139
# inds = i_simul.ravel()
140140
# m_simul = np.reshape( np.concatenate( [nodes[i,:][None,:] for i in inds.ravel()], axis=0 ), inds.shape + (-1,) )
141141
sim_type = "discrete"
142-
x_simul[0, :, :] = dr.eval_is(i0, s0[None, :])[0, :]
142+
x_simul[0, :, :] = dr(i0, s0[None, :])[0, :]
143143
else:
144144
raise Exception("Incorrect specification of driving values.")
145145
m0 = m_simul[0, :, :]
@@ -165,15 +165,15 @@ def simulate(
165165
m_simul = dp.simulate(N, T, i0=i0, stochastic=stochastic)
166166
i_simul = find_index(m_simul, dp.values)
167167
m0 = dp.node(i0)
168-
x0 = dr.eval_is(i0, s0[None, :])[0, :]
168+
x0 = dr(i0, s0[None, :])[0, :]
169169
else:
170170
m_simul = process.simulate(N, T, m0=m0, stochastic=stochastic)
171171
if isinstance(m_simul, xr.DataArray):
172172
m_simul = m_simul.data
173173
sim_type = "continuous"
174174
if m0 is None:
175175
m0 = model.calibration["exogenous"]
176-
x0 = dr.eval_ms(m0[None, :], s0[None, :])[0, :]
176+
x0 = dr(m0[None, :], s0[None, :])[0, :]
177177
x_simul[0, :, :] = x0[None, :]
178178

179179
f = model.functions["arbitrage"]
@@ -187,12 +187,10 @@ def simulate(
187187
s = s_simul[i, :, :]
188188
if sim_type == "discrete":
189189
i_m = i_simul[i, :]
190-
xx = [
191-
dr.eval_is(i_m[ii], s[ii, :][None, :])[0, :] for ii in range(s.shape[0])
192-
]
190+
xx = [dr(i_m[ii], s[ii, :][None, :])[0, :] for ii in range(s.shape[0])]
193191
x = np.vstack(xx)
194192
else:
195-
x = dr.eval_ms(m, s)
193+
x = dr(m, s)
196194

197195
x_simul[i, :, :] = x
198196

@@ -284,10 +282,10 @@ def tabulate(
284282

285283
if i0 is not None:
286284
m = dp.node(i0)
287-
xvec = dr.eval_is(i0, svec)
285+
xvec = dr(i0, svec)
288286
elif m0 is not None:
289287
m = m0
290-
xvec = dr.eval_ms(m0, svec)
288+
xvec = dr(m0, svec)
291289

292290
mm = numpy.vstack([m] * n_steps)
293291
l = [mm, svec, xvec]
@@ -336,7 +334,7 @@ def tabulate_2d(model, dr, states=None, i0=0, s0=None, n=[12, 13]):
336334
s[i_x] = xx
337335
for yy in lps[1]:
338336
s[i_y] = yy
339-
x = dr.eval_is(i0, s)
337+
x = dr(i0, s)
340338
vv.append(numpy.concatenate([s, x]))
341339
vals.append(vv)
342340
vv = numpy.array(vals)

‎src/dolo/algos/value_iteration.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def value_iteration(
103103
m = np.repeat(dprocess.node(i_ms)[None, :], N, axis=0)
104104
lb_i = controls_lb(m, s, p_batched)
105105
ub_i = controls_ub(m, s, p_batched)
106-
x_i = mdr.eval_is(i_ms, s)
106+
x_i = mdr(i_ms, s)
107107
# the calibrated controls may violate the bounds: project onto
108108
# [lb, ub] (feasible constant policy, keeps transitioned states sane)
109109
controls_0[i_ms, :, :] = np.clip(x_i, lb_i, ub_i)
@@ -150,7 +150,7 @@ def value_iteration(
150150

151151
mdrv = ev.solution
152152
for i_ms in range(n_ms):
153-
values_0[i_ms, :, :] = mdrv.eval_is(i_ms, s)
153+
values_0[i_ms, :, :] = mdrv(i_ms, s)
154154

155155
values = values_0.copy()
156156
controls = controls_0.copy()
@@ -315,15 +315,15 @@ def evaluate_policy(
315315
else:
316316
controls = np.zeros((n_ms, N, n_x))
317317
for i_m in range(n_ms):
318-
controls[i_m, :, :] = mdr.eval_is(i_m, s)
318+
controls[i_m, :, :] = mdr(i_m, s)
319319

320320
values_0 = np.zeros((n_ms, N, n_v))
321321
if dr0 is None:
322322
for i_m in range(n_ms):
323323
values_0[i_m, :, :] = v0[None, :]
324324
else:
325325
for i_m in range(n_ms):
326-
values_0[i_m, :, :] = dr0.eval_is(i_m, s)
326+
values_0[i_m, :, :] = dr0(i_m, s)
327327

328328
val = model.functions["value"]
329329
g = model.functions["transition"]

‎src/dolo/numeric/approximators.py‎

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,29 @@ def evaluate_coords(self, coeffs: Any, coords: np.ndarray) -> np.ndarray:
4545
"""Evaluate approximation at continuous coordinate matrix of shape (N, d)."""
4646
pass
4747

48-
def evaluate(self, coeffs: Any, qp: QualifiedPoint) -> np.ndarray:
49-
"""Evaluate approximation at a single QualifiedPoint."""
50-
coords = np.asarray(qp.val, dtype=float)
51-
if coords.ndim == 1:
52-
res = self.evaluate_coords(coeffs, coords[None, :])
53-
return res[0]
54-
return self.evaluate_coords(coeffs, coords)
48+
def evaluate(self, coeffs: Any, *args) -> np.ndarray:
49+
"""Universal evaluation method for approximation schemes.
50+
51+
Supports:
52+
- `evaluate(coeffs, qp)`: single QualifiedPoint / QP
53+
- `evaluate(coeffs, batch)`: QualifiedBatch / SliceQP
54+
- `evaluate(coeffs, coords)`: coordinate array (N, d) or (d,)
55+
- `evaluate(coeffs, *args)`: multi-argument forms implemented by subclasses
56+
"""
57+
if len(args) == 1:
58+
arg = args[0]
59+
if isinstance(arg, QualifiedBatch):
60+
return self.evaluate_batch(coeffs, arg)
61+
if isinstance(arg, QualifiedPoint):
62+
coords = np.asarray(arg.val, dtype=float)
63+
if coords.ndim == 1:
64+
return self.evaluate_coords(coeffs, coords[None, :])[0]
65+
return self.evaluate_coords(coeffs, coords)
66+
coords = np.asarray(arg, dtype=float)
67+
if coords.ndim == 1:
68+
return self.evaluate_coords(coeffs, coords[None, :])[0]
69+
return self.evaluate_coords(coeffs, coords)
70+
raise ValueError(f"Approximator.evaluate cannot handle {len(args)} arguments.")
5571

5672
def evaluate_batch(self, coeffs: Any, batch: QualifiedBatch) -> np.ndarray:
5773
"""Evaluate approximation over a QualifiedBatch / SliceQP."""
@@ -264,9 +280,55 @@ def evaluate_coords(self, coeffs: Any, coords: np.ndarray) -> np.ndarray:
264280
"Discrete exogenous states require regime index or QualifiedPoint for evaluation."
265281
)
266282

267-
def evaluate(self, coeffs: Any, qp: QualifiedPoint) -> np.ndarray:
283+
def evaluate(self, coeffs: Any, *args) -> np.ndarray:
284+
"""Universal evaluation on ProductApproximator.
285+
286+
Supports:
287+
- `evaluate(coeffs, qp)`: single QualifiedPoint / QP
288+
- `evaluate(coeffs, batch)`: QualifiedBatch / SliceQP
289+
- `evaluate(coeffs, s)`: continuous endogenous states alone (empty exo or single-regime)
290+
- `evaluate(coeffs, i, s)`: discrete regime index `i` + continuous states `s`
291+
- `evaluate(coeffs, m, s)`: continuous exogenous `m` + endogenous `s`
292+
"""
293+
if len(args) == 1:
294+
arg = args[0]
295+
if isinstance(arg, QualifiedBatch):
296+
return self.evaluate_batch(coeffs, arg)
297+
if isinstance(arg, QualifiedPoint):
298+
return self._evaluate_qp(coeffs, arg)
299+
# Coordinates array: continuous endogenous states alone
300+
return self.evaluate_s(coeffs, np.asarray(arg, dtype=float))
301+
302+
elif len(args) == 2:
303+
a0, a1 = args
304+
# Check for discrete regime index `i`
305+
if isinstance(a0, (int, np.integer)):
306+
s_arr = np.asarray(a1, dtype=float)
307+
if s_arr.ndim == 1:
308+
return self.evaluate_regime(coeffs, int(a0), s_arr[None, :])[0]
309+
return self.evaluate_regime(coeffs, int(a0), s_arr)
310+
311+
a0_arr = np.asarray(a0)
312+
a1_arr = np.asarray(a1, dtype=float)
313+
if a0_arr.dtype.kind in ("i", "u") and a0_arr.ndim == 0:
314+
if a1_arr.ndim == 1:
315+
return self.evaluate_regime(coeffs, int(a0_arr), a1_arr[None, :])[0]
316+
return self.evaluate_regime(coeffs, int(a0_arr), a1_arr)
317+
318+
# Continuous (m, s)
319+
if a0_arr.ndim == 1 and a1_arr.ndim == 1:
320+
return self.evaluate_ms(coeffs, a0_arr[None, :], a1_arr[None, :])[0]
321+
return self.evaluate_ms(coeffs, a0_arr, a1_arr)
322+
323+
raise ValueError(
324+
f"ProductApproximator.evaluate cannot handle {len(args)} arguments."
325+
)
326+
327+
def _evaluate_qp(self, coeffs: Any, qp: QualifiedPoint) -> np.ndarray:
268328
if self.is_empty_exo:
269329
coords = np.asarray(qp.val, dtype=float)
330+
if coords.ndim == 1:
331+
return self.sub_approximator.evaluate_coords(coeffs, coords[None, :])[0]
270332
return self.sub_approximator.evaluate_coords(coeffs, coords)
271333
elif self.is_discrete_exo:
272334
i = qp.index
@@ -277,9 +339,12 @@ def evaluate(self, coeffs: Any, qp: QualifiedPoint) -> np.ndarray:
277339
)
278340
if isinstance(qp.loc, tuple) and len(qp.loc) >= 2:
279341
endo_coords = qp.loc[1]
280-
return self.sub_approximator.evaluate_coords(
281-
coeffs[i], np.asarray(endo_coords, dtype=float)
282-
)
342+
c_arr = np.asarray(endo_coords, dtype=float)
343+
if c_arr.ndim == 1:
344+
return self.sub_approximator.evaluate_coords(coeffs[i], c_arr[None, :])[
345+
0
346+
]
347+
return self.sub_approximator.evaluate_coords(coeffs[i], c_arr)
283348
elif self.is_cartesian_exo:
284349
return self.joint_approximator.evaluate(coeffs, qp)
285350
else:

‎src/dolo/numeric/decision_rule.py‎

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,17 @@ class Chebychev:
5454

5555
class CallableDecisionRule:
5656
def __call__(self, *args):
57+
# 1. Fast path: delegate directly to approximator when present
58+
if hasattr(self, "approximator") and self.approximator is not None:
59+
return self.approximator.evaluate(self.coefficients, *args)
60+
61+
# 2. Fallback for custom / legacy decision rules without Approximator
5762
from dolo.numeric.garray import QP, SliceQP
5863
from dolo.numeric.grids import EmptyGrid
5964

60-
# 1. Single argument: SliceQP, QP, or coordinates array
6165
if len(args) == 1:
6266
arg = args[0]
6367
if isinstance(arg, SliceQP):
64-
if hasattr(self, "approximator") and self.approximator is not None:
65-
return self.approximator.evaluate_batch(self.coefficients, arg)
6668
is_empty_exo = hasattr(self, "exo_grid") and isinstance(
6769
self.exo_grid, EmptyGrid
6870
)
@@ -71,8 +73,6 @@ def __call__(self, *args):
7173
return self.eval_s(arg.endo)
7274

7375
if isinstance(arg, QP):
74-
if hasattr(self, "approximator") and self.approximator is not None:
75-
return self.approximator.evaluate(self.coefficients, arg)
7676
is_empty_exo = hasattr(self, "exo_grid") and isinstance(
7777
self.exo_grid, EmptyGrid
7878
)
@@ -91,13 +91,11 @@ def __call__(self, *args):
9191
return self.eval_s(val_arr[None, :])[0, :]
9292
return self.eval_s(val_arr)
9393

94-
# Continuous coords array
9594
s_arr = np.asarray(arg)
9695
if s_arr.ndim == 1:
9796
return self.eval_s(s_arr[None, :])[0, :]
9897
return self.eval_s(s_arr)
9998

100-
# 2. Two arguments: (i, s) or (m, s)
10199
elif len(args) == 2:
102100
a0, a1 = args
103101
if isinstance(a0, (int, np.integer)):
@@ -108,13 +106,11 @@ def __call__(self, *args):
108106

109107
a0_arr = np.asarray(a0)
110108
a1_arr = np.asarray(a1)
111-
if a0_arr.dtype.kind in ("i", "u"):
112-
if a0_arr.ndim == 0:
113-
if a1_arr.ndim == 1:
114-
return self.eval_is(int(a0_arr), a1_arr[None, :])[0, :]
115-
return self.eval_is(int(a0_arr), a1_arr)
109+
if a0_arr.dtype.kind in ("i", "u") and a0_arr.ndim == 0:
110+
if a1_arr.ndim == 1:
111+
return self.eval_is(int(a0_arr), a1_arr[None, :])[0, :]
112+
return self.eval_is(int(a0_arr), a1_arr)
116113

117-
# (m, s) coordinates
118114
if a0_arr.ndim == 1 and a1_arr.ndim == 1:
119115
return self.eval_ms(a0_arr[None, :], a1_arr[None, :])[0, :]
120116
return self.eval_ms(a0_arr, a1_arr)
@@ -205,34 +201,47 @@ def fit(self, x):
205201
set_values = fit
206202

207203
def eval_ms(self, m, s):
208-
m_arr = np.asarray(m, dtype=float)
209-
s_arr = np.asarray(s, dtype=float)
210-
if s_arr.ndim == 1:
211-
s_arr = s_arr[None, :]
212-
if m_arr.ndim == 1:
213-
m_arr = m_arr[None, :]
214-
return self.approximator.evaluate_ms(self.coefficients, m_arr, s_arr)
204+
"""Deprecated: use `dr(m, s)` instead."""
205+
import warnings
206+
207+
warnings.warn(
208+
"dr.eval_ms(m, s) is deprecated, use dr(m, s) instead.",
209+
DeprecationWarning,
210+
stacklevel=2,
211+
)
212+
return self(m, s)
215213

216214
def eval_is(self, i, s):
217-
s_arr = np.asarray(s, dtype=float)
218-
if s_arr.ndim == 1:
219-
s_arr = s_arr[None, :]
220-
return self.approximator.evaluate_regime(self.coefficients, int(i), s_arr)
215+
"""Deprecated: use `dr(i, s)` instead."""
216+
import warnings
217+
218+
warnings.warn(
219+
"dr.eval_is(i, s) is deprecated, use dr(i, s) instead.",
220+
DeprecationWarning,
221+
stacklevel=2,
222+
)
223+
return self(i, s)
221224

222225
def eval_s(self, s):
223-
s_arr = np.asarray(s, dtype=float)
224-
if s_arr.ndim == 1:
225-
s_arr = s_arr[None, :]
226-
return self.approximator.evaluate_s(self.coefficients, s_arr)
226+
"""Deprecated: use `dr(s)` instead."""
227+
import warnings
228+
229+
warnings.warn(
230+
"dr.eval_s(s) is deprecated, use dr(s) instead.",
231+
DeprecationWarning,
232+
stacklevel=2,
233+
)
234+
return self(s)
227235

228236
def eval_ijs(self, i, j, s):
237+
"""Evaluate policy at transitioned state for regime transition (i -> j)."""
229238
if isinstance(self.exo_grid, UnstructuredGrid):
230-
return self.eval_is(j, s)
239+
return self(j, s)
231240
elif isinstance(self.exo_grid, EmptyGrid):
232-
return self.eval_s(s)
241+
return self(s)
233242
elif isinstance(self.exo_grid, CartesianGrid):
234243
m = self.dprocess.inode(i, j)[None, :].repeat(s.shape[0], axis=0)
235-
return self.eval_ms(m, s)
244+
return self(m, s)
236245
else:
237246
raise NotImplementedError()
238247

‎src/dolo/numeric/grids.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ def __add__(self, g):
129129
raise Exception("Not implemented.")
130130

131131
n = np.array(tuple(self.n) + tuple(g.n))
132-
min = np.array(tuple(self.min) + tuple(self.min))
133-
max = np.array(tuple(self.max) + tuple(self.max))
132+
min = np.array(tuple(self.min) + tuple(g.min))
133+
max = np.array(tuple(self.max) + tuple(g.max))
134134

135135
return UniformCartesianGrid(min, max, n)
136136

0 commit comments

Comments
 (0)