Repository: scikit-hep/pyhf

Issue:
Uniform default precision for all backends?
# Question

As seen [in the example](https://pyhf.readthedocs.io/en/v0.6.1/_generated/pyhf.set_backend.html) for `pyhf.set_backend` the default precision for the `numpy` backend is `64b` while the default precision for TensorFlow and PyTorch is `32b`. 

https://github.com/scikit-hep/pyhf/blob/98bb222d9c45ad13335ab5423d866e3a1508c329/src/pyhf/tensor/numpy_backend.py#L44

https://github.com/scikit-hep/pyhf/blob/98bb222d9c45ad13335ab5423d866e3a1508c329/src/pyhf/tensor/jax_backend.py#L58

https://github.com/scikit-hep/pyhf/blob/98bb222d9c45ad13335ab5423d866e3a1508c329/src/pyhf/tensor/tensorflow_backend.py#L16

https://github.com/scikit-hep/pyhf/blob/98bb222d9c45ad13335ab5423d866e3a1508c329/src/pyhf/tensor/pytorch_backend.py#L18

Given that there are known differences between `64b` and `32b` it is probably worth reconsidering / discussing if we should have a default precision of `64b` for all backends. My guess is that it is non-intuitive that switching from NumPy to Torch changes the precision.

This is somewhat related to Issue #981 and was put back into my mind by @dguest and @nhartman94 (c.f. Discussion #1397) (comments welcome from them both).


Discussion:

Resolving patch:
diff --git a/src/pyhf/__init__.py b/src/pyhf/__init__.py
--- a/src/pyhf/__init__.py
+++ b/src/pyhf/__init__.py
@@ -45,12 +45,12 @@ def set_backend(backend, custom_optimizer=None, precision=None):
         >>> pyhf.tensorlib.name
         'tensorflow'
         >>> pyhf.tensorlib.precision
-        '32b'
-        >>> pyhf.set_backend(b"pytorch", precision="64b")
+        '64b'
+        >>> pyhf.set_backend(b"pytorch", precision="32b")
         >>> pyhf.tensorlib.name
         'pytorch'
         >>> pyhf.tensorlib.precision
-        '64b'
+        '32b'
         >>> pyhf.set_backend(pyhf.tensor.numpy_backend())
         >>> pyhf.tensorlib.name
         'numpy'
diff --git a/src/pyhf/tensor/pytorch_backend.py b/src/pyhf/tensor/pytorch_backend.py
--- a/src/pyhf/tensor/pytorch_backend.py
+++ b/src/pyhf/tensor/pytorch_backend.py
@@ -15,7 +15,7 @@ class pytorch_backend:
 
     def __init__(self, **kwargs):
         self.name = 'pytorch'
-        self.precision = kwargs.get('precision', '32b')
+        self.precision = kwargs.get('precision', '64b')
         self.dtypemap = {
             'float': torch.float64 if self.precision == '64b' else torch.float32,
             'int': torch.int64 if self.precision == '64b' else torch.int32,
@@ -525,7 +525,7 @@ def to_numpy(self, tensor_in):
             >>> numpy_ndarray = pyhf.tensorlib.to_numpy(tensor)
             >>> numpy_ndarray
             array([[1., 2., 3.],
-                   [4., 5., 6.]], dtype=float32)
+                   [4., 5., 6.]])
             >>> type(numpy_ndarray)
             <class 'numpy.ndarray'>
 
diff --git a/src/pyhf/tensor/tensorflow_backend.py b/src/pyhf/tensor/tensorflow_backend.py
--- a/src/pyhf/tensor/tensorflow_backend.py
+++ b/src/pyhf/tensor/tensorflow_backend.py
@@ -13,7 +13,7 @@ class tensorflow_backend:
 
     def __init__(self, **kwargs):
         self.name = 'tensorflow'
