13
13
# limitations under the License.
14
14
import warnings
15
15
16
- from typing import Any , Optional , Tuple , Union
16
+ from typing import Any , Optional , Union
17
17
18
18
import aesara
19
19
import aesara .tensor as at
28
28
from aesara .raise_op import Assert
29
29
from aesara .tensor import TensorVariable
30
30
from aesara .tensor .basic_opt import ShapeFeature , topo_constant_folding
31
+ from aesara .tensor .extra_ops import CumOp
31
32
from aesara .tensor .random .op import RandomVariable
32
- from aesara .tensor .random .utils import normalize_size_param
33
33
34
34
from pymc .aesaraf import change_rv_size , convert_observed_data , floatX , intX
35
35
from pymc .distributions import distribution , multivariate
36
36
from pymc .distributions .continuous import Flat , Normal , get_tau_sigma
37
- from pymc .distributions .dist_math import check_parameters
38
37
from pymc .distributions .distribution import SymbolicDistribution , _moment , moment
39
38
from pymc .distributions .logprob import ignore_logprob , logp
40
- from pymc .distributions .shape_utils import (
41
- Dims ,
42
- Shape ,
43
- convert_dims ,
44
- rv_size_is_none ,
45
- to_tuple ,
46
- )
39
+ from pymc .distributions .shape_utils import Dims , Shape , convert_dims , to_tuple
47
40
from pymc .model import modelcontext
48
41
from pymc .util import check_dist_not_registered
49
42
@@ -114,110 +107,8 @@ def get_steps(
114
107
return inferred_steps
115
108
116
109
117
- class GaussianRandomWalkRV (RandomVariable ):
118
- """
119
- GaussianRandomWalk Random Variable
120
- """
121
-
122
- name = "GaussianRandomWalk"
123
- ndim_supp = 1
124
- ndims_params = [0 , 0 , 0 , 0 ]
125
- dtype = "floatX"
126
- _print_name = ("GaussianRandomWalk" , "\\ operatorname{GaussianRandomWalk}" )
127
-
128
- def make_node (self , rng , size , dtype , mu , sigma , init_dist , steps ):
129
- steps = at .as_tensor_variable (steps )
130
- if not steps .ndim == 0 or not steps .dtype .startswith ("int" ):
131
- raise ValueError ("steps must be an integer scalar (ndim=0)." )
132
-
133
- mu = at .as_tensor_variable (mu )
134
- sigma = at .as_tensor_variable (sigma )
135
- init_dist = at .as_tensor_variable (init_dist )
136
-
137
- # Resize init distribution
138
- size = normalize_size_param (size )
139
- # If not explicit, size is determined by the shapes of mu, sigma, and init
140
- init_dist_size = (
141
- size if not rv_size_is_none (size ) else at .broadcast_shape (mu , sigma , init_dist )
142
- )
143
- init_dist = change_rv_size (init_dist , init_dist_size )
144
-
145
- return super ().make_node (rng , size , dtype , mu , sigma , init_dist , steps )
146
-
147
- def _supp_shape_from_params (self , dist_params , reop_param_idx = 0 , param_shapes = None ):
148
- steps = dist_params [3 ]
149
-
150
- return (steps + 1 ,)
151
-
152
- @classmethod
153
- def rng_fn (
154
- cls ,
155
- rng : np .random .RandomState ,
156
- mu : Union [np .ndarray , float ],
157
- sigma : Union [np .ndarray , float ],
158
- init_dist : Union [np .ndarray , float ],
159
- steps : int ,
160
- size : Tuple [int ],
161
- ) -> np .ndarray :
162
- """Gaussian Random Walk generator.
163
-
164
- The init value is drawn from the Normal distribution with the same sigma as the
165
- innovations.
166
-
167
- Notes
168
- -----
169
- Currently does not support custom init distribution
170
-
171
- Parameters
172
- ----------
173
- rng: np.random.RandomState
174
- Numpy random number generator
175
- mu: array_like of float
176
- Random walk mean
177
- sigma: array_like of float
178
- Standard deviation of innovation (sigma > 0)
179
- init_dist: array_like of float
180
- Initialization value for GaussianRandomWalk
181
- steps: int
182
- Length of random walk, must be greater than 1. Returned array will be of size+1 to
183
- account as first value is initial value
184
- size: tuple of int
185
- The number of Random Walk time series generated
186
-
187
- Returns
188
- -------
189
- ndarray
190
- """
191
-
192
- if steps < 1 :
193
- raise ValueError ("Steps must be greater than 0" )
194
-
195
- # If size is None then the returned series should be (*implied_dims, 1+steps)
196
- if size is None :
197
- # broadcast parameters with each other to find implied dims
198
- bcast_shape = np .broadcast_shapes (
199
- np .asarray (mu ).shape ,
200
- np .asarray (sigma ).shape ,
201
- np .asarray (init_dist ).shape ,
202
- )
203
- dist_shape = (* bcast_shape , int (steps ))
204
-
205
- # If size is None then the returned series should be (*size, 1+steps)
206
- else :
207
- dist_shape = (* size , int (steps ))
208
-
209
- # Add one dimension to the right, so that mu and sigma broadcast safely along
210
- # the steps dimension
211
- innovations = rng .normal (loc = mu [..., None ], scale = sigma [..., None ], size = dist_shape )
212
- grw = np .concatenate ([init_dist [..., None ], innovations ], axis = - 1 )
213
- return np .cumsum (grw , axis = - 1 )
214
-
215
-
216
- gaussianrandomwalk = GaussianRandomWalkRV ()
217
-
218
-
219
- class GaussianRandomWalk (distribution .Continuous ):
220
- r"""Random Walk with Normal innovations.
110
+ class GaussianRandomWalk (SymbolicDistribution ):
111
+ r"""Random Walk with Normal innovations
221
112
222
113
Parameters
223
114
----------
@@ -236,8 +127,6 @@ class GaussianRandomWalk(distribution.Continuous):
236
127
provided.
237
128
"""
238
129
239
- rv_op = gaussianrandomwalk
240
-
241
130
def __new__ (cls , * args , steps = None , ** kwargs ):
242
131
steps = get_steps (
243
132
steps = steps ,
@@ -269,6 +158,8 @@ def dist(cls, mu=0.0, sigma=1.0, *, init_dist=None, steps=None, **kwargs) -> at.
269
158
FutureWarning ,
270
159
)
271
160
init_dist = kwargs .pop ("init" )
161
+ if not steps .ndim == 0 :
162
+ raise ValueError ("steps must be an integer scalar (ndim=0)." )
272
163
273
164
# If no scalar distribution is passed then initialize with a Normal of same mu and sigma
274
165
if init_dist is None :
@@ -293,39 +184,67 @@ def dist(cls, mu=0.0, sigma=1.0, *, init_dist=None, steps=None, **kwargs) -> at.
293
184
294
185
return super ().dist ([mu , sigma , init_dist , steps ], ** kwargs )
295
186
296
- def moment (rv , size , mu , sigma , init_dist , steps ):
297
- grw_moment = at .zeros_like (rv )
298
- grw_moment = at .set_subtensor (grw_moment [..., 0 ], moment (init_dist ))
299
- # Add one dimension to the right, so that mu broadcasts safely along the steps
300
- # dimension
301
- grw_moment = at .set_subtensor (grw_moment [..., 1 :], mu [..., None ])
302
- return at .cumsum (grw_moment , axis = - 1 )
303
-
304
- def logp (
305
- value : at .Variable ,
306
- mu : at .Variable ,
307
- sigma : at .Variable ,
308
- init_dist : at .Variable ,
309
- steps : at .Variable ,
310
- ) -> at .TensorVariable :
311
- """Calculate log-probability of Gaussian Random Walk distribution at specified value."""
312
-
313
- # Calculate initialization logp
314
- init_logp = logp (init_dist , value [..., 0 ])
315
-
316
- # Make time series stationary around the mean value
317
- stationary_series = value [..., 1 :] - value [..., :- 1 ]
318
- # Add one dimension to the right, so that mu and sigma broadcast safely along
319
- # the steps dimension
320
- series_logp = logp (Normal .dist (mu [..., None ], sigma [..., None ]), stationary_series )
321
-
322
- return check_parameters (
323
- init_logp + series_logp .sum (axis = - 1 ),
324
- steps > 0 ,
325
- msg = "steps > 0" ,
187
+ @classmethod
188
+ def ndim_supp (cls , * args ):
189
+ return 1
190
+
191
+ @classmethod
192
+ def rv_op (cls , mu , sigma , init_dist , steps , size = None ):
193
+ # If not explicit, size is determined by the shapes of mu, sigma, and init
194
+ if size is not None :
195
+ # we have all the information regarding size from users or .dist()
196
+ init_size = size
197
+ else :
198
+ # we infer size from parameters
199
+ init_size = at .broadcast_shape (
200
+ mu ,
201
+ sigma ,
202
+ init_dist ,
203
+ )
204
+
205
+ # TODO: extend for multivariate init
206
+ init_dist = change_rv_size (init_dist , init_size )
207
+ innovation_dist = Normal .dist (mu [..., None ], sigma [..., None ], size = (* init_size , steps ))
208
+ rv_out = at .cumsum (at .concatenate ([init_dist [..., None ], innovation_dist ], axis = - 1 ), axis = - 1 )
209
+
210
+ rv_out .tag .mu = mu
211
+ rv_out .tag .sigma = sigma
212
+ rv_out .tag .init_dist = init_dist
213
+ rv_out .tag .innovation_dist = innovation_dist
214
+ rv_out .tag .steps = steps
215
+ rv_out .tag .is_grw = True # for moment dispatching
216
+
217
+ return rv_out
218
+
219
+ @classmethod
220
+ def change_size (cls , rv , new_size , expand = False ):
221
+ if expand :
222
+ new_size = at .concatenate ([new_size , rv .shape [:- 1 ]])
223
+
224
+ return cls .rv_op (
225
+ mu = rv .tag .mu ,
226
+ sigma = rv .tag .sigma ,
227
+ init_dist = rv .tag .init_dist ,
228
+ steps = rv .tag .steps ,
229
+ size = new_size ,
326
230
)
327
231
328
232
233
+ @_moment .register (CumOp )
234
+ def moment_grw (op , rv , dist_params ):
235
+ """
236
+ This moment dispatch is currently only applicable for a GaussianRandomWalk.
237
+ TODO: Encapsulate GRW graph in an OpFromGraph so that we can dispatch
238
+ the moment directly on it
239
+ """
240
+ if not getattr (rv .tag , "is_grw" , False ):
241
+ raise NotImplementedError ("Moment not implemented for `CumOp`" )
242
+ init_dist = rv .tag .init_dist
243
+ innovation_dist = rv .tag .innovation_dist
244
+ grw_moment = at .concatenate ([moment (init_dist )[..., None ], moment (innovation_dist )], axis = - 1 )
245
+ return at .cumsum (grw_moment , axis = - 1 )
246
+
247
+
329
248
class AutoRegressiveRV (OpFromGraph ):
330
249
"""A placeholder used to specify a log-likelihood for an AR sub-graph."""
331
250
0 commit comments