Skip to content

Raise explicitly on Python methods that are incompatible with lazy variables #1190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pytensor/compile/function/pfunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ def construct_pfunc_ins_and_outs(
if not fgraph:
# Extend the outputs with the updates on input variables so they are
# also cloned
additional_outputs = [i.update for i in inputs if i.update]
additional_outputs = [i.update for i in inputs if i.update is not None]
if outputs is None:
out_list = []
else:
Expand Down Expand Up @@ -608,7 +608,7 @@ def construct_pfunc_ins_and_outs(
new_i.variable = iv

# If needed, replace the input's update by its cloned equivalent
if i.update:
if i.update is not None:
new_i.update = clone_d[i.update]

new_inputs.append(new_i)
Expand Down
10 changes: 7 additions & 3 deletions pytensor/compile/function/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def std_fgraph(
update_mapping = {}
out_idx = len(output_specs)
for idx, input_spec in enumerate(input_specs):
if input_spec.update:
if input_spec.update is not None:
updates.append(input_spec.update)
update_mapping[out_idx] = idx
out_idx += 1
Expand Down Expand Up @@ -1195,7 +1195,7 @@ def insert_deepcopy(fgraph, wrapped_inputs, wrapped_outputs):
updated_fgraph_inputs = {
fgraph_i
for i, fgraph_i in zip(wrapped_inputs, fgraph.inputs, strict=True)
if getattr(i, "update", False)
if getattr(i, "update", None) is not None
}

# We can't use fgraph.inputs as this don't include Constant Value.
Expand Down Expand Up @@ -1351,7 +1351,11 @@ def check_unused_inputs(inputs, outputs, on_unused_input):
ancestors(
(
[o.variable for o in outputs]
+ [i.update for i in inputs if getattr(i, "update", False)]
+ [
i.update
for i in inputs
if getattr(i, "update", None) is not None
]
),
blockers=[i.variable for i in inputs],
)
Expand Down
2 changes: 1 addition & 1 deletion pytensor/compile/nanguardmode.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def _is_numeric_value(arr, var):
return False
elif isinstance(arr, np.random.mtrand.RandomState | np.random.Generator):
return False
elif var and isinstance(var.type, RandomType):
elif var is not None and isinstance(var.type, RandomType):
return False
elif isinstance(arr, slice):
return False
Expand Down
36 changes: 31 additions & 5 deletions pytensor/scalar/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,37 @@


class _scalar_py_operators:
# These can't work because Python requires native output types
def __bool__(self):
raise TypeError(

Check warning on line 828 in pytensor/scalar/basic.py

View check run for this annotation

Codecov / codecov/patch

pytensor/scalar/basic.py#L828

Added line #L828 was not covered by tests
"ScalarVariable cannot be converted to Python boolean. "
"Call `.astype(bool)` for the symbolic equivalent."
)

def __index__(self):
raise TypeError(

Check warning on line 834 in pytensor/scalar/basic.py

View check run for this annotation

Codecov / codecov/patch

pytensor/scalar/basic.py#L834

Added line #L834 was not covered by tests
"ScalarVariable cannot be converted to Python integer. "
"Call `.astype(int)` for the symbolic equivalent."
)

def __int__(self):
raise TypeError(

Check warning on line 840 in pytensor/scalar/basic.py

View check run for this annotation

Codecov / codecov/patch

pytensor/scalar/basic.py#L840

Added line #L840 was not covered by tests
"ScalarVariable cannot be converted to Python integer. "
"Call `.astype(int)` for the symbolic equivalent."
)

def __float__(self):
raise TypeError(

Check warning on line 846 in pytensor/scalar/basic.py

View check run for this annotation

Codecov / codecov/patch

pytensor/scalar/basic.py#L846

Added line #L846 was not covered by tests
"ScalarVariable cannot be converted to Python float. "
"Call `.astype(float)` for the symbolic equivalent."
)

def __complex__(self):
raise TypeError(

Check warning on line 852 in pytensor/scalar/basic.py

View check run for this annotation

Codecov / codecov/patch

pytensor/scalar/basic.py#L852

Added line #L852 was not covered by tests
"ScalarVariable cannot be converted to Python complex number. "
"Call `.astype(complex)` for the symbolic equivalent."
)

# So that we can simplify checking code when we have a mixture of ScalarType
# variables and Tensor variables
ndim = 0
Expand All @@ -843,11 +874,6 @@
def __neg__(self):
return neg(self)

# CASTS
# def __int__(self): return AsInt(self).out
# def __float__(self): return AsDouble(self).out
# def __complex__(self): return AsComplex(self).out

# BITWISE
def __invert__(self):
return invert(self)
Expand Down
4 changes: 2 additions & 2 deletions pytensor/scalar/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ def __init__(
constant = []
if not len(init) == len(update):
raise ValueError("An update must be given for each init variable")
if until:
if until is not None:
inputs, outputs = clone([*init, *constant], [*update, until])
else:
inputs, outputs = clone([*init, *constant], update)

self.is_while = bool(until)
self.is_while = until is not None
self.inputs, self.outputs = self._cleanup_graph(inputs, outputs)
self._validate_updates(self.inputs, self.outputs)

Expand Down
2 changes: 1 addition & 1 deletion pytensor/scalar/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ def inner_loop_a(sum_a, delta, xpow, k_minus_one_minus_n, fac, dfac, x):
dfac = k_minus_one_minus_n * dfac + fac
fac *= k_minus_one_minus_n
delta = dfac / xpow
return (sum_a, delta, xpow, k_minus_one_minus_n, fac, dfac), ()
return (sum_a, delta, xpow, k_minus_one_minus_n, fac, dfac), None

init = [sum_a0, delta, xpow, k_minus_one_minus_n, fac, dfac]
constant = [x]
Expand Down
2 changes: 1 addition & 1 deletion pytensor/scan/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ def wrap_into_list(x):
# user-specified within the inner-function (e.g. by returning an update
# `dict`) or the `SharedVariable.default_update`s of a shared variable
# created in the inner-function.
if input.update and (is_local or input.variable in updates):
if input.update is not None and (is_local or input.variable in updates):
# We need to remove the `default_update`s on the shared
# variables created within the context of the loop function
# (e.g. via use of `RandomStream`); otherwise, they'll get
Expand Down
46 changes: 24 additions & 22 deletions pytensor/tensor/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3430,7 +3430,14 @@ def __getitem__(self, *args):
raise NotImplementedError(
"Not implemented for slices whose step is complex"
)
ranges = [arange(sl.start or 0, sl.stop, sl.step or 1) for sl in args[0]]
ranges = [
arange(
sl.start if sl.start is not None else 0,
sl.stop,
sl.step if sl.step is not None else 1,
)
for sl in args[0]
]
shapes = [
tuple([1] * j + [r.shape[0]] + [1] * (ndim - 1 - j))
for j, r in enumerate(ranges)
Expand Down Expand Up @@ -3481,20 +3488,18 @@ class PermuteRowElements(Op):
permutation instead.
"""

__props__ = ()
__props__ = ("inverse",)

def __init__(self, inverse: bool):
super().__init__()
self.inverse = inverse

def make_node(self, x, y, inverse):
def make_node(self, x, y):
x = as_tensor_variable(x)
y = as_tensor_variable(y)
if inverse: # as_tensor_variable does not accept booleans
inverse = as_tensor_variable(1)
else:
inverse = as_tensor_variable(0)

# y should contain integers
assert y.type.dtype in integer_dtypes
# Inverse should be an integer scalar
assert inverse.type.ndim == 0 and inverse.type.dtype in integer_dtypes

# Match shapes of x and y
x_dim = x.type.ndim
Expand All @@ -3511,7 +3516,7 @@ def make_node(self, x, y, inverse):
]
out_type = tensor(dtype=x.type.dtype, shape=out_shape)

inputlist = [x, y, inverse]
inputlist = [x, y]
outputlist = [out_type]
return Apply(self, inputlist, outputlist)

Expand Down Expand Up @@ -3564,7 +3569,7 @@ def _rec_perform(self, node, x, y, inverse, out, curdim):
raise ValueError(f"Dimension mismatch: {xs0}, {ys0}")

def perform(self, node, inp, out):
x, y, inverse = inp
x, y = inp
(outs,) = out
x_s = x.shape
y_s = y.shape
Expand All @@ -3587,7 +3592,7 @@ def perform(self, node, inp, out):
if outs[0] is None or outs[0].shape != out_s:
outs[0] = np.empty(out_s, dtype=x.dtype)

self._rec_perform(node, x, y, inverse, outs[0], curdim=0)
self._rec_perform(node, x, y, self.inverse, outs[0], curdim=0)

def infer_shape(self, fgraph, node, in_shapes):
from pytensor.tensor.math import maximum
Expand All @@ -3599,14 +3604,14 @@ def infer_shape(self, fgraph, node, in_shapes):
return [out_shape]

def grad(self, inp, grads):
from pytensor.tensor.math import Sum, eq
from pytensor.tensor.math import Sum

x, y, inverse = inp
x, y = inp
(gz,) = grads
# First, compute the gradient wrt the broadcasted x.
# If 'inverse' is False (0), apply the inverse of y on gz.
# Else, apply y on gz.
gx = permute_row_elements(gz, y, eq(inverse, 0))
gx = permute_row_elements(gz, y, not self.inverse)

# If x has been broadcasted along some axes, we need to sum
# the gradient over these axes, but keep the dimension (as
Expand Down Expand Up @@ -3643,20 +3648,17 @@ def grad(self, inp, grads):
if x.type.dtype in discrete_dtypes:
gx = x.zeros_like()

# The elements of y and of inverse both affect the output,
# The elements of y affect the output,
# so they are connected to the output,
# and the transformation isn't defined if their values
# are non-integer, so the gradient with respect to them is
# undefined

return [gx, grad_undefined(self, 1, y), grad_undefined(self, 1, inverse)]


_permute_row_elements = PermuteRowElements()
return [gx, grad_undefined(self, 1, y)]


def permute_row_elements(x, y, inverse=0):
return _permute_row_elements(x, y, inverse)
def permute_row_elements(x, y, inverse=False):
return PermuteRowElements(inverse=inverse)(x, y)


def inverse_permutation(perm):
Expand Down
4 changes: 2 additions & 2 deletions pytensor/tensor/conv/abstract_conv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2199,7 +2199,7 @@ def __init__(
):
border_mode = "valid"

self.imshp = tuple(imshp) if imshp else (None,) * (2 + convdim)
self.imshp = tuple(imshp) if imshp is not None else (None,) * (2 + convdim)
for imshp_i in self.imshp:
if imshp_i is not None:
# Components of imshp should be constant or ints
Expand All @@ -2209,7 +2209,7 @@ def __init__(
raise ValueError(
"imshp should be None or a tuple of constant int values"
).with_traceback(sys.exc_info()[2])
if kshp:
if kshp is not None:
self.kshp = tuple(kshp)
else:
self.kshp = (None,) * ((2 + 2 * convdim) if unshared else (2 + convdim))
Expand Down
8 changes: 4 additions & 4 deletions pytensor/tensor/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -1811,14 +1811,14 @@ def R_op(self, inputs, eval_points):
if eval_points[0] is None and eval_points[1] is None:
return [None]

if eval_points[0]:
if eval_points[0] is not None:
t1 = self(eval_points[0], inputs[1])
if eval_points[1]:
if eval_points[1] is not None:
t2 = self(inputs[0], eval_points[1])

if eval_points[0] and eval_points[1]:
if eval_points[0] is not None and eval_points[1] is not None:
return [t1 + t2]
elif eval_points[0]:
elif eval_points[0] is not None:
return [t1]
else:
return [t2]
Expand Down
10 changes: 6 additions & 4 deletions pytensor/tensor/rewriting/blas.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ def local_dot22_to_dot22scalar(fgraph, node):
"""
if node.op != mul:
return False
i_dot22 = [x.owner and x.owner.op == _dot22 for x in node.inputs]
i_dot22 = [x.owner is not None and x.owner.op == _dot22 for x in node.inputs]
if not any(i_dot22):
return False # no dot22
if i_dot22.count(True) > 1:
Expand All @@ -813,14 +813,16 @@ def local_dot22_to_dot22scalar(fgraph, node):
dot22_idx = i_dot22.index(True)
d = node.inputs[dot22_idx]
i_scalar = [_as_scalar(x, dtype=d.dtype) for x in node.inputs]
if not any(i_scalar):
if all(i is None for i in i_scalar):
# Check if we can reorder the graph as this mul have a mul in inputs.
# We support only 1 additional level of mul.
# The canonizer should have merged those mul together.
i_mul = [
x.owner
and x.owner.op == mul
and any(_as_scalar(x_i, dtype=d.dtype) for x_i in x.owner.inputs)
and any(
_as_scalar(x_i, dtype=d.dtype) is not None for x_i in x.owner.inputs
)
for x in node.inputs
]
if not any(i_mul):
Expand All @@ -834,7 +836,7 @@ def local_dot22_to_dot22scalar(fgraph, node):

scalar_idx = -1
for i, x in enumerate(m.owner.inputs):
if _as_scalar(x, dtype=d.dtype) and (
if _as_scalar(x, dtype=d.dtype) is not None and (
pytensor.scalar.upcast(x.type.dtype, d.type.dtype) == d.type.dtype
):
scalar_idx = i
Expand Down
10 changes: 5 additions & 5 deletions pytensor/tensor/rewriting/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -1331,14 +1331,14 @@ def local_sum_prod_of_mul_or_div(fgraph, node):

# If we have a `Prod`, then the outside terms need to be raised to the power of the number of elements
# that were contracted in the input
if isinstance(node.op, Prod) and inner_term:
if isinstance(node.op, Prod) and inner_term is not None:
dtype = inner_term.dtype
n_reduced_elements = prod(
[inner_term.shape[i].astype(dtype) for i in reduced_axes]
)
outer_term = outer_term**n_reduced_elements

if not inner_term:
if inner_term is None:
# Sum/Prod is useless, just return the outer_term
# (This can only happen for mul, not division)
new_out = outer_term
Expand Down Expand Up @@ -1992,7 +1992,7 @@ def local_pow_canonicalize(fgraph, node):
# x ** 1 = x
new_out = broadcast_arrays(*node.inputs)[0]

if not new_out:
if new_out is None:
return

if new_out.dtype != node.out.dtype:
Expand Down Expand Up @@ -2119,7 +2119,7 @@ def local_pow_to_nested_squaring(fgraph, node):
rval1_scal = None
while y_to_do > 0:
log_to_do = int(np.log2(y_to_do))
if rval1:
if rval1 is not None:
rval1 *= pow2[log_to_do]
rval1_scal *= pow2_scal[log_to_do]
else:
Expand All @@ -2137,7 +2137,7 @@ def local_pow_to_nested_squaring(fgraph, node):
rval = [reciprocal(rval1)]
else:
rval = [rval1]
if rval:
if rval is not None:
rval[0] = cast(rval[0], odtype)
return rval

Expand Down
2 changes: 1 addition & 1 deletion pytensor/tensor/rewriting/special.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def softmax_simplifier(numerators, denominators):
matching_denom = denominator
break

if matching_denom:
if matching_denom is not None:
softmax = Softmax(axis=sum_axis)(numerator.owner.inputs[0])
copy_stack_trace(numerator, softmax)
numerators.remove(numerator)
Expand Down
2 changes: 1 addition & 1 deletion pytensor/tensor/rewriting/subtensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ def merge_two_slices(fgraph, slice1, len1, slice2, len2):
val = switch(le(len2, 0), len1 + 1, val)
val = switch(ge(sl2, len2), len1 + 1, val)
val = switch(lt(sl2, 0), -len1 - 1, val)
if sl1.step:
if sl1.step is not None:
val = switch(eq(sl1.step, 0), len1 + 1, val)
return val
else:
Expand Down
Loading