-        self.precision = kwargs.get('precision', '32b')
+        self.precision = kwargs.get('precision', '64b')
         self.dtypemap = {
             'float': tf.float64 if self.precision == '64b' else tf.float32,
             'int': tf.int64 if self.precision == '64b' else tf.int32,
@@ -36,7 +36,7 @@ def clip(self, tensor_in, min_value, max_value):
             >>> a = pyhf.tensorlib.astensor([-2, -1, 0, 1, 2])
             >>> t = pyhf.tensorlib.clip(a, -1, 1)
             >>> print(t)
-            tf.Tensor([-1. -1.  0.  1.  1.], shape=(5,), dtype=float32)
+            tf.Tensor([-1. -1.  0.  1.  1.], shape=(5,), dtype=float64)
 
         Args:
             tensor_in (:obj:`tensor`): The input tensor object
@@ -64,7 +64,7 @@ def erf(self, tensor_in):
             >>> a = pyhf.tensorlib.astensor([-2., -1., 0., 1., 2.])
             >>> t = pyhf.tensorlib.erf(a)
             >>> print(t)
-            tf.Tensor([-0.9953223 -0.8427007  0.         0.8427007  0.9953223], shape=(5,), dtype=float32)
+            tf.Tensor([-0.99532227 -0.84270079  0.          0.84270079  0.99532227], shape=(5,), dtype=float64)
 
         Args:
             tensor_in (:obj:`tensor`): The input tensor object
@@ -85,7 +85,7 @@ def erfinv(self, tensor_in):
             >>> a = pyhf.tensorlib.astensor([-2., -1., 0., 1., 2.])
             >>> t = pyhf.tensorlib.erfinv(pyhf.tensorlib.erf(a))
             >>> print(t)
-            tf.Tensor([-2.000001   -0.99999964  0.          0.99999964  1.9999981 ], shape=(5,), dtype=float32)
+            tf.Tensor([-2. -1.  0.  1.  2.], shape=(5,), dtype=float64)
 
         Args:
             tensor_in (:obj:`tensor`): The input tensor object
@@ -107,7 +107,7 @@ def tile(self, tensor_in, repeats):
             >>> print(t)
             tf.Tensor(
             [[1. 1.]
-             [2. 2.]], shape=(2, 2), dtype=float32)
+             [2. 2.]], shape=(2, 2), dtype=float64)
 
         Args:
             tensor_in (:obj:`tensor`): The tensor to be repeated
@@ -138,7 +138,7 @@ def conditional(self, predicate, true_callable, false_callable):
             >>> b = tensorlib.astensor([5])
             >>> t = tensorlib.conditional((a < b)[0], lambda: a + b, lambda: a - b)
             >>> print(t)
-            tf.Tensor([9.], shape=(1,), dtype=float32)
+            tf.Tensor([9.], shape=(1,), dtype=float64)
 
         Args:
             predicate (:obj:`scalar`): The logical condition that determines which callable to evaluate
@@ -188,9 +188,9 @@ def astensor(self, tensor_in, dtype='float'):
             >>> 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=
+            <tf.Tensor: shape=(2, 3), dtype=float64, numpy=
             array([[1., 2., 3.],
-                   [4., 5., 6.]], dtype=float32)>
+                   [4., 5., 6.]])>
             >>> type(tensor)
             <class 'tensorflow.python.framework.ops.EagerTensor'>
 
@@ -284,7 +284,7 @@ def ravel(self, tensor):
             >>> tensor = pyhf.tensorlib.astensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
             >>> t_ravel = pyhf.tensorlib.ravel(tensor)
             >>> print(t_ravel)
-            tf.Tensor([1. 2. 3. 4. 5. 6.], shape=(6,), dtype=float32)
+            tf.Tensor([1. 2. 3. 4. 5. 6.], shape=(6,), dtype=float64)
 
         Args:
             tensor (Tensor): Tensor object
@@ -320,7 +320,7 @@ def where(self, mask, tensor_in_1, tensor_in_2):
             ...     pyhf.tensorlib.astensor([2, 2, 2]),
             ... )
             >>> print(t)
