Skip to content

Commit 08fbf47

Browse files
authored
feat(qsystem): add Random number generation module (#822)
Closes: #769 Depends on: - CQCL/tket2#786 - CQCL/tket2#790 - #810 Some changes from the issue: - Introduces `RNG` guppy type, instead of the `RNGContext` guppy struct. - We have `def __new__(seed: int) -> "RNG":` instead of `__init__(seed: int) -> "RNGContext"` - We have `maybe_rng` instead of `init_safe(seed: int) -> Option["RNGContext"]` Other thoughts: Unsure if this interface is the best we can do: ```py def test() -> tuple[int, nat, float, int]: rng = RNG(42) rint = rng.random_int() rnat = rng.random_nat() rfloat = rng.random_float() rint_bnd = rng.random_int_bounded(100) rng.discard() return rint, rnat, rfloat, rint_bnd ``` It might be nice to provide a context manager interface such as: ```py def test() -> tuple[int, nat, float, int]: with RNG(42) as rng: rint = rng.random_int() rnat = rng.random_nat() rfloat = rng.random_float() rint_bnd = rng.random_int_bounded(100) return rint, rnat, rfloat, rint_bnd ``` which would enforce the linear discipline on `RNG`. Mark confirms it's currently not possible within guppy. --- Release-As: 0.16.1
1 parent 32ded02 commit 08fbf47

File tree

9 files changed

+179
-10
lines changed

9 files changed

+179
-10
lines changed

.pre-commit-config.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.6.0
3+
rev: v5.0.0
44
hooks:
55
- id: check-added-large-files
66
- id: check-case-conflict
@@ -38,7 +38,7 @@ repos:
3838
- id: debug-statements
3939

4040
- repo: https://github.com/crate-ci/typos
41-
rev: v1.21.0
41+
rev: typos-dict-v0.12.6
4242
hooks:
4343
- id: typos
4444
args: []

.typos.toml

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ inot = "inot"
55
inout = "inout"
66
inouts = "inouts"
77
anc = "anc"
8+
setable = "setable"

guppylang/compiler/expr_compiler.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def _if_else(
174174
only_true_inputs: list[PlaceNode] | None = None,
175175
only_false_inputs: list[PlaceNode] | None = None,
176176
) -> tuple[AbstractContextManager[None], AbstractContextManager[None]]:
177-
"""Builds a `Conditional`, returing context managers to build the `True` and
177+
"""Builds a `Conditional`, returning context managers to build the `True` and
178178
`False` branch.
179179
"""
180180
conditional = self.builder.add_conditional(

guppylang/std/_internal/compiler/arithmetic.py

+21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from hugr import tys as ht
88
from hugr.std.int import int_t
99

10+
from guppylang.std._internal.compiler.prelude import error_type
1011
from guppylang.tys.ty import NumericType
1112

1213
INT_T = int_t(NumericType.INT_WIDTH)
@@ -55,6 +56,26 @@ def iwiden_s(from_width: int, to_width: int) -> ops.ExtOp:
5556
)
5657

5758

59+
def inarrow_u(from_width: int, to_width: int) -> ops.ExtOp:
60+
"""Returns an unsigned `std.arithmetic.int.narrow_u` operation."""
61+
return _instantiate_int_op(
62+
"inarrow_u",
63+
[from_width, to_width],
64+
[int_t(from_width)],
65+
[ht.Either([error_type()], [int_t(to_width)])],
66+
)
67+
68+
69+
def inarrow_s(from_width: int, to_width: int) -> ops.ExtOp:
70+
"""Returns a signed `std.arithmetic.int.narrow_s` operation."""
71+
return _instantiate_int_op(
72+
"inarrow_s",
73+
[from_width, to_width],
74+
[int_t(from_width)],
75+
[ht.Either([error_type()], [int_t(to_width)])],
76+
)
77+
78+
5879
# ------------------------------------------------------
5980
# --------- std.arithmetic.conversions ops -------------
6081
# ------------------------------------------------------
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from hugr import Wire
2+
from hugr import tys as ht
3+
from hugr.std.int import int_t
4+
5+
from guppylang.definition.custom import CustomInoutCallCompiler
6+
from guppylang.definition.value import CallReturnWires
7+
from guppylang.std._internal.compiler.arithmetic import inarrow_s, iwiden_s
8+
from guppylang.std._internal.compiler.prelude import build_unwrap_right
9+
from guppylang.std._internal.compiler.quantum import (
10+
QSYSTEM_RANDOM_EXTENSION,
11+
RNGCONTEXT_T,
12+
)
13+
from guppylang.std._internal.util import external_op
14+
15+
16+
class RandomIntCompiler(CustomInoutCallCompiler):
17+
def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires:
18+
[ctx] = args
19+
[rnd, ctx] = self.builder.add_op(
20+
external_op("RandomInt", [], ext=QSYSTEM_RANDOM_EXTENSION)(
21+
ht.FunctionType([RNGCONTEXT_T], [int_t(5), RNGCONTEXT_T]), []
22+
),
23+
ctx,
24+
)
25+
[rnd] = self.builder.add_op(iwiden_s(5, 6), rnd)
26+
return CallReturnWires(regular_returns=[rnd], inout_returns=[ctx])
27+
28+
29+
class RandomIntBoundedCompiler(CustomInoutCallCompiler):
30+
def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires:
31+
[ctx, bound] = args
32+
bound_sum = self.builder.add_op(inarrow_s(6, 5), bound)
33+
bound = build_unwrap_right(
34+
self.builder, bound_sum, "bound must be a 32-bit integer"
35+
)
36+
[rnd, ctx] = self.builder.add_op(
37+
external_op("RandomIntBounded", [], ext=QSYSTEM_RANDOM_EXTENSION)(
38+
ht.FunctionType([RNGCONTEXT_T, int_t(5)], [int_t(5), RNGCONTEXT_T]), []
39+
),
40+
ctx,
41+
bound,
42+
)
43+
[rnd] = self.builder.add_op(iwiden_s(5, 6), rnd)
44+
return CallReturnWires(regular_returns=[rnd], inout_returns=[ctx])

guppylang/std/_internal/compiler/quantum.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@
88
from hugr import ext as he
99
from hugr import tys as ht
1010
from hugr.std.float import FLOAT_T
11-
from tket2_exts import futures, qsystem, qsystem_utils, quantum, result, rotation
11+
from tket2_exts import (
12+
futures,
13+
qsystem,
14+
qsystem_random,
15+
qsystem_utils,
16+
quantum,
17+
result,
18+
rotation,
19+
)
1220

1321
from guppylang.definition.custom import CustomInoutCallCompiler
1422
from guppylang.definition.value import CallReturnWires
@@ -19,17 +27,22 @@
1927

2028
FUTURES_EXTENSION = futures()
2129
QSYSTEM_EXTENSION = qsystem()
30+
QSYSTEM_RANDOM_EXTENSION = qsystem_random()
2231
QSYSTEM_UTILS_EXTENSION = qsystem_utils()
2332
QUANTUM_EXTENSION = quantum()
2433
RESULT_EXTENSION = result()
2534
ROTATION_EXTENSION = rotation()
2635

36+
RNGCONTEXT_T_DEF = QSYSTEM_RANDOM_EXTENSION.get_type("context")
37+
RNGCONTEXT_T = ht.ExtType(RNGCONTEXT_T_DEF)
38+
2739
ROTATION_T_DEF = ROTATION_EXTENSION.get_type("rotation")
2840
ROTATION_T = ht.ExtType(ROTATION_T_DEF)
2941

3042
TKET2_EXTENSIONS = [
3143
FUTURES_EXTENSION,
3244
QSYSTEM_EXTENSION,
45+
QSYSTEM_RANDOM_EXTENSION,
3346
QSYSTEM_UTILS_EXTENSION,
3447
QUANTUM_EXTENSION,
3548
RESULT_EXTENSION,

guppylang/std/qsystem/random.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from typing import no_type_check
2+
3+
from guppylang.decorator import guppy
4+
from guppylang.module import GuppyModule
5+
from guppylang.std._internal.compiler.qsystem import (
6+
RandomIntBoundedCompiler,
7+
RandomIntCompiler,
8+
)
9+
from guppylang.std._internal.compiler.quantum import (
10+
QSYSTEM_RANDOM_EXTENSION,
11+
RNGCONTEXT_T,
12+
)
13+
from guppylang.std._internal.util import external_op
14+
from guppylang.std.builtins import nat, owned
15+
from guppylang.std.option import Option
16+
17+
qsystem_random = GuppyModule("qsystem.random")
18+
19+
20+
@guppy.hugr_op(
21+
external_op("NewRNGContext", [], ext=QSYSTEM_RANDOM_EXTENSION),
22+
module=qsystem_random,
23+
)
24+
@no_type_check
25+
def _new_rng_context(seed: int) -> Option["RNG"]: ...
26+
27+
28+
@guppy(qsystem_random)
29+
def maybe_rng(seed: int) -> Option["RNG"]: # type: ignore[type-arg] # "Option" expects no type arguments, but 1 given
30+
"""Safely create a new random number generator using a seed.
31+
32+
Returns `nothing` if RNG is already initialized."""
33+
return _new_rng_context(seed) # type: ignore[no-any-return] # Returning Any from function declared to return "Option"
34+
35+
36+
@guppy.type(RNGCONTEXT_T, copyable=False, droppable=False, module=qsystem_random)
37+
class RNG:
38+
"""Random number generator."""
39+
40+
@guppy(qsystem_random) # type: ignore[misc] # Unsupported decorated constructor type; Self argument missing for a non-static method (or an invalid type for self)
41+
def __new__(seed: int) -> "RNG":
42+
"""Create a new random number generator using a seed."""
43+
return _new_rng_context(seed).unwrap() # type: ignore[no-any-return] # Returning Any from function declared to return "RNGContext"
44+
45+
@guppy.hugr_op(
46+
external_op("DeleteRNGContext", [], ext=QSYSTEM_RANDOM_EXTENSION),
47+
module=qsystem_random,
48+
)
49+
@no_type_check
50+
def discard(self: "RNG" @ owned) -> None: ...
51+
52+
@guppy.custom(RandomIntCompiler(), module=qsystem_random)
53+
@no_type_check
54+
def random_int(self: "RNG") -> int: ...
55+
56+
@guppy(qsystem_random)
57+
def random_nat(self: "RNG") -> nat:
58+
"""Generate a random 32-bit natural number."""
59+
return nat(self.random_int()) # type: ignore[call-arg] # Too many arguments for "nat"
60+
61+
@guppy.hugr_op(
62+
external_op("RandomFloat", [], ext=QSYSTEM_RANDOM_EXTENSION),
63+
module=qsystem_random,
64+
)
65+
@no_type_check
66+
def random_float(self: "RNG") -> float: ...
67+
68+
@guppy.custom(RandomIntBoundedCompiler(), module=qsystem_random)
69+
@no_type_check
70+
def random_int_bounded(self: "RNG", bound: int) -> int: ...

pyproject.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ dependencies = [
3939
"pydantic >=2.7.0b1,<3",
4040
"typing-extensions >=4.9.0,<5",
4141
"hugr >=0.10.3,<0.11",
42-
"tket2-exts>=0.4.0,<0.5",
42+
"tket2-exts>=0.5.0,<0.6",
4343
]
4444

4545
[project.optional-dependencies]
@@ -65,7 +65,7 @@ test = [
6565
"pytest-notebook >=0.10.0,<0.11",
6666
"pytest-snapshot >=0.9.0,<1",
6767
"ipykernel >=6.29.5,<7",
68-
"tket2>=0.6.1",
68+
"tket2>=0.7.0",
6969
"pytest-benchmark>=5.1.0",
7070
]
7171
execution = [
@@ -76,7 +76,7 @@ execution = [
7676
"maturin >=1.7.7,<2",
7777
"pip >=24",
7878
]
79-
pytket_integration = [{ include-group = "test" }, "pytket >=1.34.0,<2"]
79+
pytket_integration = [{ include-group = "test" }, "pytket >=1.34.0"]
8080

8181
[tool.uv.workspace]
8282
members = ["execute_llvm"]
@@ -86,7 +86,7 @@ execute-llvm = { workspace = true }
8686

8787
# Uncomment these to test the latest dependency version during development
8888
# hugr = { git = "https://github.com/CQCL/hugr", subdirectory = "hugr-py", rev = "e40b6c7" }
89-
# tket2-exts = { git = "https://github.com/CQCL/tket2", subdirectory = "tket2-exts", rev = "175a02d"}
89+
# tket2-exts = { git = "https://github.com/CQCL/tket2", subdirectory = "tket2-exts", rev = "633ebd7"}
9090
# tket2 = { git = "https://github.com/CQCL/tket2", subdirectory = "tket2-py", rev = "259cf88"}
9191

9292

tests/integration/test_qsystem.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from guppylang.module import GuppyModule
55
from guppylang.std.angles import angle
66

7-
from guppylang.std.builtins import owned
7+
from guppylang.std.builtins import nat, owned
8+
from guppylang.std.qsystem.random import RNG, maybe_rng
89
from guppylang.std.qsystem.utils import get_current_shot
910
from guppylang.std.quantum import qubit
1011
from guppylang.std.qsystem.functional import (
@@ -13,6 +14,7 @@
1314
qsystem_functional,
1415
measure_and_reset,
1516
zz_max,
17+
reset,
1618
rz,
1719
measure,
1820
qfree,
@@ -30,7 +32,7 @@ def compile_qsystem_guppy(fn) -> ModulePointer: # type: ignore[no-untyped-def]
3032
), "`@compile_qsystem_guppy` does not support extra arguments."
3133

3234
module = GuppyModule("module")
33-
module.load(angle, qubit, get_current_shot) # type: ignore[arg-type]
35+
module.load(angle, qubit, get_current_shot, RNG, maybe_rng) # type: ignore[arg-type]
3436
module.load_all(qsystem_functional)
3537
guppylang.decorator.guppy(module)(fn)
3638
return module.compile()
@@ -53,3 +55,21 @@ def test(q1: qubit @ owned, q2: qubit @ owned, a1: angle) -> bool:
5355
return b
5456

5557
validate(test)
58+
59+
60+
def test_qsystem_random(validate): # type: ignore[no-untyped-def]
61+
"""Compile various operations from the qsystem random extension."""
62+
63+
@compile_qsystem_guppy
64+
def test() -> tuple[int, nat, float, int]:
65+
rng = RNG(42)
66+
rng2 = maybe_rng(84)
67+
rng2.unwrap_nothing()
68+
rint = rng.random_int()
69+
rnat = rng.random_nat()
70+
rfloat = rng.random_float()
71+
rint_bnd = rng.random_int_bounded(100)
72+
rng.discard()
73+
return rint, rnat, rfloat, rint_bnd
74+
75+
validate(test)

0 commit comments

Comments
 (0)