Skip to content

Commit

Permalink
revert: Allow 0-dimensional shape tensors (#972)
Browse files Browse the repository at this point in the history
* Allow for arbitrary shape tensors, including 0-dimensional ("empty") shape tensors (floats)
   - Reverts PR #413
* Enforce optimizers return objective function as a scalar to harmonize return type
* Revert tests that enforce minimum shape
* Add tests for return structure shape of pyhf.infer.mle.fit
* Add docstring examples for astensor
  • Loading branch information
matthewfeickert authored Jul 25, 2020
1 parent eadcd11 commit 0ff43e8
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 61 deletions.
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)
>>> -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]
)
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)

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]
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

0 comments on commit 0ff43e8

Please sign in to comment.