Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cirq-core/cirq/contrib/qasm_import/_lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __init__(self):
'ID',
'ARROW',
'EQ',
'AND',
] + list(reserved.values())

def t_newline(self, t):
Expand Down Expand Up @@ -101,6 +102,10 @@ def t_EQ(self, t):
"""=="""
return t

def t_AND(self, t):
"""&&"""
return t

def t_ID(self, t):
r"""[a-zA-Z_][a-zA-Z\d_]*"""
if t.value in QasmLexer.reserved:
Expand Down
29 changes: 22 additions & 7 deletions cirq-core/cirq/contrib/qasm_import/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1100,19 +1100,34 @@ def p_reset(self, p):

p[0] = [ops.ResetChannel().on(qreg[i]) for i in range(len(qreg))]

# condition list
# condition_list : carg EQ NATURAL_NUMBER
# | condition_list AND carg EQ NATURAL_NUMBER

def p_condition_list_single(self, p):
"""condition_list : carg EQ NATURAL_NUMBER"""
p[0] = [(p[1], p[3])]

def p_condition_list_and(self, p):
"""condition_list : condition_list AND carg EQ NATURAL_NUMBER"""
p[0] = p[1] + [(p[3], p[5])]

# if operations
# if : IF '(' carg EQ NATURAL_NUMBER ')' ID qargs

def p_if(self, p):
"""if : IF '(' carg EQ NATURAL_NUMBER ')' gate_op"""
# We have to split the register into bits (since that's what measurement does above),
# and create one condition per bit, checking against that part of the binary value.
"""if : IF '(' condition_list ')' gate_op"""
# For each condition, we have to split the register into bits (since that's what
# measurement does above), and create one condition per bit, checking against that part of
# the binary value.
conditions = []
for i, key in enumerate(p[3]):
v = (p[5] >> i) & 1
conditions.append(sympy.Eq(sympy.Symbol(key), v))
for cond in p[3]:
carg, val = cond
for i, key in enumerate(carg):
v = (val >> i) & 1
conditions.append(sympy.Eq(sympy.Symbol(key), v))
p[0] = [
ops.ClassicallyControlledOperation(conditions=conditions, sub_operation=tuple(p[7])[0])
ops.ClassicallyControlledOperation(conditions=conditions, sub_operation=tuple(p[5])[0])
]

def p_gate_params_multiple(self, p):
Expand Down
44 changes: 41 additions & 3 deletions cirq-core/cirq/contrib/qasm_import/_parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,50 @@ def test_classical_control_multi_bit() -> None:
ct.assert_same_circuits(parsed_qasm.circuit, expected_circuit)
assert parsed_qasm.qregs == {'q': 2}

# Note that this will *not* round-trip, but there's no good way around that due to the
# difference in how Cirq and QASM do multi-bit measurements.
with pytest.raises(ValueError, match='QASM does not support multiple conditions'):
# Note that this will *not* round-trip in QASM 2.0, but there's no good way around that due
# to the difference in how Cirq and QASM 2.0 do multi-bit measurements.
# Exporting multi-controlled operations in QASM 3.0 is supported with explicit '&&' between
# conditions.
with pytest.raises(
ValueError,
match='QASM 2.0 does not support multiple conditions. Consider exporting with QASM 3.0.',
):
_ = cirq.qasm(parsed_qasm.circuit)


def test_classical_control_multi_cond_and() -> None:
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
bit[2] a;
a[0] = measure q[0];
a[1] = measure q[1];
if (a[0]==1 && a[1]==0) cx q[0],q[1];
"""
parser = QasmParser()

q_0 = cirq.NamedQubit('q_0')
q_1 = cirq.NamedQubit('q_1')

# Expect two independent conditions: a_0 == 1 and a_1 == 0
expected_circuit = cirq.Circuit(
cirq.measure(q_0, key='a_0'),
cirq.measure(q_1, key='a_1'),
cirq.CNOT(q_0, q_1).with_classical_controls(
sympy.Eq(sympy.Symbol('a_0'), 1), sympy.Eq(sympy.Symbol('a_1'), 0)
),
)

parsed_qasm = parser.parse(qasm)

assert parsed_qasm.supportedFormat

ct.assert_same_circuits(parsed_qasm.circuit, expected_circuit)
assert parsed_qasm.qregs == {'q': 2}


def test_CX_gate_not_enough_args() -> None:
qasm = """OPENQASM 2.0;
qreg q[2];
Expand Down
9 changes: 6 additions & 3 deletions cirq-core/cirq/ops/classically_controlled_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,12 @@ def _control_keys_(self) -> frozenset[cirq.MeasurementKey]:

def _qasm_(self, args: cirq.QasmArgs) -> str | None:
args.validate_version('2.0', '3.0')
if len(self._conditions) > 1:
raise ValueError('QASM does not support multiple conditions.')
if args.version == "2.0" and len(self._conditions) > 1:
raise ValueError(
'QASM 2.0 does not support multiple conditions. Consider exporting with QASM 3.0.'
)
subop_qasm = protocols.qasm(self._sub_operation, args=args)
if not self._conditions:
return subop_qasm
return f'if ({protocols.qasm(self._conditions[0], args=args)}) {subop_qasm}'
condition_qasm = " && ".join(protocols.qasm(c, args=args) for c in self._conditions)
return f'if ({condition_qasm}) {subop_qasm}'
28 changes: 26 additions & 2 deletions cirq-core/cirq/ops/classically_controlled_operation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,32 @@ def test_qasm_multiple_conditions() -> None:
sympy.Eq(sympy.Symbol('a'), 0), sympy.Eq(sympy.Symbol('b'), 0)
),
)
with pytest.raises(ValueError, match='QASM does not support multiple conditions'):
_ = cirq.qasm(circuit)
with pytest.raises(
ValueError,
match='QASM 2.0 does not support multiple conditions. Consider exporting with QASM 3.0.',
):
_ = cirq.qasm(circuit, args=cirq.QasmArgs(version='2.0'))

qasm = cirq.qasm(circuit, args=cirq.QasmArgs(version='3.0'))
assert (
qasm
== f"""// Generated from Cirq v{cirq.__version__}
OPENQASM 3.0;
include "stdgates.inc";
// Qubits: [q(0), q(1)]
qubit[2] q;
bit[1] m_a;
bit[1] m_b;
m_a[0] = measure q[0];
m_b[0] = measure q[0];
if (m_a==0 && m_b==0) x q[1];
"""
)


@pytest.mark.parametrize('sim', ALL_SIMULATORS)
Expand Down