-            tf.Tensor([1. 2. 1.], shape=(3,), dtype=float32)
+            tf.Tensor([1. 2. 1.], shape=(3,), dtype=float64)
 
         Args:
             mask (bool): Boolean mask (boolean or tensor object of booleans)
@@ -359,9 +359,9 @@ def simple_broadcast(self, *args):
             ...   pyhf.tensorlib.astensor([2, 3, 4]),
             ...   pyhf.tensorlib.astensor([5, 6, 7]))
             >>> print([str(t) for t in b]) # doctest: +NORMALIZE_WHITESPACE
-            ['tf.Tensor([1. 1. 1.], shape=(3,), dtype=float32)',
-             'tf.Tensor([2. 3. 4.], shape=(3,), dtype=float32)',
-             'tf.Tensor([5. 6. 7.], shape=(3,), dtype=float32)']
+            ['tf.Tensor([1. 1. 1.], shape=(3,), dtype=float64)',
+             'tf.Tensor([2. 3. 4.], shape=(3,), dtype=float64)',
+             'tf.Tensor([5. 6. 7.], shape=(3,), dtype=float64)']
 
         Args:
             args (Array of Tensors): Sequence of arrays
@@ -408,13 +408,13 @@ def poisson_logpdf(self, n, lam):
             >>> import pyhf
             >>> pyhf.set_backend("tensorflow")
             >>> t = pyhf.tensorlib.poisson_logpdf(5., 6.)
-            >>> print(t)
-            tf.Tensor(-1.8286943, shape=(), dtype=float32)
+            >>> print(t) # doctest:+ELLIPSIS
+            tf.Tensor(-1.8286943966417..., shape=(), dtype=float64)
             >>> values = pyhf.tensorlib.astensor([5., 9.])
             >>> rates = pyhf.tensorlib.astensor([6., 8.])
             >>> t = pyhf.tensorlib.poisson_logpdf(values, rates)
             >>> print(t)
-            tf.Tensor([-1.8286943 -2.086854 ], shape=(2,), dtype=float32)
+            tf.Tensor([-1.8286944 -2.0868536], shape=(2,), dtype=float64)
 
         Args:
             n (:obj:`tensor` or :obj:`float`): The value at which to evaluate the approximation to the Poisson distribution p.m.f.
@@ -439,13 +439,13 @@ def poisson(self, n, lam):
             >>> import pyhf
             >>> pyhf.set_backend("tensorflow")
             >>> t = pyhf.tensorlib.poisson(5., 6.)
-            >>> print(t)
-            tf.Tensor(0.16062315, shape=(), dtype=float32)
+            >>> print(t) # doctest:+ELLIPSIS
+            tf.Tensor(0.1606231410479..., shape=(), dtype=float64)
             >>> values = pyhf.tensorlib.astensor([5., 9.])
             >>> rates = pyhf.tensorlib.astensor([6., 8.])
             >>> t = pyhf.tensorlib.poisson(values, rates)
             >>> print(t)
-            tf.Tensor([0.16062315 0.12407687], shape=(2,), dtype=float32)
+            tf.Tensor([0.16062314 0.12407692], shape=(2,), dtype=float64)
 
         Args:
             n (:obj:`tensor` or :obj:`float`): The value at which to evaluate the approximation to the Poisson distribution p.m.f.
@@ -471,13 +471,13 @@ def normal_logpdf(self, x, mu, sigma):
             >>> pyhf.set_backend("tensorflow")
             >>> t = pyhf.tensorlib.normal_logpdf(0.5, 0., 1.)
             >>> print(t)
-            tf.Tensor(-1.0439385, shape=(), dtype=float32)
+            tf.Tensor(-1.0439385332046727, shape=(), dtype=float64)
             >>> values = pyhf.tensorlib.astensor([0.5, 2.0])
             >>> means = pyhf.tensorlib.astensor([0., 2.3])
             >>> sigmas = pyhf.tensorlib.astensor([1., 0.8])
             >>> t = pyhf.tensorlib.normal_logpdf(values, means, sigmas)
             >>> print(t)
