Skip to content

Commit

Permalink
Merge pull request #2 from sbalian/numpy-support
Browse files Browse the repository at this point in the history
Add numpy support
  • Loading branch information
sbalian committed Apr 3, 2022
2 parents 4aba1b7 + 2db9c07 commit f0d4866
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 42 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
rm get-poetry.py
source $HOME/.poetry/env
poetry install
poetry install -E numpy
- name: Build
run: |
source $HOME/.poetry/env
Expand All @@ -37,7 +38,7 @@ jobs:
poetry run isort . --check-only
poetry run black --check .
poetry run flake8 .
poetry run mypy qrandom.py
poetry run mypy qrandom
- name: Test
run: |
source $HOME/.poetry/env
Expand Down
51 changes: 40 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ ANU API that serves real quantum random numbers.
pip install quantum-random
```

Optionally, for [NumPy][numpy] support,

```bash
pip install quantum-random[numpy]
```

Note that the NumPy integration is not well-tested and is not available
for Python 3.10.

## Usage

Just import `qrandom` and use it like you'd use the
Expand All @@ -31,22 +40,25 @@ Just import `qrandom` and use it like you'd use the
-0.8370871276247828
```

Alternatively, you can `import QuantumRandom from qrandom` and use the class
directly (just like `random.Random`).

Under the hood, batches of quantum numbers are fetched from the API as needed
and each batch contains 1024 numbers. If you wish to pre-fetch more, use
`qrandom.fill(n)`, where `n` is the number of batches.

## Notes on implementation
Optionally, if you have installed the NumPy integration,

The `qrandom` module exposes a class derived from `random.Random` with a
`random()` method that outputs quantum floats in the range [0, 1)
(converted from 64-bit ints). Overriding `random.Random.random`
is sufficient to make the `qrandom` module behave mostly like the
`random` module as described in the [Python docs][pyrandom]. The exceptions
at the moment are `getrandbits()` and `randbytes()` that are not available in
`qrandom`. Because `getrandbits()` is not available, `randrange()` cannot
produce arbitrarily long sequences. Finally, the user is warned when `seed()`
is called because there is no state. For the same reason, `getstate()` and
`setstate()` are not implemented.
```python
>>> from qrandom.numpy import quantum_rng

>>> qrng = quantum_rng()

>>> qrng.random((3, 3)) # use like numpy.random.default_rng()
array([[0.37220278, 0.24337193, 0.67534826],
[0.209068 , 0.25108681, 0.49201691],
[0.35894084, 0.72219929, 0.55388594]])
```

## Tests

Expand All @@ -61,6 +73,21 @@ poetry run tox

See [here](./docs/uniform.md) for a visualisation and a Kolmogorov–Smirnov test.

## Notes on implementation

The `qrandom` module exposes a class derived from `random.Random` with a
`random()` method that outputs quantum floats in the range [0, 1)
(converted from 64-bit ints). Overriding `random.Random.random`
is sufficient to make the `qrandom` module behave mostly like the
`random` module as described in the [Python docs][pyrandom]. The exceptions
at the moment are `getrandbits()` and `randbytes()` that are not available in
`qrandom`. Because `getrandbits()` is not available, `randrange()` cannot
produce arbitrarily long sequences. Finally, the user is warned when `seed()`
is called because there is no state. For the same reason, `getstate()` and
`setstate()` are not implemented.

NumPy support is provided using [RandomGen][randomgen].

## License

See [LICENCE](./LICENSE).
Expand All @@ -69,3 +96,5 @@ See [LICENCE](./LICENSE).
[pyrandom]: https://docs.python.org/3.9/library/random.html
[poetry]: https://python-poetry.org
[pyenv]: https://github.com/pyenv/pyenv
[numpy]: https://numpy.org
[randomgen]: https://github.com/bashtage/randomgen
51 changes: 44 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
[tool.poetry]
name = "quantum-random"
version = "1.0.1"
version = "1.1.1"
description = "Quantum random numbers"
authors = ["Seto Balian <[email protected]>"]
packages = [{include = "qrandom.py"}]
packages = [{ include = "qrandom" }]
readme = "README.md"
license = "MIT"
repository = "https://github.com/sbalian/quantum-random"
maintainers = ["Seto Balian <[email protected]>"]
classifiers = [
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Topic :: Software Development",
"Topic :: Scientific/Engineering",
"Typing :: Typed",
"Operating System :: Microsoft :: Windows",
"Operating System :: Unix",
"Operating System :: MacOS",
"License :: OSI Approved :: MIT License",
]

