3636
3737from typing import cast
3838
39- import pytensor
4039import pytensor .tensor as pt
4140
4241from pytensor .graph .basic import Apply , Constant , Variable
4342from pytensor .graph .fg import FunctionGraph
44- from pytensor .graph .op import Op , compute_test_value
43+ from pytensor .graph .op import Op
4544from pytensor .graph .rewriting .basic import EquilibriumGraphRewriter , node_rewriter
4645from pytensor .graph .traversal import ancestors
4746from pytensor .ifelse import IfElse , ifelse
6059 AdvancedSubtensor1 ,
6160 as_index_literal ,
6261 get_canonical_form_slice ,
63- is_basic_idx ,
62+ unflatten_index_variables ,
6463)
6564from pytensor .tensor .type import TensorType
66- from pytensor .tensor .type_other import NoneConst , NoneTypeT , SliceType
65+ from pytensor .tensor .type_other import NoneConst , NoneTypeT
6766from pytensor .tensor .variable import TensorVariable
6867
6968from pymc .logprob .abstract import (
8988from pymc .pytensorf import constant_fold
9089
9190
92- def is_newaxis (x ):
93- return isinstance (x , type (None )) or isinstance (getattr (x , "type" , None ), NoneTypeT )
94-
95-
9691def expand_indices (
97- indices : tuple [Variable | slice | None , ...], shape : tuple [TensorVariable ]
92+ indices : tuple [Variable | slice , ...],
93+ shape : tuple [TensorVariable ],
9894) -> tuple [TensorVariable ]:
9995 """Convert basic and/or advanced indices into a single, broadcasted advanced indexing operation.
10096
@@ -106,32 +102,34 @@ def expand_indices(
106102 The shape of the array being indexed.
107103
108104 """
109- n_non_newaxis = sum (1 for idx in indices if not is_newaxis (idx ))
110- n_missing_dims = len (shape ) - n_non_newaxis
111- full_indices = list (indices ) + [slice (None )] * n_missing_dims
105+ # Make implicit slice(None) explicit
106+ full_indices = list (indices ) + [slice (None )] * (len (shape ) - len (indices ))
112107
113- # We need to know if a "subspace" was generated by advanced indices
114- # bookending basic indices. If so, we move the advanced indexing subspace
115- # to the "front" of the shape (i.e. left-most indices/last-most
116- # dimensions).
117- index_types = [is_basic_idx (idx ) for idx in full_indices ]
108+ # We need to know if a "subspace" was generated by advanced indices bookending basic indices.
109+ # If so, we move the advanced indexing subspace to the "front" of the shape
110+ # (i.e. left-most indices/last-most dimensions).
111+ # NOTE: Scalar integers are classified as "advanced" here even though they may be basic
112+ # in a pure-basic-indexing context. This is safe because when all non-slice indices are
113+ # scalars, n_subspace_dims=0 and the moved_subspace logic becomes a no-op.
114+ is_basic_idx = [isinstance (idx , slice ) for idx in full_indices ]
118115
119116 first_adv_idx = len (shape )
120117 try :
121- first_adv_idx = index_types .index (False )
122- first_bsc_after_adv_idx = index_types .index (True , first_adv_idx )
123- index_types .index (False , first_bsc_after_adv_idx )
118+ first_adv_idx = is_basic_idx .index (False )
119+ first_bsc_after_adv_idx = is_basic_idx .index (True , first_adv_idx )
120+ # If this fails, we get to the except branch
121+ is_basic_idx .index (False , first_bsc_after_adv_idx )
124122 moved_subspace = True
125123 except ValueError :
126124 moved_subspace = False
127125
128- n_basic_indices = sum (index_types )
126+ n_basic_indices = sum (is_basic_idx )
129127
130128 # The number of dimensions in the subspace created by the advanced indices
131129 n_subspace_dims = max (
132130 (
133131 getattr (idx , "ndim" , 0 )
134- for idx , is_basic in zip (full_indices , index_types )
132+ for idx , is_basic in zip (full_indices , is_basic_idx )
135133 if not is_basic
136134 ),
137135 default = 0 ,
@@ -144,63 +142,53 @@ def expand_indices(
144142 shape_copy = list (shape )
145143 n_preceding_basics = 0
146144 for d , idx in enumerate (full_indices ):
147- if not is_basic_idx ( idx ):
148- s = shape_copy . pop ( 0 )
149-
145+ s = shape_copy . pop ( 0 )
146+ if not isinstance ( idx , slice ):
147+ # Advanced indexing
150148 idx = pt .as_tensor (idx )
151149
152150 if moved_subspace :
153- # The subspace generated by advanced indices appear as the
154- # upper dimensions in the "expanded" index space, so we need to
155- # add broadcast dimensions for the non-basic indices to the end
156- # of these advanced indices
157- expanded_idx = idx [(Ellipsis ,) + (None ,) * n_basic_indices ]
151+ # The subspace generated by advanced indices appear as the leftmost dimensions
152+ # in the "expanded" index space, so we need to add broadcast dimensions
153+ # for the non-basic indices to the end of these advanced indices
154+ expanded_idx = pt .shape_padright (idx , n_basic_indices )
158155 else :
159156 # In this case, we need to add broadcast dimensions for the
160157 # basic indices that proceed and follow the group of advanced
161158 # indices; otherwise, a contiguous group of advanced indices
162159 # forms a broadcasted set of indices that are iterated over
163160 # within the same subspace, which means that all their
164161 # corresponding "expanded" indices have exactly the same shape.
165- expanded_idx = idx [(None ,) * n_preceding_basics ][
166- (Ellipsis ,) + (None ,) * (n_basic_indices - n_preceding_basics )
167- ]
162+ expanded_idx = pt .shape_padleft (idx , n_preceding_basics )
163+ expanded_idx = pt .shape_padright (expanded_idx , n_basic_indices - n_preceding_basics )
168164 else :
169- if is_newaxis (idx ):
170- n_preceding_basics += 1
171- continue
172-
173- s = shape_copy .pop (0 )
174-
175- if isinstance (idx , slice ) or isinstance (getattr (idx , "type" , None ), SliceType ):
176- idx = as_index_literal (idx )
177- idx_slice , _ = get_canonical_form_slice (idx , s )
178- idx = pt .arange (idx_slice .start , idx_slice .stop , idx_slice .step )
165+ # Basic indexing (slice)
166+ idx = as_index_literal (idx )
167+ idx_slice , _ = get_canonical_form_slice (idx , s )
168+ idx = pt .arange (idx_slice .start , idx_slice .stop , idx_slice .step )
179169
180170 if moved_subspace :
181- # Basic indices appear in the lower dimensions
182- # (i.e. right-most) in the output, and are preceded by
183- # the subspace generated by the advanced indices.
184- expanded_idx = idx [( None ,) * ( n_subspace_dims + n_preceding_basics )][
185- ( Ellipsis ,) + ( None ,) * ( n_basic_indices - n_preceding_basics - 1 )
186- ]
171+ # Basic indices appear in the rightmost dimensions in the output,
172+ # and are preceded by the subspace generated by the advanced indices.
173+ expanded_idx = pt . shape_padleft ( idx , n_subspace_dims + n_preceding_basics )
174+ expanded_idx = pt . shape_padright (
175+ expanded_idx , n_basic_indices - n_preceding_basics - 1
176+ )
187177 else :
188178 # In this case, we need to know when the basic indices have
189- # moved past the contiguous group of advanced indices (in the
190- # "expanded" index space), so that we can properly pad those
191- # dimensions in this basic index's shape.
179+ # moved past the contiguous group of advanced indices
180+ # (in the "expanded" index space), so that we can properly
181+ # pad those dimensions in this basic index's shape.
192182 # Don't forget that a single advanced index can introduce an
193183 # arbitrary number of dimensions to the expanded index space.
194184
195- # If we're currently at a basic index that's past the first
196- # advanced index, then we're necessarily past the group of
197- # advanced indices.
185+ # If we're currently at a basic index that's past the first advanced index,
186+ # then we're necessarily past the group of advanced indices.
198187 n_preceding_dims = (
199188 n_subspace_dims if d > first_adv_idx else 0
200189 ) + n_preceding_basics
201- expanded_idx = idx [(None ,) * n_preceding_dims ][
202- (Ellipsis ,) + (None ,) * (n_output_dims - n_preceding_dims - 1 )
203- ]
190+ expanded_idx = pt .shape_padleft (idx , n_preceding_dims )
191+ expanded_idx = pt .shape_padright (expanded_idx , n_output_dims - n_preceding_dims - 1 )
204192
205193 n_preceding_basics += 1
206194
@@ -227,13 +215,14 @@ def rv_pull_down(x: TensorVariable) -> TensorVariable:
227215class MixtureRV (MeasurableOp , Op ):
228216 """A placeholder used to specify a log-likelihood for a mixture sub-graph."""
229217
230- __props__ = ("indices_end_idx" , "out_dtype" , "out_broadcastable" )
218+ __props__ = ("indices_end_idx" , "out_dtype" , "out_broadcastable" , "idx_list" )
231219
232- def __init__ (self , indices_end_idx , out_dtype , out_broadcastable ):
220+ def __init__ (self , indices_end_idx , out_dtype , out_broadcastable , idx_list ):
233221 super ().__init__ ()
234222 self .indices_end_idx = indices_end_idx
235223 self .out_dtype = out_dtype
236224 self .out_broadcastable = out_broadcastable
225+ self .idx_list = tuple (idx_list )
237226
238227 def make_node (self , * inputs ):
239228 return Apply (self , list (inputs ), [TensorType (self .out_dtype , self .out_broadcastable )()])
@@ -285,6 +274,7 @@ def find_measurable_index_mixture(fgraph, node):
285274 created for each ``i`` in ``enumerate(mixture_comps)``.
286275 """
287276 mixing_indices = node .inputs [1 :]
277+ idx_list = node .op .idx_list
288278
289279 # TODO: Add check / test case for Advanced Boolean indexing
290280 if isinstance (node .op , AdvancedSubtensor | AdvancedSubtensor1 ):
@@ -313,18 +303,9 @@ def find_measurable_index_mixture(fgraph, node):
313303 1 + len (mixing_indices ),
314304 old_mixture_rv .dtype ,
315305 old_mixture_rv .broadcastable ,
306+ idx_list = idx_list ,
316307 )
317- new_node = mix_op .make_node (* ([join_axis , * mixing_indices , * mixture_rvs ]))
318-
319- new_mixture_rv = new_node .default_output ()
320-
321- if pytensor .config .compute_test_value != "off" :
322- # We can't use `MixtureRV` to compute a test value; instead, we'll use
323- # the original node's test value.
324- if not hasattr (old_mixture_rv .tag , "test_value" ):
325- compute_test_value (node )
326-
327- new_mixture_rv .tag .test_value = old_mixture_rv .tag .test_value
308+ new_mixture_rv = mix_op (join_axis , * mixing_indices , * mixture_rvs )
328309
329310 return [new_mixture_rv ]
330311
@@ -334,16 +315,17 @@ def logprob_MixtureRV(op, values, *inputs: TensorVariable | slice | None, name=N
334315 (value ,) = values
335316
336317 join_axis = cast (Variable , inputs [0 ])
337- indices = cast ( TensorVariable , inputs [1 : op .indices_end_idx ])
318+ flat_indices = inputs [1 : op .indices_end_idx ]
338319 comp_rvs = cast (TensorVariable , inputs [op .indices_end_idx :])
339320
340- assert len (indices ) > 0
321+ assert len (flat_indices ) > 0
341322
342- if len (indices ) > 1 or indices [0 ].ndim > 0 :
323+ # Use flat_indices (tensor variables only) for the scalar vs advanced check.
324+ # Slices in idx_list don't indicate advanced indexing.
325+ if len (flat_indices ) > 1 or flat_indices [0 ].ndim > 0 :
343326 if isinstance (join_axis .type , NoneTypeT ):
344327 # `join_axis` will be `NoneConst` if the "join" was a `MakeVector`
345- # (i.e. scalar measurable variables were combined to make a
346- # vector).
328+ # (i.e. scalar measurable variables were combined to make a vector).
347329 # Since some form of advanced indexing is necessarily occurring, we
348330 # need to reformat the MakeVector arguments so that they fit the
349331 # `Join` format expected by the logic below.
@@ -354,7 +336,9 @@ def logprob_MixtureRV(op, values, *inputs: TensorVariable | slice | None, name=N
354336 join_axis_val = constant_fold ((join_axis ,))[0 ].item ()
355337 original_shape = shape_tuple (comp_rvs [0 ])
356338
357- bcast_indices = expand_indices (indices , original_shape )
339+ # Reconstruct full indices
340+ full_indices = unflatten_index_variables (list (flat_indices ), op .idx_list )
341+ bcast_indices = expand_indices (full_indices , original_shape )
358342
359343 logp_val = pt .empty (bcast_indices [0 ].shape )
360344
@@ -393,7 +377,7 @@ def logprob_MixtureRV(op, values, *inputs: TensorVariable | slice | None, name=N
393377 if join_axis_val is not None :
394378 comp_logp = pt .squeeze (comp_logp , axis = join_axis_val )
395379 logp_val += ifelse (
396- pt .eq (indices [0 ], i ),
380+ pt .eq (flat_indices [0 ], i ),
397381 comp_logp ,
398382 pt .zeros_like (comp_logp ),
399383 )
0 commit comments