Repository: PennyLaneAI/pennylane
Pull request #1267 (2021-05-02 21:39:05)

NumPy random generators
Currently, the recommended way of creating random numbers with NumPy involves creating a local generator object, and then querying that object for random numbers

```python
import numpy as np
rng = np.default_rng()
rng.random((3,2))
```

Currently, we cannot use this method with PennyLane wrapped NumPy.  This PR adds a wrapped generator object and  `default_rng` generator constructor to PennyLane's NumPy random.  It also import the bitstring generators into the random namespace.  Now we can call

```python
from pennylane import numpy as np
rng = np.default_rng()
rng.random((3,2))
```

and get  an array with a `requires_grad` attribute,

josh146: @albi3ro, I'll attach the `WIP` label until tests are added 🙂 
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1267](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (ba4e6b9) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/fe5d1dffa152fd34e4f9224df02eecb219affa06?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (fe5d1df) will **decrease** coverage by `0.08%`.
> The diff coverage is `47.36%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #1267      +/-   ##
==========================================
- Coverage   98.11%   98.03%   -0.09%     
==========================================
  Files         146      146              
  Lines       11056    11074      +18     
==========================================
+ Hits        10848    10856       +8     
- Misses        208      218      +10     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/numpy/random.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL251bXB5L3JhbmRvbS5weQ==) | `54.54% <47.36%> (-45.46%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [fe5d1df...ba4e6b9](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1267?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index e90dabadc..4fc4862f7 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -90,6 +90,19 @@
   (0, 1)
   ```
 
+* PennyLane NumPy now includes the
+  [random module's](https://numpy.org/doc/stable/reference/random/index.html#module-numpy.random)
+  `Generator` objects, the recommended way of random number generation. This allows for
+  random number generation using a local, rather than global seed.
+
+```python
+from pennylane import numpy as np
+
+rng = np.random.default_rng()
+random_mat1 = rng.random((3,2))
+random_mat2 = rng.standard_normal(3, requires_grad=False)
+```
+
 <h3>Improvements</h3>
 
 * Added custom gate application for Toffoli in `default.qubit`.
@@ -157,7 +170,7 @@ Thomas Bromley, Olivia Di Matteo, Diego Guala, Anthony Hayes, Josh Izaac, Antal
 This release contains contributions from (in alphabetical order):
 
 Thomas Bromley, Olivia Di Matteo, Diego Guala, Anthony Hayes, Ryan Hill,
-Josh Izaac, Maria Schuld, Antal Száva.
+Josh Izaac, Christina Lee, Maria Schuld, Antal Száva.
 
 # Release 0.15.0
 
diff --git a/pennylane/numpy/random.py b/pennylane/numpy/random.py
index aaa687c34..038553fd0 100644
--- a/pennylane/numpy/random.py
+++ b/pennylane/numpy/random.py
@@ -15,7 +15,45 @@
 This package provides a wrapped version of autograd.numpy.random, such that
 it works with the PennyLane :class:`~.tensor` class.
 """
+import semantic_version
+
 from autograd.numpy import random as _random
-from .wrapper import wrap_arrays
+from numpy import __version__ as np_version
+from numpy.random import MT19937, PCG64, Philox, SFC64  # pylint: disable=unused-import
+
+from .wrapper import wrap_arrays, tensor_wrapper
 
 wrap_arrays(_random.__dict__, globals())
+
+
+np_version_spec = semantic_version.Spec(">=0.17.0")
+if np_version_spec.match(semantic_version.Version(np_version)):
+    # pylint: disable=too-few-public-methods
+    # pylint: disable=missing-class-docstring
+    class Generator(_random.Generator):
+        def __init__(self, *args, **kwargs):
+            super().__init__(*args, **kwargs)
+
+            self.__doc__ = "PennyLane wrapped NumPy Generator object\n" + super().__doc__
+
+            for name in dir(_random.Generator):
+                if name[0] != "_":
+                    self.__dict__[name] = tensor_wrapper(getattr(super(), name))
+
+    # pylint: disable=missing-function-docstring
+    def default_rng(seed=None):
+        # Mostly copied from NumPy, but uses our Generator instead
+
+        if hasattr(seed, "capsule"):  # I changed this line
+            # We were passed a BitGenerator, so just wrap it up.
+            return Generator(seed)
+        if isinstance(seed, Generator):
+            # Pass through a Generator.
+            return seed
+        # Otherwise we need to instantiate a new BitGenerator and Generator as
+        # normal.
+        return Generator(PCG64(seed))
+
+    default_rng.__doc__ = (
+        "PennyLane duplicated generator constructor\n" + _random.default_rng.__doc__
+    )


Test patch:
diff --git a/tests/numpy/test_numpy_random.py b/tests/numpy/test_numpy_random.py
new file mode 100644
index 000000000..2851ad528
--- /dev/null
+++ b/tests/numpy/test_numpy_random.py
@@ -0,0 +1,117 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Tests for ``pennylane.numpy.random`` wrapping.  Arrays generated should have an 
+additional property, ``requires_grad``, that marks them as trainable/ non-trainable.
+"""
+
+import pytest
+
+from pennylane import numpy as np
+from pennylane.numpy import random
+
+# distributions that require no extra parameters
+distributions_no_extra_input = [
+    "exponential",
+    "gumbel",
+    "laplace",
+    "logistic",
+    "lognormal",
+    "normal",
+    "poisson",
+    "rayleigh",
+    "standard_cauchy",
+    "standard_exponential",
+    "standard_normal",
+    "uniform",
+]
+
+bit_generator_classes = [random.MT19937, random.PCG64, random.Philox, random.SFC64]
+
+general_gen = random.default_rng()
+
+
+class TestGeneratorDistributions:
+    @pytest.mark.parametrize("distribution", distributions_no_extra_input)
+    def test_generator_distributions(self, distribution):
+        """Tests the distributions from a generator object that don't require
+        additional distribution-specific parameters to make sure they have a
+        ``requires_grad`` attribute."""
+        size = (3,)
+        output = getattr(general_gen, distribution)(size=size)
+
+        assert isinstance(output, np.tensor)
+
+        assert output.shape == size
+        assert output.requires_grad == True
+
+        output.requires_grad = False
+        assert output.requires_grad == False
+
+
+class Test_default_rng:
+    def test_no_input(self):
+        """Tests that np.random.default_rng() returns a generator object when
+        no input is passed."""
+        rng = random.default_rng()
+
+        assert isinstance(rng, random.Generator)
+
+        output = rng.random((3,))
+        assert isinstance(output, np.tensor)
+
+    @pytest.mark.parametrize("bitgen_cls", bit_generator_classes)
+    def test_bit_generators(self, bitgen_cls):
+        """Tests that np.random.default_rng() returns a generator when passed a bit
+        generator.  Also checks that the bit generators are imported into the namespace."""
+
+        bitgen = bitgen_cls()
+
+        rng = random.default_rng(bitgen)
+
+        assert isinstance(rng, random.Generator)
+        assert isinstance(rng.bit_generator, bitgen_cls)
+
+        output = rng.random((3,))
+        assert isinstance(output, np.tensor)
+
+    def test_generator_input(self):
+        """Tests that ``np.random.default_rng`` passes through a Generator when its passed as input."""
+
+        rng1 = random.default_rng()
+        rng2 = random.default_rng(rng1)
+
+        assert rng1 == rng2
+
+    def test_seed_reproducible(self):
+        """Tests that setting a seed to ``default_rng`` gives reproducible results."""
+
+        seed = 42
+        size = (3, 2)
+
+        rng1 = random.default_rng(seed)
+        rng2 = random.default_rng(seed)
+
+        assert isinstance(rng1, random.Generator)
+        assert isinstance(rng2, random.Generator)
+
+        mat1 = rng1.random(size=size)
+        mat2 = rng2.random(size=size)
+
+        assert np.all(mat1 == mat2)
+
+        mat1_2 = rng1.normal(size=size)
+        mat2_2 = rng2.normal(size=size)
+
+        assert np.all(mat1_2 == mat2_2)