Skip to content
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

revert: Allow 0-dimensional shape tensors #972

Merged
merged 31 commits into from
Jul 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7b411e8
Revert non-empty tensor for NumPy backend
matthewfeickert Jul 17, 2020
1765d40
Revert non-empty tensor for JAX backend
matthewfeickert Jul 17, 2020
253ee7c
Revert non-empty tensor for PyTorch backend
matthewfeickert Jul 17, 2020
165c6e2
Revert non-empty tensor for TensorFlow backend
matthewfeickert Jul 17, 2020
a45f42b
Add docstring example for astensor
matthewfeickert Jul 17, 2020
62c923a
Ensure tensor of float has empty shape
matthewfeickert Jul 17, 2020
a604c16
THIS WILL FAIL BUT SAVE WORK
matthewfeickert Jul 17, 2020
be5f65f
Use tf.broadcast_to
matthewfeickert Jul 21, 2020
33ed1b7
Unsqueeze PyTorch 0-d tensors
matthewfeickert Jul 21, 2020
64ab36c
Have test_tensor understand floats
matthewfeickert Jul 21, 2020
b3d31d4
qmu is a 0-d tensor
matthewfeickert Jul 21, 2020
47088cd
Allow 0-d result tensor
matthewfeickert Jul 21, 2020
07d4ae9
Use torch.Tensor.view for faster op
matthewfeickert Jul 21, 2020
de66e09
Use Lukas's pattern as seems nicer
matthewfeickert Jul 21, 2020
dfe5ddf
q_mu should be a float
matthewfeickert Jul 21, 2020
d5ebe1f
Ensure float is returned
matthewfeickert Jul 21, 2020
a0f3fd0
Add atleast_1d method
matthewfeickert Jul 21, 2020
936a31e
Enforce scalar return for qmu
matthewfeickert Jul 21, 2020
b964d89
Use list comprehensions
matthewfeickert Jul 21, 2020
c3a03e2
Fix typo n docstrings
matthewfeickert Jul 21, 2020
77cd4d1
Add clarifyng note
matthewfeickert Jul 21, 2020
acd7117
Correct docs formatting
matthewfeickert Jul 21, 2020
267d08e
Return list only if multiple tensors given
matthewfeickert Jul 21, 2020
a1d5f72
Show that a single entry gives a tensor back
matthewfeickert Jul 21, 2020
062c150
Add stub of tests for test_atelast_1d
matthewfeickert Jul 21, 2020
16a0c85
Restore harmonization in backends
matthewfeickert Jul 21, 2020
de4ea16
Factor out adding atelast_1d test to another feat PR
matthewfeickert Jul 21, 2020
bad1cf1
Fix docstrings for mle
matthewfeickert Jul 22, 2020
290466e
Ensure returned objective function is a scalar
matthewfeickert Jul 23, 2020
70435f5
returned objective is still tensor, just 0-d so repr's as tensor
matthewfeickert Jul 23, 2020
385a6f8
Add mle.fit tests for shape of returns
matthewfeickert Jul 24, 2020
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 src/pyhf/infer/mle.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def fit(data, pdf, init_pars=None, par_bounds=None, **kwargs):
>>> bestfit_pars
array([0. , 1.0030512 , 0.96266961])
>>> twice_nll
array([24.98393521])
array(24.98393521)
kratsg marked this conversation as resolved.
Show resolved Hide resolved
>>> -2 * model.logpdf(bestfit_pars, data) == twice_nll
array([ True])

Expand Down Expand Up @@ -86,7 +86,7 @@ def fixed_poi_fit(poi_val, data, pdf, init_pars=None, par_bounds=None, **kwargs)
>>> bestfit_pars
array([1. , 0.97224597, 0.87553894])
>>> twice_nll
array([28.92218013])
array(28.92218013)
>>> -2 * model.logpdf(bestfit_pars, data) == twice_nll
array([ True])

