Skip to content

Commit 4bb47dd

Browse files
Simplified implementation of gate modifiers (#64)
* add gate modifier classes * clean up * fix isort * implement to_ast for OQpyGateModifier * add negctrl * add number of controls to ctrl and negctrl * test that only ints are passed to PhysicalQubits * test modifier in gate context * add gate modifier arguments * add qubit argument to gate modifiers * reorganize xfail tests * add test for second method * test overlapping qubit sets * add overlapping set test for the first method * clean code * remove OQPyGateModifier * clean tests * change pow argument to exp * change to singular * check types * update qubit array test * encapsulate modifier building code in a method * move function --------- Co-authored-by: Phil Reinhold <[email protected]>
1 parent f0885b7 commit 4bb47dd

File tree

3 files changed

+170
-4
lines changed

3 files changed

+170
-4
lines changed

oqpy/program.py

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from oqpy import classical_types, quantum_types
3535
from oqpy.base import (
3636
AstConvertible,
37+
OQPyExpression,
3738
Var,
3839
expr_matches,
3940
map_to_ast,
@@ -495,19 +496,107 @@ def returns(self, expression: AstConvertible) -> Program:
495496
self._add_statement(ast.ReturnStatement(to_ast(self, expression)))
496497
return self
497498

499+
def _create_modifiers_ast(
500+
self,
501+
control: quantum_types.Qubit | Iterable[quantum_types.Qubit] | None,
502+
neg_control: quantum_types.Qubit | Iterable[quantum_types.Qubit] | None,
503+
inv: bool,
504+
exp: AstConvertible,
505+
) -> tuple[list[ast.QuantumGateModifier], list[AstConvertible]]:
506+
"""Create the AST for the gate modifiers."""
507+
used_qubits: list[AstConvertible] = []
508+
modifiers: list[ast.QuantumGateModifier] = []
509+
510+
control = control if control is not None else []
511+
control = {control} if isinstance(control, quantum_types.Qubit) else set(control)
512+
if control:
513+
modifiers.append(
514+
ast.QuantumGateModifier(
515+
modifier=ast.GateModifierName.ctrl,
516+
argument=to_ast(self, len(control)) if len(control) > 1 else None,
517+
)
518+
)
519+
used_qubits.extend(sorted(control))
520+
521+
neg_control = neg_control if neg_control is not None else []
522+
neg_control = (
523+
{neg_control} if isinstance(neg_control, quantum_types.Qubit) else set(neg_control)
524+
)
525+
if neg_control:
526+
modifiers.append(
527+
ast.QuantumGateModifier(
528+
modifier=ast.GateModifierName.negctrl,
529+
argument=to_ast(self, len(neg_control)) if len(neg_control) > 1 else None,
530+
)
531+
)
532+
for qubit in sorted(neg_control):
533+
if qubit in used_qubits:
534+
raise ValueError(f"Qubit {qubit} has already been defined as a control qubit.")
535+
else:
536+
used_qubits.append(qubit)
537+
538+
if inv:
539+
modifiers.append(
540+
ast.QuantumGateModifier(
541+
modifier=ast.GateModifierName.inv,
542+
)
543+
)
544+
545+
if isinstance(exp, OQPyExpression) or (isinstance(exp, float) and exp != 1.0):
546+
modifiers.append(
547+
ast.QuantumGateModifier(
548+
modifier=ast.GateModifierName.pow, argument=to_ast(self, exp)
549+
)
550+
)
551+
return modifiers, used_qubits
552+
498553
def gate(
499-
self, qubits: AstConvertible | Iterable[AstConvertible], name: str, *args: Any
554+
self,
555+
qubits: AstConvertible | Iterable[AstConvertible],
556+
name: str,
557+
*args: Any,
558+
control: quantum_types.Qubit | Iterable[quantum_types.Qubit] | None = None,
559+
neg_control: quantum_types.Qubit | Iterable[quantum_types.Qubit] | None = None,
560+
inv: bool = False,
561+
exp: AstConvertible = 1,
500562
) -> Program:
501-
"""Apply a gate to a qubit or set of qubits."""
563+
"""Apply a gate with its modifiers to a qubit or set of qubits.
564+
565+
Args:
566+
qubits (AstConvertible | Iterable[AstConvertible]): The qubit or list of qubits
567+
to which the gate will be applied
568+
name (str): The gate name
569+
*args (Any): A list of parameters passed to the gate
570+
control (quantum_types.Qubit | Iterable[quantum_types.Qubit] | None): The list
571+
of control qubits (default: None)
572+
neg_control: (quantum_types.Qubit | Iterable[quantum_types.Qubit] | None): The list
573+
of negative control qubits (default: None)
574+
inv (bool): Flag to use the inverse gate (default: False)
575+
exp (AstConvertible): The exponent used with `pow` gate modifier
576+
577+
Returns:
578+
Program: The OQpy program to which the gate is added
579+
"""
580+
modifiers, used_qubits = self._create_modifiers_ast(control, neg_control, inv, exp)
581+
502582
if isinstance(qubits, (quantum_types.Qubit, quantum_types.IndexedQubitArray)):
503583
qubits = [qubits]
504584
assert isinstance(qubits, Iterable)
585+
586+
for qubit in qubits:
587+
if qubit in used_qubits:
588+
raise ValueError(
589+
f"Qubit {qubit} has already been defined as a control qubit or a negative control qubit."
590+
)
591+
else:
592+
used_qubits.append(qubit)
593+
505594
self._add_statement(
506595
ast.QuantumGate(
507-
[],
596+
modifiers,
508597
ast.Identifier(name),
509598
map_to_ast(self, args),
510-
map_to_ast(self, qubits),
599+
map_to_ast(self, used_qubits),
511600
)
512601
)
513602
return self

oqpy/quantum_types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ def __init__(
4848
self.size = size
4949
self.annotations = annotations
5050

51+
def __hash__(self) -> int:
52+
return hash(self.name)
53+
54+
def __eq__(self, other: object) -> bool:
55+
return isinstance(other, Qubit) and self.name == other.name
56+
57+
def __lt__(self, other: Qubit) -> bool:
58+
return self.name < other.name
59+
5160
def to_ast(self, prog: Program) -> ast.Expression:
5261
"""Converts the OQpy variable into an ast node."""
5362
prog._add_var(self)
@@ -77,6 +86,7 @@ class PhysicalQubits:
7786
"""
7887

7988
def __class_getitem__(cls, item: int) -> Qubit:
89+
assert isinstance(item, int)
8090
return Qubit(f"${item}", needs_declaration=False)
8191

8292

tests/test_directives.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,8 @@ def test_add_incomptible_type():
496496
def test_measure_reset_pragma():
497497
prog = Program()
498498
q = PhysicalQubits[0]
499+
with pytest.raises(AssertionError):
500+
reg = PhysicalQubits[0:1]
499501
c = BitVar(name="c")
500502
prog.reset(q)
501503
prog.pragma("CLASSIFIER linear")
@@ -2246,6 +2248,58 @@ def g() -> int[32] {
22462248
).strip()
22472249

22482250
assert prog.to_qasm() == expected
2251+
_check_respects_type_hints(prog)
2252+
2253+
2254+
def test_gate_modifiers():
2255+
prog = oqpy.Program()
2256+
qreg = [oqpy.PhysicalQubits[i] for i in range(0, 8)]
2257+
six_qubit_reg = [qreg[i] for i in [1, 4, 7, 2, 3, 5]]
2258+
2259+
prog.gate(qreg[2], "t", control=qreg[1])
2260+
prog.gate(qreg[2], "x", neg_control=qreg[1])
2261+
prog.gate(qreg[3], "rz", inv=True)
2262+
prog.gate(qreg[2], "t", exp=0.5)
2263+
prog.gate(qreg[0], "x", inv=True, exp=oqpy.IntVar(5, "i") / 2)
2264+
2265+
prog.gate(
2266+
six_qubit_reg[-1],
2267+
"x",
2268+
control=six_qubit_reg[0:2],
2269+
neg_control=six_qubit_reg[2:5],
2270+
inv=True,
2271+
exp=1 / 2,
2272+
)
2273+
2274+
prog.gate(
2275+
qreg[6],
2276+
"rz1",
2277+
control=[qreg[2], qreg[4], qreg[5]],
2278+
neg_control=[qreg[0], qreg[1], qreg[0]],
2279+
)
2280+
2281+
with pytest.raises(ValueError):
2282+
prog.gate(qreg[2], "t", control=qreg[2])
2283+
with pytest.raises(ValueError):
2284+
prog.gate(qreg[2], "x", neg_control=qreg[2])
2285+
with pytest.raises(ValueError):
2286+
prog.gate(qreg[1], "x", control=qreg[2], neg_control=qreg[2])
2287+
2288+
expected = textwrap.dedent(
2289+
"""
2290+
OPENQASM 3.0;
2291+
int[32] i = 5;
2292+
ctrl @ t $1, $2;
2293+
negctrl @ x $1, $2;
2294+
inv @ rz $3;
2295+
pow(0.5) @ t $2;
2296+
inv @ pow(i / 2) @ x $0;
2297+
ctrl(2) @ negctrl(3) @ inv @ pow(0.5) @ x $1, $4, $2, $3, $7, $5;
2298+
ctrl(3) @ negctrl(2) @ rz1 $2, $4, $5, $0, $1, $6;
2299+
"""
2300+
).strip()
2301+
assert prog.to_qasm() == expected
2302+
_check_respects_type_hints(prog)
22492303

22502304

22512305
def test_invalid_gates():
@@ -2266,6 +2320,7 @@ def test_invalid_gates():
22662320
def test_gate_declarations():
22672321
prog = oqpy.Program()
22682322
q = oqpy.Qubit("q", needs_declaration=False)
2323+
r = oqpy.Qubit("r", needs_declaration=False)
22692324
with oqpy.gate(
22702325
prog,
22712326
q,
@@ -2280,6 +2335,10 @@ def test_gate_declarations():
22802335
prog.gate(q, "u", theta, 0, 0)
22812336
with oqpy.gate(prog, q, "t"):
22822337
prog.gate(q, "rz", oqpy.pi / 4)
2338+
with oqpy.gate(prog, [q, r], "cnot"):
2339+
prog.gate(r, "x", control=q)
2340+
with oqpy.gate(prog, [q, r], "ncphaseshift", [oqpy.AngleVar(name="theta")]) as theta:
2341+
prog.gate(r, "phase", theta, neg_control=[q])
22832342

22842343
prog.gate(oqpy.PhysicalQubits[1], "t")
22852344
prog.gate(oqpy.PhysicalQubits[2], "t")
@@ -2296,6 +2355,12 @@ def test_gate_declarations():
22962355
gate t q {
22972356
rz(pi / 4) q;
22982357
}
2358+
gate cnot q, r {
2359+
ctrl @ x q, r;
2360+
}
2361+
gate ncphaseshift(theta) q, r {
2362+
negctrl @ phase(theta) q, r;
2363+
}
22992364
gate rz(theta) q {
23002365
u(theta, 0, 0) q;
23012366
}
@@ -2305,6 +2370,7 @@ def test_gate_declarations():
23052370
).strip()
23062371

23072372
assert prog.to_qasm() == expected
2373+
_check_respects_type_hints(prog)
23082374

23092375

23102376
def test_include():
@@ -2321,6 +2387,7 @@ def test_include():
23212387
).strip()
23222388

23232389
assert prog.to_qasm() == expected
2390+
_check_respects_type_hints(prog)
23242391

23252392

23262393
def test_qubit_array():

0 commit comments

Comments
 (0)