Skip to content

Commit 0aceb97

Browse files
committed
Adapt to AdvancedIndexing changes
1 parent b92f29e commit 0aceb97

2 files changed

Lines changed: 62 additions & 133 deletions

File tree

pymc/logprob/mixture.py

Lines changed: 62 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@
3636

3737
from typing import cast
3838

39-
import pytensor
4039
import pytensor.tensor as pt
4140

4241
from pytensor.graph.basic import Apply, Constant, Variable
4342
from pytensor.graph.fg import FunctionGraph
44-
from pytensor.graph.op import Op, compute_test_value
43+
from pytensor.graph.op import Op
4544
from pytensor.graph.rewriting.basic import EquilibriumGraphRewriter, node_rewriter
4645
from pytensor.graph.traversal import ancestors
4746
from pytensor.ifelse import IfElse, ifelse
@@ -60,10 +59,10 @@
6059
AdvancedSubtensor1,
6160
as_index_literal,
6261
get_canonical_form_slice,
63-
is_basic_idx,
62+
unflatten_index_variables,
6463
)
6564
from pytensor.tensor.type import TensorType
66-
from pytensor.tensor.type_other import NoneConst, NoneTypeT, SliceType
65+
from pytensor.tensor.type_other import NoneConst, NoneTypeT
6766
from pytensor.tensor.variable import TensorVariable
6867

6968
from pymc.logprob.abstract import (
@@ -89,12 +88,9 @@
8988
from 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-
9691
def 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:
227215
class 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
)

tests/logprob/test_mixture.py

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -827,61 +827,6 @@ def test_expand_indices_single_indices(A_parts, indices):
827827
assert np.array_equal(res, exp_res)
828828

829829

830-
@pytest.mark.parametrize(
831-
"A_parts, indices",
832-
[
833-
(
834-
(
835-
np.random.normal(size=(4, 3)),
836-
np.random.normal(size=(4, 3)),
837-
np.random.normal(size=(4, 3)),
838-
),
839-
(None,),
840-
),
841-
(
842-
(
843-
np.random.normal(size=(4, 3)),
844-
np.random.normal(size=(4, 3)),
845-
np.random.normal(size=(4, 3)),
846-
),
847-
(None, None, None),
848-
),
849-
(
850-
(
851-
np.random.normal(size=(4, 3)),
852-
np.random.normal(size=(4, 3)),
853-
np.random.normal(size=(4, 3)),
854-
),
855-
(None, 1, None, 0, None),
856-
),
857-
(
858-
(
859-
np.random.normal(size=(4, 3)),
860-
np.random.normal(size=(4, 3)),
861-
np.random.normal(size=(4, 3)),
862-
),
863-
(slice(2, 3), None, 1, None, 0, None),
864-
),
865-
(
866-
(
867-
np.random.normal(size=(4, 3)),
868-
np.random.normal(size=(4, 3)),
869-
np.random.normal(size=(4, 3)),
870-
),
871-
(slice(2, 3), None, 1, 0, None),
872-
),
873-
],
874-
)
875-
def test_expand_indices_newaxis(A_parts, indices):
876-
A = pt.stack(A_parts)
877-
at_indices = [as_index_constant(idx) for idx in indices]
878-
full_indices = expand_indices(at_indices, shape_tuple(A))
879-
assert len(full_indices) == A.ndim
880-
exp_res = A[indices].eval()
881-
res = A[full_indices].eval()
882-
assert np.array_equal(res, exp_res)
883-
884-
885830
def test_mixture_with_DiracDelta():
886831
srng = pt.random.RandomStream(29833)
887832

0 commit comments

Comments
 (0)