[tool.poetry.dependencies]
python = ">=3.7,<3.11"
requests = "^2.25.1"

numpy = { version = "^1.17", optional = true, python = ">=3.7,<3.10" }
randomgen = { version = "^1.21.2", optional = true, python = ">=3.7,<3.10" }

[tool.poetry.dev-dependencies]
isort = "^5.7.0"
flake8 = "^3.8.4"
Expand All @@ -25,6 +41,9 @@ black = "^22.3.0"
matplotlib = "^3.5.1"
scipy = "<1.8"

[tool.poetry.extras]
numpy = ["numpy", "randomgen"]

[tool.isort]
line_length = 79
known_first_party = "qrandom"
Expand Down
39 changes: 23 additions & 16 deletions qrandom.py → qrandom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import requests

__version__ = "1.1.1"

__all__ = [
"betavariate",
"choice",
Expand All @@ -45,21 +47,24 @@
"fill",
]

_ANU_PARAMS: Dict[str, Union[int, str]] = {
"length": 1024,
"type": "hex16",
"size": 8,
}
_ANU_URL: str = "https://qrng.anu.edu.au/API/jsonI.php"
_ANU_URL = "https://qrng.anu.edu.au/API/jsonI.php"


def _get_qrand_int64() -> List[int]:
def _get_qrand_int64(size: int = 1024) -> List[int]:
"""Gets quantum random int64s from the ANU API.
Raises RuntimeError if the ANU API call is not successful.
size is the number of int64s fetched (1024 by default).
Raises RuntimeError if the ANU API call is not successful. This includes
the case of size > 1024.
"""
response = requests.get(_ANU_URL, _ANU_PARAMS)
params: Dict[str, Union[int, str]] = {
"length": size,
"type": "hex16",
"size": 8,
}
response = requests.get(_ANU_URL, params)
response.raise_for_status()
r_json = response.json()

Expand All @@ -73,11 +78,11 @@ def _get_qrand_int64() -> List[int]:
return


class _QuantumRandom(pyrandom.Random):
class QuantumRandom(pyrandom.Random):
"""Quantum random number generator."""

def __init__(self):
"""Initializes an instance of _QuantumRandom."""
"""Initializes an instance of QuantumRandom."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
super().__init__()
Expand All @@ -90,12 +95,14 @@ def fill(self, n: int = 1):
self._rand_int64.extend(_get_qrand_int64())
return

def random(self) -> float:
"""Gets the next quantum random number in the range [0.0, 1.0)."""
def _get_rand_int64(self) -> int:
if not self._rand_int64:
self.fill()
rand_int64 = self._rand_int64.pop()
return rand_int64 / (2**64)
return self._rand_int64.pop()

def random(self) -> float:
"""Gets the next quantum random number in the range [0.0, 1.0)."""
return self._get_rand_int64() / (2**64)

def seed(self, *args, **kwds) -> None:
"""Method is ignored. There is no seed for the quantum vacuum.
Expand All @@ -116,7 +123,7 @@ def _notimplemented(self, *args, **kwds) -> NoReturn:
getstate = setstate = _notimplemented


_inst = _QuantumRandom()
_inst = QuantumRandom()
betavariate = _inst.betavariate
choice = _inst.choice
choices = _inst.choices
Expand Down
20 changes: 20 additions & 0 deletions qrandom/numpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Adds numpy support to qrandom."""

import numpy.random
import randomgen # type: ignore

from . import QuantumRandom


class ANUQRNG(QuantumRandom):
def __init__(self):
super().__init__()

def random_raw(self, voidp) -> int:
return self._get_rand_int64()


def quantum_rng():
"""Constructs a new Generator with a quantum BitGenerator (ANUQRNG)."""
qrn = ANUQRNG()
return numpy.random.Generator(randomgen.UserBitGenerator(qrn.random_raw))
8 changes: 7 additions & 1 deletion tests/get_responses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python

import json
from typing import Dict, Union

import requests

Expand All @@ -12,7 +13,12 @@ def main():
num_hits = 10
for hit in range(num_hits):
print(f"Getting {hit+1} of {num_hits} ...")
response = requests.get(qrandom._ANU_URL, qrandom._ANU_PARAMS)
params: Dict[str, Union[int, str]] = {
"length": 1024,
"type": "hex16",
"size": 8,
}
response = requests.get(qrandom._ANU_URL, params)
response.raise_for_status()
json_r = response.json()
json_responses.append(json_r)
Expand Down
Loading

0 comments on commit f0d4866

Please sign in to comment.