Expand Down
2 changes: 1 addition & 1 deletion src/pyhf/infer/test_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ def qmu(mu, data, pdf, init_pars, par_bounds):
qmu = fixed_poi_fit_lhood_val - unconstrained_fit_lhood_val
qmu = tensorlib.where(
muhatbhat[pdf.config.poi_index] > mu, tensorlib.astensor(0.0), qmu
)[0]
)
matthewfeickert marked this conversation as resolved.
Show resolved Hide resolved
matthewfeickert marked this conversation as resolved.
Show resolved Hide resolved
return tensorlib.clip(qmu, 0, max_value=None)
2 changes: 1 addition & 1 deletion src/pyhf/optimize/opt_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ def wrap_objective(objective, data, pdf, stitch_pars, do_grad=False, jit_pieces=
def func(pars):
pars = tensorlib.astensor(pars)
constrained_pars = stitch_pars(pars)
return objective(constrained_pars, data, pdf)
return objective(constrained_pars, data, pdf)[0]

return func
4 changes: 2 additions & 2 deletions src/pyhf/optimize/opt_pytorch.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ def func(pars):
constrained_pars = stitch_pars(pars)
constr_nll = objective(constrained_pars, data, pdf)
grad = torch.autograd.grad(constr_nll, pars)[0]
return constr_nll.detach().numpy(), grad
return constr_nll.detach().numpy()[0], grad

else:

def func(pars):
pars = tensorlib.astensor(pars)
constrained_pars = stitch_pars(pars)
constr_nll = objective(constrained_pars, data, pdf)
return constr_nll
return constr_nll[0]

return func
4 changes: 2 additions & 2 deletions src/pyhf/optimize/opt_tflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ def func(pars):
# when tf.gather is used and this needs to be converted back to a
# tensor to be usable as a value
grad = tape.gradient(constr_nll, pars)
return constr_nll.numpy(), tf.convert_to_tensor(grad)
return constr_nll.numpy()[0], tf.convert_to_tensor(grad)

else:

def func(pars):
pars = tensorlib.astensor(pars)
constrained_pars = stitch_pars(pars)
return objective(constrained_pars, data, pdf)
return objective(constrained_pars, data, pdf)[0]

return func
20 changes: 13 additions & 7 deletions src/pyhf/tensor/jax_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ def astensor(self, tensor_in, dtype='float'):
"""
Convert to a JAX ndarray.

Example:

>>> import pyhf
>>> pyhf.set_backend("jax")
>>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
>>> tensor
DeviceArray([[1., 2., 3.],
[4., 5., 6.]], dtype=float64)
>>> type(tensor)
<class 'jax.interpreters.xla.DeviceArray'>

Args:
tensor_in (Number or Tensor): Tensor object

Expand All @@ -163,13 +174,8 @@ def astensor(self, tensor_in, dtype='float'):
except KeyError:
log.error('Invalid dtype: dtype must be float, int, or bool.')
raise
tensor = np.asarray(tensor_in, dtype=dtype)
# Ensure non-empty tensor shape for consistency
try:
tensor.shape[0]
except IndexError:
tensor = np.reshape(tensor, [1])
return np.asarray(tensor, dtype=dtype)

return np.asarray(tensor_in, dtype=dtype)

def sum(self, tensor_in, axis=None):
return np.sum(tensor_in, axis=axis)
Expand Down
19 changes: 12 additions & 7 deletions src/pyhf/tensor/numpy_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ def astensor(self, tensor_in, dtype='float'):
"""
Convert to a NumPy array.

Example:

>>> import pyhf
>>> pyhf.set_backend("numpy")
>>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
>>> tensor
array([[1., 2., 3.],
[4., 5., 6.]])
>>> type(tensor)
<class 'numpy.ndarray'>

Args:
tensor_in (Number or Tensor): Tensor object

Expand All @@ -157,13 +168,7 @@ def astensor(self, tensor_in, dtype='float'):
log.error('Invalid dtype: dtype must be float, int, or bool.')
raise

tensor = np.asarray(tensor_in, dtype=dtype)
# Ensure non-empty tensor shape for consistency
try:
tensor.shape[0]
except IndexError:
tensor = tensor.reshape(1)
return tensor
return np.asarray(tensor_in, dtype=dtype)

def sum(self, tensor_in, axis=None):
return np.sum(tensor_in, axis=axis)
Expand Down
20 changes: 13 additions & 7 deletions src/pyhf/tensor/pytorch_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ def astensor(self, tensor_in, dtype='float'):
"""
Convert to a PyTorch Tensor.

Example:

>>> import pyhf
>>> pyhf.set_backend("pytorch")
>>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
>>> tensor
tensor([[1., 2., 3.],
[4., 5., 6.]])
>>> type(tensor)
<class 'torch.Tensor'>

Args:
tensor_in (Number or Tensor): Tensor object

Expand All @@ -124,13 +135,7 @@ def astensor(self, tensor_in, dtype='float'):
log.error('Invalid dtype: dtype must be float, int, or bool.')
raise

tensor = torch.as_tensor(tensor_in, dtype=dtype)
# Ensure non-empty tensor shape for consistency
try:
tensor.shape[0]
except IndexError:
tensor = tensor.expand(1)
return tensor
return torch.as_tensor(tensor_in, dtype=dtype)
matthewfeickert marked this conversation as resolved.
Show resolved Hide resolved

def gather(self, tensor, indices):
return tensor[indices.type(torch.LongTensor)]
Expand Down Expand Up @@ -222,6 +227,7 @@ def simple_broadcast(self, *args):
list of Tensors: The sequence broadcast together.
"""

args = [arg.view(1) if not self.shape(arg) else arg for arg in args]
kratsg marked this conversation as resolved.
Show resolved Hide resolved
max_dim = max(map(len, args))
try:
assert not [arg for arg in args if 1 < len(arg) < max_dim]
Expand Down
33 changes: 17 additions & 16 deletions src/pyhf/tensor/tensorflow_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ def astensor(self, tensor_in, dtype='float'):
"""
Convert to a TensorFlow Tensor.

Example:

>>> import pyhf
>>> pyhf.set_backend("tensorflow")
>>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
>>> tensor
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[1., 2., 3.],
[4., 5., 6.]], dtype=float32)>
>>> type(tensor)
<class 'tensorflow.python.framework.ops.EagerTensor'>

Args:
tensor_in (Number or Tensor): Tensor object

Expand All @@ -156,11 +168,6 @@ def astensor(self, tensor_in, dtype='float'):
tensor.device
except AttributeError:
tensor = tf.convert_to_tensor(tensor_in)
# Ensure non-empty tensor shape for consistency
try:
tensor.shape[0]
except IndexError:
tensor = tf.reshape(tensor, [1])
if tensor.dtype is not dtype:
tensor = tf.cast(tensor, dtype)
return tensor
Expand Down Expand Up @@ -276,22 +283,16 @@ def simple_broadcast(self, *args):
list of Tensors: The sequence broadcast together.

"""
max_dim = max(map(lambda arg: arg.shape[0], args))

max_dim = max(map(tf.size, args))
try:
assert not [arg for arg in args if 1 < arg.shape[0] < max_dim]
assert not [arg for arg in args if 1 < tf.size(arg) < max_dim]
except AssertionError as error:
log.error(
'ERROR: The arguments must be of compatible size: 1 or %i', max_dim
)
raise error

broadcast = [
arg
if arg.shape[0] > 1
else tf.tile(tf.slice(arg, [0], [1]), tf.stack([max_dim]))
for arg in args
]
return broadcast
return [tf.broadcast_to(arg, (max_dim,)) for arg in args]

def einsum(self, subscripts, *operands):
"""
Expand Down Expand Up @@ -452,7 +453,7 @@ def normal_cdf(self, x, mu=0.0, sigma=1):
TensorFlow Tensor: The CDF
"""
normal = tfp.distributions.Normal(
self.astensor(mu, dtype='float')[0], self.astensor(sigma, dtype='float')[0],
self.astensor(mu, dtype='float'), self.astensor(sigma, dtype='float'),
)
return normal.cdf(x)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_backend_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def test_hypotest_q_mu(
q_mu = pyhf.infer.test_statistics.qmu(
1.0, data, pdf, pdf.config.suggested_init(), pdf.config.suggested_bounds(),
)
test_statistic.append(pyhf.tensorlib.tolist(q_mu))
test_statistic.append(q_mu)

# compare to NumPy/SciPy
test_statistic = np.array(test_statistic)
Expand Down
8 changes: 4 additions & 4 deletions tests/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,15 @@ def test_batched_constraints(backend):
)
)
assert np.isclose(
result[0],
result,
sum(
[
default_backend.poisson_logpdf(data, rate)
for data, rate in zip([12, 13, 14], [12, 13, 14])
]
),
)
assert result.shape == (1,)
assert result.shape == ()

suggested_pars = [1.1] * 3 + [0.0] * 5 # 2 pois 5 norm
constraint = poisson_constraint_combined(config)
Expand All @@ -208,15 +208,15 @@ def test_batched_constraints(backend):
)
)
assert np.isclose(
result[0],
result,
sum(
[
default_backend.poisson_logpdf(data, rate)
for data, rate in zip([12, 13, 14], [12 * 1.1, 13 * 1.1, 14 * 1.1])
]
),
)
assert result.shape == (1,)
assert result.shape == ()

constraint = poisson_constraint_combined(config, batch_size=10)
result = constraint.logpdf(
Expand Down
30 changes: 30 additions & 0 deletions tests/test_infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,36 @@ def check_uniform_type(in_list):
)


def test_mle_fit_default(tmpdir, hypotest_args):
"""
Check that the default return structure of pyhf.infer.mle.fit is as expected
"""
tb = pyhf.tensorlib

_, data, model = hypotest_args
kwargs = {}
result = pyhf.infer.mle.fit(data, model, **kwargs)
# bestfit_pars
assert isinstance(result, type(tb.astensor(result)))
assert pyhf.tensorlib.shape(result) == (model.config.npars,)


def test_mle_fit_return_fitted_val(tmpdir, hypotest_args):
"""
Check that the return structure of pyhf.infer.mle.fit with the
return_fitted_val keyword arg is as expected
"""
tb = pyhf.tensorlib

_, data, model = hypotest_args
kwargs = {"return_fitted_val": True}
result = pyhf.infer.mle.fit(data, model, **kwargs)
# bestfit_pars, twice_nll
assert pyhf.tensorlib.shape(result[0]) == (model.config.npars,)
assert isinstance(result[0], type(tb.astensor(result[0])))
assert pyhf.tensorlib.shape(result[1]) == ()


def test_hypotest_default(tmpdir, hypotest_args):
"""
Check that the default return structure of pyhf.infer.hypotest is as expected
Expand Down
2 changes: 1 addition & 1 deletion tests/test_optim.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def test_optim_with_value(backend, source, spec, mu):
return_fitted_val=True,
)
assert pyhf.tensorlib.tolist(result)
assert pyhf.tensorlib.shape(fitted_val) == (1,)
assert pyhf.tensorlib.shape(fitted_val) == ()


@pytest.mark.parametrize('mu', [1.0], ids=['mu=1'])
Expand Down
19 changes: 9 additions & 10 deletions tests/test_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ def test_simple_tensor_ops(backend):
assert tb.tolist(tb.abs(tb.astensor([-1, -2]))) == [1, 2]
a = tb.astensor(1)
b = tb.astensor(2)
assert tb.tolist(a < b)[0] is True
assert tb.tolist(b < a)[0] is False
assert tb.tolist(a < a)[0] is False
assert tb.tolist(a > b)[0] is False
assert tb.tolist(b > a)[0] is True
assert tb.tolist(a > a)[0] is False
assert tb.tolist(a < b) is True
assert tb.tolist(b < a) is False
assert tb.tolist(a < a) is False
assert tb.tolist(a > b) is False
assert tb.tolist(b > a) is True
assert tb.tolist(a > a) is False
a = tb.astensor(4)
b = tb.astensor(5)
assert tb.tolist(tb.conditional((a < b)[0], lambda: a + b, lambda: a - b)) == [9]
assert tb.tolist(tb.conditional((a > b)[0], lambda: a + b, lambda: a - b)) == [-1]
assert tb.tolist(tb.conditional((a < b), lambda: a + b, lambda: a - b)) == 9.0
assert tb.tolist(tb.conditional((a > b), lambda: a + b, lambda: a - b)) == -1.0


def test_complex_tensor_ops(backend):
Expand Down Expand Up @@ -145,10 +145,9 @@ def test_shape(backend):
tb = pyhf.tensorlib
assert tb.shape(tb.ones((1, 2, 3, 4, 5))) == (1, 2, 3, 4, 5)
assert tb.shape(tb.ones((0, 0))) == (0, 0)
assert tb.shape(tb.astensor(1.0)) == ()
assert tb.shape(tb.astensor([])) == (0,)
assert tb.shape(tb.astensor([1.0])) == (1,)
assert tb.shape(tb.astensor(1.0)) == tb.shape(tb.astensor([1.0]))
assert tb.shape(tb.astensor(0.0)) == tb.shape(tb.astensor([0.0]))
assert tb.shape(tb.astensor((1.0, 1.0))) == tb.shape(tb.astensor([1.0, 1.0]))
assert tb.shape(tb.astensor((0.0, 0.0))) == tb.shape(tb.astensor([0.0, 0.0]))
with pytest.raises(
Expand Down