Skip to content

Commit fcdf63e

Browse files
jkalsi1mhuckapavoljuhas
authored
Support "input" modifier in QASM3 parser (#7997)
Added reserved keywords: 'input', 'float', and 'angle'. Wrote test cases for the new keywords. Input parameters become sympy.Symbols in the parsed qasm. Fixes #7933 --------- Co-authored-by: Michael Hucka <mhucka@caltech.edu> Co-authored-by: Michael Hucka <mhucka@google.com> Co-authored-by: Pavol Juhas <juhas@google.com>
1 parent b3366ab commit fcdf63e

3 files changed

Lines changed: 182 additions & 16 deletions

File tree

cirq-core/cirq/contrib/qasm_import/_lexer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def __init__(self):
3737
'gate': 'GATE',
3838
'if': 'IF',
3939
'pi': 'PI',
40+
'input': 'INPUT',
41+
'float': 'FLOAT',
42+
'angle': 'ANGLE',
4043
}
4144

4245
tokens = [

cirq-core/cirq/contrib/qasm_import/_parser.py

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ class Qasm:
3737
"""Qasm stores the final result of the Qasm parsing."""
3838

3939
def __init__(
40-
self, supported_format: bool, qelib1_include: bool, qregs: dict, cregs: dict, c: Circuit
40+
self,
41+
supported_format: bool,
42+
qelib1_include: bool,
43+
qregs: dict,
44+
cregs: dict,
45+
input_params: dict[str, str],
46+
circuit: Circuit,
4147
):
4248
"""Initializes Qasm.
4349
@@ -53,7 +59,8 @@ def __init__(
5359
self.supportedFormat = supported_format
5460
self.qregs = qregs
5561
self.cregs = cregs
56-
self.circuit = c
62+
self.circuit = circuit
63+
self.input_params = input_params
5764

5865

5966
def _generate_op_qubits(args: list[list[ops.Qid]], lineno: int) -> list[list[ops.Qid]]:
@@ -187,24 +194,34 @@ def __init__(self) -> None:
187194
Attributes:
188195
gate_set: The gates available to use in the circuit, including those from
189196
libraries, and user-defined ones.
197+
circuit: A Cirq Circuit object.
198+
qregs: Quantum registers.
199+
cregs: Classical registers.
190200
in_custom_gate_scope: This is set to True when the parser is in the middle
191201
of parsing a custom gate definition.
192202
custom_gate_scoped_params: The params declared within the current custom
193203
gate definition. Empty if not in custom gate scope.
194204
custom_gate_scoped_qubits: The qubits declared within the current custom
195205
gate definition. Empty if not in custom gate scope.
206+
input_params: The input parameters mapped from name to type.
207+
qelibinc: Boolean indicating whether the Quantum Experience standard header
208+
is present or not.
209+
supported_format: Boolean indicating whether the format is supported.
210+
format_version: The OpenQASM version string.
196211
"""
197212
self.parser = yacc.yacc(module=self, debug=False, write_tables=False)
198213
self.circuit = Circuit()
199214
self.qregs: dict[str, int] = {}
200215
self.cregs: dict[str, int] = {}
201216
self.gate_set: dict[str, CustomGate | QasmGateStatement] = {**self.basic_gates}
202-
self.in_custom_gate_scope = False
217+
self.in_custom_gate_scope: bool = False
203218
self.custom_gate_scoped_params: set[str] = set()
204219
self.custom_gate_scoped_qubits: dict[str, ops.Qid] = {}
205-
self.qelibinc = False
220+
self.input_params: dict[str, str] = {}
221+
self.qelibinc: bool = False
206222
self.lexer = QasmLexer()
207-
self.supported_format = False
223+
self.supported_format: bool = False
224+
self.format_version: str = ""
208225
self.parsedQasm: Qasm | None = None
209226
self.qubits: dict[str, ops.Qid] = {}
210227
self.functions = {
@@ -810,7 +827,14 @@ def p_start(self, p):
810827
def p_qasm_format_only(self, p):
811828
"""qasm : format"""
812829
self.supported_format = True
813-
p[0] = Qasm(self.supported_format, self.qelibinc, self.qregs, self.cregs, self.circuit)
830+
p[0] = Qasm(
831+
self.supported_format,
832+
self.qelibinc,
833+
self.qregs,
834+
self.cregs,
835+
self.input_params,
836+
self.circuit,
837+
)
814838

815839
def p_qasm_no_format_specified_error(self, p):
816840
"""qasm : QELIBINC
@@ -823,31 +847,49 @@ def p_qasm_include(self, p):
823847
"""qasm : qasm QELIBINC"""
824848
self.qelibinc = True
825849
self.gate_set |= self.qelib_gates
826-
p[0] = Qasm(self.supported_format, self.qelibinc, self.qregs, self.cregs, self.circuit)
850+
p[0] = Qasm(
851+
self.supported_format,
852+
self.qelibinc,
853+
self.qregs,
854+
self.cregs,
855+
self.input_params,
856+
self.circuit,
857+
)
827858

828859
def p_qasm_include_stdgates(self, p):
829860
"""qasm : qasm STDGATESINC"""
830861
self.qelibinc = True
831862
self.gate_set |= self.qelib_gates
832-
p[0] = Qasm(self.supported_format, self.qelibinc, self.qregs, self.cregs, self.circuit)
863+
p[0] = Qasm(
864+
self.supported_format,
865+
self.qelibinc,
866+
self.qregs,
867+
self.cregs,
868+
self.input_params,
869+
self.circuit,
870+
)
833871

834872
def p_qasm_circuit(self, p):
835873
"""qasm : qasm circuit"""
836-
p[0] = Qasm(self.supported_format, self.qelibinc, self.qregs, self.cregs, p[2])
874+
p[0] = Qasm(
875+
self.supported_format, self.qelibinc, self.qregs, self.cregs, self.input_params, p[2]
876+
)
837877

838878
def p_format(self, p):
839879
"""format : FORMAT_SPEC"""
840880
if p[1] not in ["2.0", "3.0"]:
841881
raise QasmException(
842-
f"Unsupported OpenQASM version: {p[1]}, "
843-
"only 2.0 and 3.0 are supported currently by Cirq"
882+
f"Unsupported OpenQASM version: {p[1]}. "
883+
"Only 2.0 and 3.0 are supported currently by Cirq"
844884
)
885+
self.format_version = p[1]
845886

846887
# circuit : new_reg circuit
847888
# | gate_op circuit
848889
# | measurement circuit
849890
# | reset circuit
850891
# | if circuit
892+
# | input_decl circuit
851893
# | empty
852894

853895
def p_circuit_reg(self, p):
@@ -862,6 +904,10 @@ def p_circuit_gate_or_measurement_or_if(self, p):
862904
self.circuit.append(p[2])
863905
p[0] = self.circuit
864906

907+
def p_circuit_input_decl(self, p):
908+
"""circuit : input_decl circuit"""
909+
p[0] = self.circuit
910+
865911
def p_circuit_empty(self, p):
866912
"""circuit : empty"""
867913
p[0] = self.circuit
@@ -891,7 +937,7 @@ def p_new_reg(self, p):
891937
else:
892938
# QUBIT '[' NATURAL_NUMBER ']' ID ';'
893939
name, length = p[5], p[3]
894-
if name in self.qregs.keys() or name in self.cregs.keys():
940+
if name in self.qregs or name in self.cregs or name in self.input_params:
895941
raise QasmException(f"{name} is already defined at line {p.lineno(2)}")
896942
if length == 0:
897943
raise QasmException(f"Illegal, zero-length register '{name}' at line {p.lineno(4)}")
@@ -934,6 +980,34 @@ def p_params_single(self, p):
934980
"""params : expr"""
935981
p[0] = [p[1]]
936982

983+
# input declarations (OpenQASM 3.0 only)
984+
# input_decl : INPUT input_type '[' NATURAL_NUMBER ']' ID ';'
985+
986+
def p_input_type(self, p):
987+
"""input_type : FLOAT
988+
| ANGLE
989+
"""
990+
p[0] = p[1]
991+
992+
def p_input_decl(self, p):
993+
"""input_decl : INPUT input_type '[' NATURAL_NUMBER ']' ID ';'"""
994+
if self.format_version != "3.0":
995+
raise QasmException(
996+
f"'input' modifier at line {p.lineno(1)} is only supported in OpenQASM 3.0"
997+
)
998+
# INPUT input_type '[' NATURAL_NUMBER ']' ID ';'
999+
bit_width = p[4]
1000+
if bit_width == 0:
1001+
raise QasmException(
1002+
f"Illegal bit width of zero for input '{p[6]}' at line {p.lineno(4)}"
1003+
)
1004+
input_type = f"{p[2]}[{bit_width}]"
1005+
name = p[6]
1006+
if name in self.input_params or name in self.qregs or name in self.cregs:
1007+
raise QasmException(f"{name} is already defined at line {p.lineno(6)}")
1008+
self.input_params[name] = input_type
1009+
p[0] = (name, input_type)
1010+
9371011
# expr : term
9381012
# | ID
9391013
# | func '(' expression ')'
@@ -946,10 +1020,11 @@ def p_expr_term(self, p):
9461020

9471021
def p_expr_identifier(self, p):
9481022
"""expr : ID"""
949-
if not self.in_custom_gate_scope:
950-
raise QasmException(f"Parameter '{p[1]}' in line {p.lineno(1)} not supported")
951-
if p[1] not in self.custom_gate_scoped_params:
952-
raise QasmException(f"Undefined parameter '{p[1]}' in line {p.lineno(1)}'")
1023+
if p[1] not in self.input_params:
1024+
if not self.in_custom_gate_scope:
1025+
raise QasmException(f"Parameter '{p[1]}' in line {p.lineno(1)} not supported")
1026+
if p[1] not in self.custom_gate_scoped_params:
1027+
raise QasmException(f"Undefined parameter '{p[1]}' in line {p.lineno(1)}'")
9531028
p[0] = sympy.Symbol(p[1])
9541029

9551030
def p_expr_parens(self, p):

cirq-core/cirq/contrib/qasm_import/_parser_test.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2437,3 +2437,91 @@ def test_all_qelib_gates_unitary_equivalence(
24372437
U_import = cirq.unitary(imported)
24382438
assert np.allclose(U_import, U_native, atol=1e-8)
24392439
assert parsed_qasm.qregs == {'q': num_args}
2440+
2441+
2442+
@pytest.mark.parametrize('input_type', ['angle', 'float'])
2443+
def test_input_basic(input_type: str) -> None:
2444+
qasm = f"""
2445+
OPENQASM 3.0;
2446+
qreg q[1];
2447+
input {input_type}[64] theta;
2448+
U(theta, 0, 0) q[0];
2449+
"""
2450+
parsed_qasm = QasmParser().parse(qasm)
2451+
2452+
assert parsed_qasm.input_params == {'theta': f'{input_type}[64]'}
2453+
2454+
theta = sympy.Symbol('theta')
2455+
q_0 = cirq.NamedQubit('q_0')
2456+
expected_circuit = Circuit(QasmUGate(theta / np.pi, 0, 0).on(q_0))
2457+
ct.assert_same_circuits(parsed_qasm.circuit, expected_circuit)
2458+
2459+
2460+
def test_input_two_params() -> None:
2461+
qasm = """
2462+
OPENQASM 3.0;
2463+
qreg q[1];
2464+
input angle[64] theta;
2465+
input float[64] phi;
2466+
U(theta, phi, 0) q[0];
2467+
"""
2468+
parsed_qasm = QasmParser().parse(qasm)
2469+
2470+
assert parsed_qasm.input_params == {'theta': 'angle[64]', 'phi': 'float[64]'}
2471+
2472+
theta = sympy.Symbol('theta')
2473+
phi = sympy.Symbol('phi')
2474+
q_0 = cirq.NamedQubit('q_0')
2475+
expected_circuit = Circuit(QasmUGate(theta / np.pi, phi / np.pi, 0).on(q_0))
2476+
ct.assert_same_circuits(parsed_qasm.circuit, expected_circuit)
2477+
2478+
2479+
def test_input_not_allowed_in_qasm2() -> None:
2480+
qasm = """
2481+
OPENQASM 2.0;
2482+
qreg q[1];
2483+
input float[64] n;
2484+
"""
2485+
with pytest.raises(
2486+
QasmException, match="'input' modifier at line 4 is only supported in OpenQASM 3.0"
2487+
):
2488+
QasmParser().parse(qasm)
2489+
2490+
2491+
@pytest.mark.parametrize(
2492+
'first_decl,second_decl,name',
2493+
[
2494+
('input float[32] theta;', 'input angle[32] theta;', 'theta'),
2495+
('qreg q[1];', 'input float[32] q;', 'q'),
2496+
('creg c[1];', 'input float[32] c;', 'c'),
2497+
('input float[32] q;', 'qreg q[1];', 'q'),
2498+
],
2499+
)
2500+
def test_input_duplicate_identifier_error(first_decl: str, second_decl: str, name: str) -> None:
2501+
qasm = f"""
2502+
OPENQASM 3.0;
2503+
{first_decl}
2504+
{second_decl}
2505+
"""
2506+
with pytest.raises(QasmException, match=f"{name} is already defined"):
2507+
QasmParser().parse(qasm)
2508+
2509+
2510+
def test_input_zero_bit_width_error() -> None:
2511+
qasm = """
2512+
OPENQASM 3.0;
2513+
qreg q[1];
2514+
input float[0] theta;
2515+
"""
2516+
with pytest.raises(QasmException, match="Illegal bit width of zero for input 'theta'"):
2517+
QasmParser().parse(qasm)
2518+
2519+
2520+
def test_input_invalid_type_error() -> None:
2521+
qasm = """
2522+
OPENQASM 3.0;
2523+
qreg q[1];
2524+
input badtype theta;
2525+
"""
2526+
with pytest.raises(QasmException, match="Syntax error"):
2527+
QasmParser().parse(qasm)

0 commit comments

Comments
 (0)