-            tf.Tensor([-1.0439385 -0.7661075], shape=(2,), dtype=float32)
+            tf.Tensor([-1.04393853 -0.76610747], shape=(2,), dtype=float64)
 
         Args:
             x (:obj:`tensor` or :obj:`float`): The value at which to evaluate the Normal distribution p.d.f.
@@ -503,13 +503,13 @@ def normal(self, x, mu, sigma):
             >>> pyhf.set_backend("tensorflow")
             >>> t = pyhf.tensorlib.normal(0.5, 0., 1.)
             >>> print(t)
-            tf.Tensor(0.35206532, shape=(), dtype=float32)
+            tf.Tensor(0.3520653267642995, shape=(), dtype=float64)
             >>> values = pyhf.tensorlib.astensor([0.5, 2.0])
             >>> means = pyhf.tensorlib.astensor([0., 2.3])
             >>> sigmas = pyhf.tensorlib.astensor([1., 0.8])
             >>> t = pyhf.tensorlib.normal(values, means, sigmas)
             >>> print(t)
-            tf.Tensor([0.35206532 0.46481887], shape=(2,), dtype=float32)
+            tf.Tensor([0.35206533 0.46481887], shape=(2,), dtype=float64)
 
         Args:
             x (:obj:`tensor` or :obj:`float`): The value at which to evaluate the Normal distribution p.d.f.
@@ -533,11 +533,11 @@ def normal_cdf(self, x, mu=0.0, sigma=1):
             >>> pyhf.set_backend("tensorflow")
             >>> t = pyhf.tensorlib.normal_cdf(0.8)
             >>> print(t)
-            tf.Tensor(0.7881446, shape=(), dtype=float32)
+            tf.Tensor(0.7881446014166034, shape=(), dtype=float64)
             >>> values = pyhf.tensorlib.astensor([0.8, 2.0])
             >>> t = pyhf.tensorlib.normal_cdf(values)
             >>> print(t)
-            tf.Tensor([0.7881446  0.97724986], shape=(2,), dtype=float32)
+            tf.Tensor([0.7881446  0.97724987], shape=(2,), dtype=float64)
 
         Args:
             x (:obj:`tensor` or :obj:`float`): The observed value of the random variable to evaluate the CDF for
@@ -564,7 +564,7 @@ def poisson_dist(self, rate):
             >>> poissons = pyhf.tensorlib.poisson_dist(rates)
             >>> t = poissons.log_prob(values)
             >>> print(t)
-            tf.Tensor([-1.7403021 -2.086854 ], shape=(2,), dtype=float32)
+            tf.Tensor([-1.74030218 -2.0868536 ], shape=(2,), dtype=float64)
 
         Args:
             rate (:obj:`tensor` or :obj:`float`): The mean of the Poisson distribution (the expected number of events)
@@ -590,7 +590,7 @@ def normal_dist(self, mu, sigma):
             >>> normals = pyhf.tensorlib.normal_dist(means, stds)
             >>> t = normals.log_prob(values)
             >>> print(t)
-            tf.Tensor([-1.4189385 -2.2257915], shape=(2,), dtype=float32)
+            tf.Tensor([-1.41893853 -2.22579135], shape=(2,), dtype=float64)
 
         Args:
             mu (:obj:`tensor` or :obj:`float`): The mean of the Normal distribution
@@ -616,11 +616,11 @@ def to_numpy(self, tensor_in):
             >>> print(tensor)
             tf.Tensor(
             [[1. 2. 3.]
-             [4. 5. 6.]], shape=(2, 3), dtype=float32)
+             [4. 5. 6.]], shape=(2, 3), dtype=float64)
             >>> numpy_ndarray = pyhf.tensorlib.to_numpy(tensor)
             >>> numpy_ndarray
             array([[1., 2., 3.],
-                   [4., 5., 6.]], dtype=float32)
+                   [4., 5., 6.]])
             >>> type(numpy_ndarray)
             <class 'numpy.ndarray'>
 


Test patch: