diff --git a/doc/gates.md b/doc/gates.md
index 32e73d6f8..a8106f157 100644
--- a/doc/gates.md
+++ b/doc/gates.md
@@ -2757,7 +2757,6 @@ Stabilizer Generators:
Decomposition (into H, S, CX, M, R):
# The following circuit is equivalent (up to global phase) to `RX 0`
- H 0
R 0
H 0
@@ -2790,10 +2789,6 @@ Stabilizer Generators:
Decomposition (into H, S, CX, M, R):
# The following circuit is equivalent (up to global phase) to `RY 0`
- S 0
- S 0
- S 0
- H 0
R 0
H 0
S 0
diff --git a/doc/python_api_reference_vDev.md b/doc/python_api_reference_vDev.md
index b729716b3..5175612a0 100644
--- a/doc/python_api_reference_vDev.md
+++ b/doc/python_api_reference_vDev.md
@@ -46,6 +46,8 @@ API references for stable versions are kept on the [stim github wiki](https://gi
- [`stim.Circuit.search_for_undetectable_logical_errors`](#stim.Circuit.search_for_undetectable_logical_errors)
- [`stim.Circuit.shortest_graphlike_error`](#stim.Circuit.shortest_graphlike_error)
- [`stim.Circuit.to_file`](#stim.Circuit.to_file)
+ - [`stim.Circuit.to_qasm`](#stim.Circuit.to_qasm)
+ - [`stim.Circuit.to_tableau`](#stim.Circuit.to_tableau)
- [`stim.Circuit.with_inlined_feedback`](#stim.Circuit.with_inlined_feedback)
- [`stim.Circuit.without_noise`](#stim.Circuit.without_noise)
- [`stim.CircuitErrorLocation`](#stim.CircuitErrorLocation)
@@ -2359,6 +2361,130 @@ def to_file(
"""
```
+
+```python
+# stim.Circuit.to_qasm
+
+# (in class stim.Circuit)
+def to_qasm(
+ self,
+ *,
+ open_qasm_version: int,
+ skip_dets_and_obs: bool = False,
+) -> str:
+ """Creates an equivalent OpenQASM implementation of the circuit.
+
+ Args:
+ open_qasm_version: Defaults to 3. The version of OpenQASM to target.
+ This should be set to 2 or to 3.
+
+ Differences between the versions are:
+ - Support for operations on classical bits operations (only version
+ 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with
+ version 3.
+ - Support for feedback operations (only version 3).
+ - Support for subroutines (only version 3). Without subroutines,
+ non-standard dissipative gates like MR and RX need to decompose
+ inline every single time they're used.
+ - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc).
+ skip_dets_and_obs: Defaults to False. When set to False, the output will
+ include a `dets` register and an `obs` register (assuming the circuit
+ has detectors and observables). These registers will be computed as part
+ of running the circuit. This requires performing a simulation of the
+ circuit, in order to correctly account for the expected value of
+ measurements.
+
+ When set to True, the `dets` and `obs` registers are not included in the
+ output, and no simulation of the circuit is performed.
+
+ Returns:
+ The OpenQASM code as a string.
+
+ Examples:
+ >>> import stim
+ >>> circuit = stim.Circuit("""
+ ... R 0 1
+ ... H 0
+ ... CX 0 1
+ ... M 0 1
+ ... DETECTOR rec[-1] rec[-2]
+ ... """);
+ >>> qasm = circuit.to_qasm();
+ >>> print(qasm.strip().replace('\n\n', '\n'))
+ OPENQASM 3.0;
+ include "stdgates.inc";
+ qubit q[2];
+ bit m[2];
+ bit dets[1];
+ reset q[0];
+ reset q[1];
+ h q[0];
+ cx q[0], q[1];
+ measure q[0] -> m[0];
+ measure q[1] -> m[1];
+ dets[0] = rec[1] ^ rec[0] ^ 0;
+ """
+```
+
+
+```python
+# stim.Circuit.to_tableau
+
+# (in class stim.Circuit)
+def to_tableau(
+ self,
+ *,
+ ignore_noise: bool = False,
+ ignore_measurement: bool = False,
+ ignore_reset: bool = False,
+) -> stim.Tableau:
+ """Converts the circuit into an equivalent stabilizer tableau.
+
+ Args:
+ ignore_noise: Defaults to False. When False, any noise operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ noise operations are skipped over as if they weren't even present in the
+ circuit.
+ ignore_measurement: Defaults to False. When False, any measurement
+ operations in the circuit will cause the conversion to fail with an
+ exception. When True, measurement operations are skipped over as if they
+ weren't even present in the circuit.
+ ignore_reset: Defaults to False. When False, any reset operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ reset operations are skipped over as if they weren't even present in the
+ circuit.
+
+ Returns:
+ A tableau equivalent to the circuit (up to global phase).
+
+ Raises:
+ ValueError:
+ The circuit contains noise operations but ignore_noise=False.
+ OR
+ The circuit contains measurement operations but
+ ignore_measurement=False.
+ OR
+ The circuit contains reset operations but ignore_reset=False.
+
+ Examples:
+ >>> import stim
+ >>> stim.Circuit('''
+ ... H 0
+ ... CNOT 0 1
+ ... ''').to_tableau()
+ stim.Tableau.from_conjugated_generators(
+ xs=[
+ stim.PauliString("+Z_"),
+ stim.PauliString("+_X"),
+ ],
+ zs=[
+ stim.PauliString("+XX"),
+ stim.PauliString("+ZZ"),
+ ],
+ )
+ """
+```
+
```python
# stim.Circuit.with_inlined_feedback
diff --git a/doc/stim.pyi b/doc/stim.pyi
index bb3b2f8cd..16246528a 100644
--- a/doc/stim.pyi
+++ b/doc/stim.pyi
@@ -1715,6 +1715,116 @@ class Circuit:
>>> contents
'H 5\nX 0\n'
"""
+ def to_qasm(
+ self,
+ *,
+ open_qasm_version: int,
+ skip_dets_and_obs: bool = False,
+ ) -> str:
+ """Creates an equivalent OpenQASM implementation of the circuit.
+
+ Args:
+ open_qasm_version: Defaults to 3. The version of OpenQASM to target.
+ This should be set to 2 or to 3.
+
+ Differences between the versions are:
+ - Support for operations on classical bits operations (only version
+ 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with
+ version 3.
+ - Support for feedback operations (only version 3).
+ - Support for subroutines (only version 3). Without subroutines,
+ non-standard dissipative gates like MR and RX need to decompose
+ inline every single time they're used.
+ - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc).
+ skip_dets_and_obs: Defaults to False. When set to False, the output will
+ include a `dets` register and an `obs` register (assuming the circuit
+ has detectors and observables). These registers will be computed as part
+ of running the circuit. This requires performing a simulation of the
+ circuit, in order to correctly account for the expected value of
+ measurements.
+
+ When set to True, the `dets` and `obs` registers are not included in the
+ output, and no simulation of the circuit is performed.
+
+ Returns:
+ The OpenQASM code as a string.
+
+ Examples:
+ >>> import stim
+ >>> circuit = stim.Circuit("""
+ ... R 0 1
+ ... H 0
+ ... CX 0 1
+ ... M 0 1
+ ... DETECTOR rec[-1] rec[-2]
+ ... """);
+ >>> qasm = circuit.to_qasm();
+ >>> print(qasm.strip().replace('\n\n', '\n'))
+ OPENQASM 3.0;
+ include "stdgates.inc";
+ qubit q[2];
+ bit m[2];
+ bit dets[1];
+ reset q[0];
+ reset q[1];
+ h q[0];
+ cx q[0], q[1];
+ measure q[0] -> m[0];
+ measure q[1] -> m[1];
+ dets[0] = rec[1] ^ rec[0] ^ 0;
+ """
+ def to_tableau(
+ self,
+ *,
+ ignore_noise: bool = False,
+ ignore_measurement: bool = False,
+ ignore_reset: bool = False,
+ ) -> stim.Tableau:
+ """Converts the circuit into an equivalent stabilizer tableau.
+
+ Args:
+ ignore_noise: Defaults to False. When False, any noise operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ noise operations are skipped over as if they weren't even present in the
+ circuit.
+ ignore_measurement: Defaults to False. When False, any measurement
+ operations in the circuit will cause the conversion to fail with an
+ exception. When True, measurement operations are skipped over as if they
+ weren't even present in the circuit.
+ ignore_reset: Defaults to False. When False, any reset operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ reset operations are skipped over as if they weren't even present in the
+ circuit.
+
+ Returns:
+ A tableau equivalent to the circuit (up to global phase).
+
+ Raises:
+ ValueError:
+ The circuit contains noise operations but ignore_noise=False.
+ OR
+ The circuit contains measurement operations but
+ ignore_measurement=False.
+ OR
+ The circuit contains reset operations but ignore_reset=False.
+
+ Examples:
+ >>> import stim
+ >>> stim.Circuit('''
+ ... H 0
+ ... CNOT 0 1
+ ... ''').to_tableau()
+ stim.Tableau.from_conjugated_generators(
+ xs=[
+ stim.PauliString("+Z_"),
+ stim.PauliString("+_X"),
+ ],
+ zs=[
+ stim.PauliString("+XX"),
+ stim.PauliString("+ZZ"),
+ ],
+ )
+ """
def with_inlined_feedback(
self,
) -> stim.Circuit:
diff --git a/file_lists/source_files_no_main b/file_lists/source_files_no_main
index 042c535fd..a0b00cf76 100644
--- a/file_lists/source_files_no_main
+++ b/file_lists/source_files_no_main
@@ -2,6 +2,7 @@ src/stim.cc
src/stim/arg_parse.cc
src/stim/circuit/circuit.cc
src/stim/circuit/circuit_instruction.cc
+src/stim/circuit/export_qasm.cc
src/stim/circuit/gate_data.cc
src/stim/circuit/gate_data_annotations.cc
src/stim/circuit/gate_data_blocks.cc
diff --git a/file_lists/test_files b/file_lists/test_files
index 3fd76c0d8..42fc44604 100644
--- a/file_lists/test_files
+++ b/file_lists/test_files
@@ -1,6 +1,7 @@
src/stim.test.cc
src/stim/arg_parse.test.cc
src/stim/circuit/circuit.test.cc
+src/stim/circuit/export_qasm.test.cc
src/stim/circuit/gate_data.test.cc
src/stim/circuit/gate_decomposition.test.cc
src/stim/circuit/gate_target.test.cc
diff --git a/glue/python/src/stim/__init__.pyi b/glue/python/src/stim/__init__.pyi
index bb3b2f8cd..16246528a 100644
--- a/glue/python/src/stim/__init__.pyi
+++ b/glue/python/src/stim/__init__.pyi
@@ -1715,6 +1715,116 @@ class Circuit:
>>> contents
'H 5\nX 0\n'
"""
+ def to_qasm(
+ self,
+ *,
+ open_qasm_version: int,
+ skip_dets_and_obs: bool = False,
+ ) -> str:
+ """Creates an equivalent OpenQASM implementation of the circuit.
+
+ Args:
+ open_qasm_version: Defaults to 3. The version of OpenQASM to target.
+ This should be set to 2 or to 3.
+
+ Differences between the versions are:
+ - Support for operations on classical bits operations (only version
+ 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with
+ version 3.
+ - Support for feedback operations (only version 3).
+ - Support for subroutines (only version 3). Without subroutines,
+ non-standard dissipative gates like MR and RX need to decompose
+ inline every single time they're used.
+ - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc).
+ skip_dets_and_obs: Defaults to False. When set to False, the output will
+ include a `dets` register and an `obs` register (assuming the circuit
+ has detectors and observables). These registers will be computed as part
+ of running the circuit. This requires performing a simulation of the
+ circuit, in order to correctly account for the expected value of
+ measurements.
+
+ When set to True, the `dets` and `obs` registers are not included in the
+ output, and no simulation of the circuit is performed.
+
+ Returns:
+ The OpenQASM code as a string.
+
+ Examples:
+ >>> import stim
+ >>> circuit = stim.Circuit("""
+ ... R 0 1
+ ... H 0
+ ... CX 0 1
+ ... M 0 1
+ ... DETECTOR rec[-1] rec[-2]
+ ... """);
+ >>> qasm = circuit.to_qasm();
+ >>> print(qasm.strip().replace('\n\n', '\n'))
+ OPENQASM 3.0;
+ include "stdgates.inc";
+ qubit q[2];
+ bit m[2];
+ bit dets[1];
+ reset q[0];
+ reset q[1];
+ h q[0];
+ cx q[0], q[1];
+ measure q[0] -> m[0];
+ measure q[1] -> m[1];
+ dets[0] = rec[1] ^ rec[0] ^ 0;
+ """
+ def to_tableau(
+ self,
+ *,
+ ignore_noise: bool = False,
+ ignore_measurement: bool = False,
+ ignore_reset: bool = False,
+ ) -> stim.Tableau:
+ """Converts the circuit into an equivalent stabilizer tableau.
+
+ Args:
+ ignore_noise: Defaults to False. When False, any noise operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ noise operations are skipped over as if they weren't even present in the
+ circuit.
+ ignore_measurement: Defaults to False. When False, any measurement
+ operations in the circuit will cause the conversion to fail with an
+ exception. When True, measurement operations are skipped over as if they
+ weren't even present in the circuit.
+ ignore_reset: Defaults to False. When False, any reset operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ reset operations are skipped over as if they weren't even present in the
+ circuit.
+
+ Returns:
+ A tableau equivalent to the circuit (up to global phase).
+
+ Raises:
+ ValueError:
+ The circuit contains noise operations but ignore_noise=False.
+ OR
+ The circuit contains measurement operations but
+ ignore_measurement=False.
+ OR
+ The circuit contains reset operations but ignore_reset=False.
+
+ Examples:
+ >>> import stim
+ >>> stim.Circuit('''
+ ... H 0
+ ... CNOT 0 1
+ ... ''').to_tableau()
+ stim.Tableau.from_conjugated_generators(
+ xs=[
+ stim.PauliString("+Z_"),
+ stim.PauliString("+_X"),
+ ],
+ zs=[
+ stim.PauliString("+XX"),
+ stim.PauliString("+ZZ"),
+ ],
+ )
+ """
def with_inlined_feedback(
self,
) -> stim.Circuit:
diff --git a/src/stim.h b/src/stim.h
index 6c5a2a040..b64047f3e 100644
--- a/src/stim.h
+++ b/src/stim.h
@@ -6,6 +6,7 @@
#include "stim/arg_parse.h"
#include "stim/circuit/circuit.h"
#include "stim/circuit/circuit_instruction.h"
+#include "stim/circuit/export_qasm.h"
#include "stim/circuit/gate_data.h"
#include "stim/circuit/gate_data_table.h"
#include "stim/circuit/gate_decomposition.h"
diff --git a/src/stim/circuit/circuit.pybind.cc b/src/stim/circuit/circuit.pybind.cc
index 335ba4499..43d28ded1 100644
--- a/src/stim/circuit/circuit.pybind.cc
+++ b/src/stim/circuit/circuit.pybind.cc
@@ -18,6 +18,7 @@
#include "stim/circuit/circuit_instruction.pybind.h"
#include "stim/circuit/circuit_repeat_block.pybind.h"
+#include "stim/circuit/export_qasm.h"
#include "stim/circuit/gate_target.pybind.h"
#include "stim/cmd/command_diagram.pybind.h"
#include "stim/dem/detector_error_model_target.pybind.h"
@@ -38,6 +39,7 @@
#include "stim/simulators/measurements_to_detection_events.pybind.h"
#include "stim/simulators/tableau_simulator.h"
#include "stim/simulators/transform_without_feedback.h"
+#include "stim/stabilizers/conversions.h"
using namespace stim;
using namespace stim_pybind;
@@ -1318,6 +1320,130 @@ void stim_pybind::pybind_circuit_methods(pybind11::module &, pybind11::class_(circuit, ignore_noise, ignore_measurement, ignore_reset);
+ },
+ pybind11::kw_only(),
+ pybind11::arg("ignore_noise") = false,
+ pybind11::arg("ignore_measurement") = false,
+ pybind11::arg("ignore_reset") = false,
+ clean_doc_string(R"DOC(
+ @signature def to_tableau(self, *, ignore_noise: bool = False, ignore_measurement: bool = False, ignore_reset: bool = False) -> stim.Tableau:
+ Converts the circuit into an equivalent stabilizer tableau.
+
+ Args:
+ ignore_noise: Defaults to False. When False, any noise operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ noise operations are skipped over as if they weren't even present in the
+ circuit.
+ ignore_measurement: Defaults to False. When False, any measurement
+ operations in the circuit will cause the conversion to fail with an
+ exception. When True, measurement operations are skipped over as if they
+ weren't even present in the circuit.
+ ignore_reset: Defaults to False. When False, any reset operations in the
+ circuit will cause the conversion to fail with an exception. When True,
+ reset operations are skipped over as if they weren't even present in the
+ circuit.
+
+ Returns:
+ A tableau equivalent to the circuit (up to global phase).
+
+ Raises:
+ ValueError:
+ The circuit contains noise operations but ignore_noise=False.
+ OR
+ The circuit contains measurement operations but
+ ignore_measurement=False.
+ OR
+ The circuit contains reset operations but ignore_reset=False.
+
+ Examples:
+ >>> import stim
+ >>> stim.Circuit('''
+ ... H 0
+ ... CNOT 0 1
+ ... ''').to_tableau()
+ stim.Tableau.from_conjugated_generators(
+ xs=[
+ stim.PauliString("+Z_"),
+ stim.PauliString("+_X"),
+ ],
+ zs=[
+ stim.PauliString("+XX"),
+ stim.PauliString("+ZZ"),
+ ],
+ )
+ )DOC")
+ .data());
+
+ c.def(
+ "to_qasm",
+ [](const Circuit &self, int open_qasm_version, bool skip_dets_and_obs) -> std::string {
+ std::stringstream out;
+ export_open_qasm(self, out, open_qasm_version, skip_dets_and_obs);
+ return out.str();
+ },
+ pybind11::kw_only(),
+ pybind11::arg("open_qasm_version") = 3,
+ pybind11::arg("skip_dets_and_obs") = false,
+ clean_doc_string(R"DOC(
+ @signature def to_qasm(self, *, open_qasm_version: int, skip_dets_and_obs: bool = False) -> str:
+ Creates an equivalent OpenQASM implementation of the circuit.
+
+ Args:
+ open_qasm_version: Defaults to 3. The version of OpenQASM to target.
+ This should be set to 2 or to 3.
+
+ Differences between the versions are:
+ - Support for operations on classical bits operations (only version
+ 3). This means DETECTOR and OBSERVABLE_INCLUDE only work with
+ version 3.
+ - Support for feedback operations (only version 3).
+ - Support for subroutines (only version 3). Without subroutines,
+ non-standard dissipative gates like MR and RX need to decompose
+ inline every single time they're used.
+ - Minor name changes (e.g. creg -> bit, qelib1.inc -> stdgates.inc).
+ skip_dets_and_obs: Defaults to False. When set to False, the output will
+ include a `dets` register and an `obs` register (assuming the circuit
+ has detectors and observables). These registers will be computed as part
+ of running the circuit. This requires performing a simulation of the
+ circuit, in order to correctly account for the expected value of
+ measurements.
+
+ When set to True, the `dets` and `obs` registers are not included in the
+ output, and no simulation of the circuit is performed.
+
+ Returns:
+ The OpenQASM code as a string.
+
+ Examples:
+ >>> import stim
+ >>> circuit = stim.Circuit("""
+ ... R 0 1
+ ... H 0
+ ... CX 0 1
+ ... M 0 1
+ ... DETECTOR rec[-1] rec[-2]
+ ... """);
+ >>> qasm = circuit.to_qasm();
+ >>> print(qasm.strip().replace('\n\n', '\n'))
+ OPENQASM 3.0;
+ include "stdgates.inc";
+ qubit q[2];
+ bit m[2];
+ bit dets[1];
+ reset q[0];
+ reset q[1];
+ h q[0];
+ cx q[0], q[1];
+ measure q[0] -> m[0];
+ measure q[1] -> m[1];
+ dets[0] = rec[1] ^ rec[0] ^ 0;
+ )DOC")
+ .data());
+
c.def(
"__len__",
[](const Circuit &self) {
diff --git a/src/stim/circuit/circuit_pybind_test.py b/src/stim/circuit/circuit_pybind_test.py
index 70772b4cc..8d369b5ac 100644
--- a/src/stim/circuit/circuit_pybind_test.py
+++ b/src/stim/circuit/circuit_pybind_test.py
@@ -1573,3 +1573,63 @@ def test_detslice_filter_coords_flexibility():
assert str(d1) == str(d3)
assert str(d1) == str(d4)
assert str(d1) == str(d5)
+
+
+def test_to_qasm():
+ c = stim.Circuit("""
+ RX 0 1
+ TICK
+ H 1
+ CX 0 1
+ TICK
+ M 0 1
+ DETECTOR rec[-1] rec[-2]
+ C_XYZ 0
+ """)
+
+ assert c.to_qasm().strip() == """
+OPENQASM 3.0;
+
+include "stdgates.inc";
+gate cxyz q0 { U(pi/2, 0, pi/2) q0; }
+def rx(qubit q0) { reset q0; h q0; }
+
+qubit q[2];
+bit m[2];
+bit dets[1];
+
+rx(q[0]);
+rx(q[1]);
+barrier q;
+
+h q[1];
+cx q[0], q[1];
+barrier q;
+
+measure q[0] -> m[0];
+measure q[1] -> m[1];
+dets[0] = rec[1] ^ rec[0] ^ 0;
+cxyz q[0];
+ """.strip()
+
+ assert c.to_qasm(open_qasm_version=2, skip_dets_and_obs=True).strip() == """
+OPENQASM 2.0;
+
+include "qelib1.inc";
+gate cxyz q0 { U(pi/2, 0, pi/2) q0; }
+
+qreg q[2];
+creg m[2];
+
+reset q[0]; h q[0]; // decomposed RX
+reset q[1]; h q[1]; // decomposed RX
+barrier q;
+
+h q[1];
+cx q[0], q[1];
+barrier q;
+
+measure q[0] -> m[0];
+measure q[1] -> m[1];
+cxyz q[0];
+ """.strip()
diff --git a/src/stim/circuit/export_qasm.cc b/src/stim/circuit/export_qasm.cc
new file mode 100644
index 000000000..019fcda3c
--- /dev/null
+++ b/src/stim/circuit/export_qasm.cc
@@ -0,0 +1,530 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "stim/circuit/export_qasm.h"
+
+#include
+
+#include "stim/simulators/tableau_simulator.h"
+
+using namespace stim;
+
+static void do_decomposed(
+ GateType g, const char *q0_name, const char *q1_name, const char *m_name, std::ostream &out) {
+ auto q2n = [&](GateTarget t) {
+ return t.qubit_value() == 0 ? q0_name : q1_name;
+ };
+ bool first = true;
+ for (const auto &inst : Circuit(GATE_DATA[g].h_s_cx_m_r_decomposition).operations) {
+ switch (inst.gate_type) {
+ case GateType::S:
+ for (const auto &t : inst.targets) {
+ if (!first) {
+ out << " ";
+ }
+ first = false;
+ out << "s " << q2n(t) << ";";
+ }
+ break;
+ case GateType::H:
+ for (const auto &t : inst.targets) {
+ if (!first) {
+ out << " ";
+ }
+ first = false;
+ out << "h " << q2n(t) << ";";
+ }
+ break;
+ case GateType::R:
+ for (const auto &t : inst.targets) {
+ if (!first) {
+ out << " ";
+ }
+ first = false;
+ out << "reset " << q2n(t) << ";";
+ }
+ break;
+ case GateType::CX:
+ for (size_t k = 0; k < inst.targets.size(); k += 2) {
+ if (!first) {
+ out << " ";
+ }
+ first = false;
+ auto t1 = inst.targets[k];
+ auto t2 = inst.targets[k + 1];
+ out << "cx " << q2n(t1) << ", " << q2n(t2) << ";";
+ }
+ break;
+ case GateType::M:
+ for (const auto &t : inst.targets) {
+ if (!first) {
+ out << " ";
+ }
+ first = false;
+ out << "measure " << q2n(t) << " -> " << m_name << ";";
+ }
+ break;
+ default:
+ throw std::invalid_argument("Unhandled: " + inst.str());
+ }
+ }
+}
+
+static void do_qasm_decompose_mpp(
+ const CircuitInstruction &inst, size_t num_qubits, uint64_t &measurement_offset, std::ostream &out) {
+ decompose_mpp_operation(
+ inst,
+ num_qubits,
+ [&](const CircuitInstruction &h_xz,
+ const CircuitInstruction &h_yz,
+ const CircuitInstruction &cnot,
+ const CircuitInstruction &meas) {
+ for (const auto t : h_xz.targets) {
+ out << "h q[" << t.qubit_value() << "];";
+ }
+ for (const auto t : h_yz.targets) {
+ out << "sx q[" << t.qubit_value() << "];";
+ }
+ for (size_t k = 0; k < cnot.targets.size(); k += 2) {
+ auto t1 = cnot.targets[k];
+ auto t2 = cnot.targets[k + 1];
+ out << "cx q[" << t1.qubit_value() << "],q[" << t2.qubit_value() << "];";
+ }
+ for (auto t : meas.targets) {
+ out << "measure q[" << t.qubit_value() << "] -> m[" << measurement_offset << "];";
+ measurement_offset++;
+ }
+ for (size_t k = 0; k < cnot.targets.size(); k += 2) {
+ auto t1 = cnot.targets[k];
+ auto t2 = cnot.targets[k + 1];
+ out << "cx q[" << t1.qubit_value() << "],q[" << t2.qubit_value() << "];";
+ }
+ for (const auto t : h_yz.targets) {
+ out << "sxdg q[" << t.qubit_value() << "];";
+ }
+ for (const auto t : h_xz.targets) {
+ out << "h q[" << t.qubit_value() << "];";
+ }
+ out << " // decomposed MPP\n";
+ });
+}
+
+static void qasm_output_decomposed_inline(
+ const CircuitInstruction &instruction,
+ const std::array &qasm_names,
+ uint64_t &measurement_offset,
+ std::ostream &out,
+ bool decompose_inline) {
+ auto f = GATE_DATA[instruction.gate_type].flags;
+ std::stringstream q0;
+ std::stringstream q1;
+ std::stringstream m;
+ auto step = (f & GATE_TARGETS_PAIRS) ? 2 : 1;
+ for (size_t k = 0; k < instruction.targets.size(); k += step) {
+ auto t0 = instruction.targets[k];
+ auto t1 = instruction.targets[k + step - 1];
+ if (decompose_inline) {
+ q0.str("");
+ q1.str("");
+ q0 << "q[" << t0.qubit_value() << "]";
+ q1 << "q[" << t1.qubit_value() << "]";
+ if (f & GATE_PRODUCES_RESULTS) {
+ m.str("");
+ m << "m[" << measurement_offset << "]";
+ measurement_offset++;
+ }
+ do_decomposed(
+ instruction.gate_type, q0.str().data(), q1.str().data(), m.str().data(), out);
+ out << " // decomposed " << GATE_DATA[instruction.gate_type].name << "\n";
+ } else {
+ if (f & GATE_PRODUCES_RESULTS) {
+ out << "m[" << measurement_offset << "] = ";
+ measurement_offset++;
+ }
+ out << qasm_names[(int)instruction.gate_type] << "(";
+ out << "q[" << t0.qubit_value() << "]";
+ if (step == 2) {
+ out << ", q[" << t1.qubit_value() << "]";
+ }
+ out << ");\n";
+ }
+ }
+}
+
+static void qasm_output_two_qubit_unitary(
+ const CircuitInstruction &instruction,
+ const std::array &qasm_names,
+ uint64_t &measurement_offset,
+ int open_qasm_version,
+ std::ostream &out) {
+ for (size_t k = 0; k < instruction.targets.size(); k += 2) {
+ auto t1 = instruction.targets[k];
+ auto t2 = instruction.targets[k + 1];
+ if (t1.is_qubit_target() && t2.is_qubit_target()) {
+ out << qasm_names[(int)instruction.gate_type] << " q[" << t1.qubit_value() << "], q[" << t2.qubit_value()
+ << "];\n";
+ } else if (t1.is_qubit_target() || t2.is_qubit_target()) {
+ if (open_qasm_version == 2) {
+ throw std::invalid_argument(
+ "The circuit contains feedback, but OPENQASM 2 doesn't support feedback.\n"
+ "You can use `stim.Circuit.with_inlined_feedback` to drop feedback operations.\n"
+ "Alternatively, pass the argument `open_qasm_version=3`.");
+ }
+ GateTarget control;
+ GateTarget target;
+ char basis;
+ switch (instruction.gate_type) {
+ case GateType::CX:
+ basis = 'X';
+ control = t1;
+ target = t2;
+ break;
+ case GateType::CY:
+ basis = 'Y';
+ control = t1;
+ target = t2;
+ break;
+ case GateType::CZ:
+ basis = 'Z';
+ control = t1;
+ target = t2;
+ if (control.is_qubit_target()) {
+ std::swap(control, target);
+ }
+ break;
+ case GateType::XCZ:
+ basis = 'X';
+ control = t2;
+ target = t1;
+ break;
+ case GateType::YCZ:
+ basis = 'Y';
+ control = t2;
+ target = t1;
+ break;
+ default:
+ throw std::invalid_argument("Not implemented: " + instruction.str());
+ }
+ out << "if (";
+ if (t1.is_measurement_record_target()) {
+ out << "ms[" << (measurement_offset + t1.rec_offset()) << "]";
+ } else if (t1.is_sweep_bit_target()) {
+ out << "sweep[" << t1.value() << "]";
+ } else {
+ throw std::invalid_argument("Not implemented: " + instruction.str());
+ }
+ out << ") {\n";
+ out << " " << basis << " q[" << target.qubit_value() << "];\n";
+ out << "}\n";
+ }
+ }
+}
+
+static void define_custom_single_qubit_gate(
+ GateType g,
+ const char *name,
+ std::array &qasm_names,
+ const std::bitset &used,
+ std::ostream &out) {
+ const auto &gate = GATE_DATA[g];
+ qasm_names[(int)g] = name;
+ if (!used[(int)g]) {
+ return;
+ }
+
+ out << "gate " << name << " q0 { U(";
+ auto xyz = gate.to_euler_angles();
+ std::array angles{"0", "pi/2", "pi", "-pi/2"};
+ out << angles[round(xyz[0] / 3.14159265359f)];
+ out << ", " << angles[round(xyz[1] / 3.14159265359f)];
+ out << ", " << angles[round(xyz[2] / 3.14159265359f)];
+ out << ") q0; }\n";
+}
+
+static void define_custom_decomposed_gate(
+ GateType g,
+ const char *name,
+ std::array &qasm_names,
+ const std::bitset &used,
+ std::ostream &out,
+ int open_qasm_version) {
+ const auto &gate = GATE_DATA[g];
+ qasm_names[(int)g] = name;
+ if (!used[(int)g]) {
+ return;
+ }
+
+ Circuit c(gate.h_s_cx_m_r_decomposition);
+ bool is_unitary = true;
+ for (const auto &inst : c.operations) {
+ is_unitary &= (GATE_DATA[inst.gate_type].flags & GATE_IS_UNITARY) != 0;
+ }
+ auto num_measurements = c.count_measurements();
+ if (is_unitary) {
+ out << "gate " << name;
+ out << " q0";
+ if (gate.flags & GateFlags::GATE_TARGETS_PAIRS) {
+ out << ", q1";
+ }
+ out << " { ";
+ } else {
+ if (open_qasm_version == 2) {
+ // Have to decompose inline in the circuit.
+ return;
+ }
+ out << "def " << name << "(qubit q0";
+ if (gate.flags & GateFlags::GATE_TARGETS_PAIRS) {
+ out << ", qubit q1";
+ }
+ out << ")";
+ if (num_measurements > 1) {
+ throw std::invalid_argument("Multiple measurement gates not supported.");
+ } else if (num_measurements == 1) {
+ out << " -> bit { bit b; ";
+ } else {
+ out << " { ";
+ }
+ }
+
+ do_decomposed(g, "q0", "q1", "b", out);
+ if (num_measurements > 0) {
+ out << " return b;";
+ }
+ out << " }\n";
+}
+
+static void collect_used_gates(const Circuit &c, std::bitset &used) {
+ for (const auto &inst : c.operations) {
+ used[(int)inst.gate_type] = true;
+ if (inst.gate_type == GateType::REPEAT) {
+ collect_used_gates(inst.repeat_block_body(c), used);
+ }
+ }
+}
+
+static std::array define_qasm_gates(
+ std::ostream &out, const std::bitset &used, int open_qasm_version) {
+ std::array qasm_names;
+
+ if (open_qasm_version == 2) {
+ out << "include \"qelib1.inc\";\n";
+ } else if (open_qasm_version == 3) {
+ out << "include \"stdgates.inc\";\n";
+ } else {
+ throw std::invalid_argument("Unrecognized open_qasm_version.");
+ }
+ qasm_names[(int)GateType::I] = "id";
+ qasm_names[(int)GateType::X] = "x";
+ qasm_names[(int)GateType::Y] = "y";
+ qasm_names[(int)GateType::Z] = "z";
+ qasm_names[(int)GateType::SQRT_X] = "sx";
+ qasm_names[(int)GateType::SQRT_X_DAG] = "sxdg";
+ qasm_names[(int)GateType::S] = "s";
+ qasm_names[(int)GateType::S_DAG] = "sdg";
+ qasm_names[(int)GateType::CX] = "cx";
+ qasm_names[(int)GateType::CY] = "cy";
+ qasm_names[(int)GateType::CZ] = "cz";
+ qasm_names[(int)GateType::SWAP] = "swap";
+ qasm_names[(int)GateType::H] = "h";
+ define_custom_single_qubit_gate(GateType::C_XYZ, "cxyz", qasm_names, used, out);
+ define_custom_single_qubit_gate(GateType::C_ZYX, "czyx", qasm_names, used, out);
+ define_custom_single_qubit_gate(GateType::SQRT_Y, "sy", qasm_names, used, out);
+ define_custom_single_qubit_gate(GateType::SQRT_Y_DAG, "sydg", qasm_names, used, out);
+ define_custom_single_qubit_gate(GateType::H_XY, "hxy", qasm_names, used, out);
+ define_custom_single_qubit_gate(GateType::H_YZ, "hyz", qasm_names, used, out);
+ define_custom_decomposed_gate(GateType::XCX, "xcx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::XCY, "xcy", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::XCZ, "xcz", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::YCX, "ycx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::YCY, "ycy", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::YCZ, "ycz", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SQRT_XX, "sxx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SQRT_XX_DAG, "sxxdg", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SQRT_YY, "syy", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SQRT_YY_DAG, "syydg", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SQRT_ZZ, "szz", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SQRT_ZZ_DAG, "szzdg", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::ISWAP, "iswap", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::CXSWAP, "cxswap", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::SWAPCX, "swapcx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::ISWAP_DAG, "iswapdg", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MX, "mx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MY, "my", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MRX, "mrx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MRY, "mry", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MR, "mr", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::RX, "rx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::RY, "ry", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MXX, "mxx", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MYY, "myy", qasm_names, used, out, open_qasm_version);
+ define_custom_decomposed_gate(GateType::MZZ, "mzz", qasm_names, used, out, open_qasm_version);
+ out << "\n";
+
+ return qasm_names;
+}
+
+void stim::export_open_qasm(const Circuit &circuit, std::ostream &out, int open_qasm_version, bool skip_dets_and_obs) {
+ auto stats = circuit.compute_stats();
+ std::bitset used;
+ collect_used_gates(circuit, used);
+ if (open_qasm_version != 2 && open_qasm_version != 3) {
+ throw std::invalid_argument("Only open_qasm_version=2 and open_qasm_version=3 are supported.");
+ }
+
+ // Header.
+ if (open_qasm_version == 2) {
+ out << "OPENQASM 2.0;\n";
+ } else {
+ out << "OPENQASM 3.0;\n";
+ }
+ out << "\n";
+
+ // Gate definitions.
+ std::array qasm_names = define_qasm_gates(out, used, open_qasm_version);
+
+ // Storage.
+ const char *qubit_decl = "qubit";
+ const char *bit_decl = "bit";
+ if (open_qasm_version == 2) {
+ qubit_decl = "qreg";
+ bit_decl = "creg";
+ }
+ if (stats.num_qubits > 0) {
+ out << qubit_decl << " q[" << stats.num_qubits << "];\n";
+ }
+ if (stats.num_measurements > 0) {
+ out << bit_decl << " m[" << stats.num_measurements << "];\n";
+ }
+ if (stats.num_detectors > 0 && !skip_dets_and_obs) {
+ out << bit_decl << " dets[" << stats.num_detectors << "];\n";
+ }
+ if (stats.num_observables > 0 && !skip_dets_and_obs) {
+ out << bit_decl << " obs[" << stats.num_observables << "];\n";
+ }
+ if (stats.num_sweep_bits > 0) {
+ out << bit_decl << " sweep[" << stats.num_sweep_bits << "];\n";
+ }
+ out << "\n";
+
+ simd_bits<64> reference_sample(stats.num_measurements);
+ if (stats.num_detectors > 0 || stats.num_observables > 0) {
+ reference_sample = TableauSimulator<64>::reference_sample_circuit(circuit);
+ }
+
+ // Body.
+ uint64_t measurement_offset = 0;
+ uint64_t detector_offset = 0;
+ circuit.for_each_operation([&](const CircuitInstruction &instruction) {
+ GateFlags f = GATE_DATA[instruction.gate_type].flags;
+
+ switch (instruction.gate_type) {
+ case GateType::QUBIT_COORDS:
+ case GateType::SHIFT_COORDS:
+ // Skipped.
+ return;
+
+ case GateType::MPAD:
+ measurement_offset += instruction.count_measurement_results();
+ return;
+
+ case GateType::TICK:
+ out << "barrier q;\n\n";
+ return;
+
+ case GateType::M:
+ for (const auto &t : instruction.targets) {
+ out << "measure q[" << t.qubit_value() << "] -> m[" << measurement_offset << "];\n";
+ measurement_offset++;
+ }
+ return;
+ case GateType::R:
+ for (const auto &t : instruction.targets) {
+ out << "reset q[" << t.qubit_value() << "];\n";
+ }
+ return;
+ case GateType::DETECTOR:
+ case GateType::OBSERVABLE_INCLUDE: {
+ if (skip_dets_and_obs) {
+ return;
+ }
+ if (open_qasm_version == 2) {
+ throw std::invalid_argument(
+ "The circuit contains detectors or observables, but OPENQASM 2 doesn't support the operations "
+ "needed for accumulating detector and observable values.\n"
+ "To simply ignore detectors and observables, pass the argument `skip_dets_and_obs=True`.\n"
+ "Alternatively, pass the argument `open_qasm_version=3`.");
+ }
+ if (instruction.gate_type == GateType::DETECTOR) {
+ out << "dets[" << detector_offset << "] = ";
+ detector_offset++;
+ } else {
+ out << "obs[" << (int)instruction.args[0] << "] = obs[" << (int)instruction.args[0] << "] ^ ";
+ }
+
+ int ref_value = 0;
+ for (auto t : instruction.targets) {
+ assert(t.is_measurement_record_target());
+ auto i = measurement_offset + t.rec_offset();
+ ref_value ^= reference_sample[i];
+ out << "rec[" << (measurement_offset + t.rec_offset()) << "] ^ ";
+ }
+ out << ref_value << ";\n";
+ return;
+ }
+
+ case GateType::DEPOLARIZE1:
+ case GateType::DEPOLARIZE2:
+ case GateType::X_ERROR:
+ case GateType::Y_ERROR:
+ case GateType::Z_ERROR:
+ case GateType::PAULI_CHANNEL_1:
+ case GateType::PAULI_CHANNEL_2:
+ case GateType::E:
+ case GateType::ELSE_CORRELATED_ERROR:
+ case GateType::HERALDED_ERASE:
+ case GateType::HERALDED_PAULI_CHANNEL_1:
+ throw std::invalid_argument(
+ "The circuit contains noise, but OPENQASM 2 doesn't support noise operations.\n"
+ "Use `stim.Circuit.without_noise` to get a version of the circuit without noise.");
+
+ case GateType::MPP:
+ do_qasm_decompose_mpp(instruction, stats.num_qubits, measurement_offset, out);
+ return;
+
+ default:
+ break;
+ }
+
+ if (f & (stim::GATE_IS_RESET | stim::GATE_PRODUCES_RESULTS)) {
+ qasm_output_decomposed_inline(instruction, qasm_names, measurement_offset, out, open_qasm_version == 2);
+ return;
+ }
+
+ if (f & stim::GATE_IS_UNITARY) {
+ if (f & stim::GATE_IS_SINGLE_QUBIT_GATE) {
+ for (const auto &t : instruction.targets) {
+ assert(t.is_qubit_target());
+ out << qasm_names[(int)instruction.gate_type] << " q[" << t.qubit_value() << "];\n";
+ }
+ return;
+ }
+ if (f & stim::GATE_TARGETS_PAIRS) {
+ qasm_output_two_qubit_unitary(instruction, qasm_names, measurement_offset, skip_dets_and_obs, out);
+ return;
+ }
+ }
+
+ throw std::invalid_argument("Not implemented: " + instruction.str());
+ });
+}
diff --git a/src/stim/circuit/export_qasm.h b/src/stim/circuit/export_qasm.h
new file mode 100644
index 000000000..01454325d
--- /dev/null
+++ b/src/stim/circuit/export_qasm.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _STIM_CIRCUIT_EXPORT_CIRCUIT_H
+#define _STIM_CIRCUIT_EXPORT_CIRCUIT_H
+
+#include "stim/circuit/circuit.h"
+
+namespace stim {
+
+void export_open_qasm(const Circuit &circuit, std::ostream &out, int open_qasm_version, bool skip_dets_and_obs);
+
+} // namespace stim
+
+#endif
diff --git a/src/stim/circuit/export_qasm.test.cc b/src/stim/circuit/export_qasm.test.cc
new file mode 100644
index 000000000..07b25b004
--- /dev/null
+++ b/src/stim/circuit/export_qasm.test.cc
@@ -0,0 +1,482 @@
+#include "stim/circuit/export_qasm.h"
+
+#include "gtest/gtest.h"
+
+#include "stim/circuit/circuit.test.h"
+#include "stim/simulators/transform_without_feedback.h"
+
+using namespace stim;
+
+TEST(export_circuit, export_open_qasm_feedback) {
+ Circuit c(R"CIRCUIT(
+ H 0
+ CX 0 1
+ C_XYZ 1
+ M 0
+ CX rec[-1] 1
+ CX sweep[5] 1
+ TICK
+ H 0
+ )CIRCUIT");
+ std::stringstream out;
+ export_open_qasm(c, out, 3, false);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0;
+
+include "stdgates.inc";
+gate cxyz q0 { U(pi/2, 0, pi/2) q0; }
+
+qubit q[2];
+bit m[1];
+bit sweep[6];
+
+h q[0];
+cx q[0], q[1];
+cxyz q[1];
+measure q[0] -> m[0];
+if (ms[0]) {
+ X q[1];
+}
+if (sweep[5]) {
+ X q[1];
+}
+barrier q;
+
+h q[0];
+)QASM");
+}
+
+TEST(export_circuit, export_open_qasm_qec) {
+ Circuit c(R"CIRCUIT(
+ R 0 1 2
+ TICK
+ X 0
+ TICK
+ CX 0 1
+ TICK
+ CX 2 1
+ TICK
+ M 1
+ DETECTOR rec[-1]
+ TICK
+ R 1
+ TICK
+ CX 0 1
+ TICK
+ CX 2 1
+ TICK
+ M 0 1 2
+ DETECTOR rec[-2] rec[-4]
+ DETECTOR rec[-1] rec[-2] rec[-3]
+ OBSERVABLE_INCLUDE(0) rec[-1]
+ )CIRCUIT");
+
+ std::stringstream out;
+ export_open_qasm(c, out, 3, false);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0;
+
+include "stdgates.inc";
+
+qubit q[3];
+bit m[4];
+bit dets[3];
+bit obs[1];
+
+reset q[0];
+reset q[1];
+reset q[2];
+barrier q;
+
+x q[0];
+barrier q;
+
+cx q[0], q[1];
+barrier q;
+
+cx q[2], q[1];
+barrier q;
+
+measure q[1] -> m[0];
+dets[0] = rec[0] ^ 1;
+barrier q;
+
+reset q[1];
+barrier q;
+
+cx q[0], q[1];
+barrier q;
+
+cx q[2], q[1];
+barrier q;
+
+measure q[0] -> m[1];
+measure q[1] -> m[2];
+measure q[2] -> m[3];
+dets[1] = rec[2] ^ rec[0] ^ 0;
+dets[2] = rec[3] ^ rec[2] ^ rec[1] ^ 0;
+obs[0] = obs[0] ^ rec[3] ^ 0;
+)QASM");
+
+ out.str("");
+ export_open_qasm(c, out, 2, true);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0;
+
+include "qelib1.inc";
+
+qreg q[3];
+creg m[4];
+
+reset q[0];
+reset q[1];
+reset q[2];
+barrier q;
+
+x q[0];
+barrier q;
+
+cx q[0], q[1];
+barrier q;
+
+cx q[2], q[1];
+barrier q;
+
+measure q[1] -> m[0];
+barrier q;
+
+reset q[1];
+barrier q;
+
+cx q[0], q[1];
+barrier q;
+
+cx q[2], q[1];
+barrier q;
+
+measure q[0] -> m[1];
+measure q[1] -> m[2];
+measure q[2] -> m[3];
+)QASM");
+}
+
+TEST(export_circuit, export_qasm_decomposed_operations) {
+ Circuit c(R"CIRCUIT(
+ R 3
+ RX 0 1
+ MX 2
+ TICK
+
+ MXX 0 1
+ DETECTOR rec[-1]
+ TICK
+
+ M 2
+ MR 3
+ MRX 4
+ )CIRCUIT");
+
+ std::stringstream out;
+ export_open_qasm(c, out, 3, false);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0;
+
+include "stdgates.inc";
+def mx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; h q0; return b; }
+def mrx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; reset q0; h q0; return b; }
+def mr(qubit q0) -> bit { bit b; measure q0 -> b; reset q0; return b; }
+def rx(qubit q0) { reset q0; h q0; }
+def mxx(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; h q0; measure q0 -> b; h q0; cx q0, q1; return b; }
+
+qubit q[5];
+bit m[5];
+bit dets[1];
+
+reset q[3];
+rx(q[0]);
+rx(q[1]);
+m[0] = mx(q[2]);
+barrier q;
+
+m[1] = mxx(q[0], q[1]);
+dets[0] = rec[1] ^ 0;
+barrier q;
+
+measure q[2] -> m[2];
+m[3] = mr(q[3]);
+m[4] = mrx(q[4]);
+)QASM");
+
+ out.str("");
+ export_open_qasm(c, out, 2, true);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0;
+
+include "qelib1.inc";
+
+qreg q[5];
+creg m[5];
+
+reset q[3];
+reset q[0]; h q[0]; // decomposed RX
+reset q[1]; h q[1]; // decomposed RX
+h q[2]; measure q[2] -> m[0]; h q[2]; // decomposed MX
+barrier q;
+
+cx q[0], q[1]; h q[0]; measure q[0] -> m[1]; h q[0]; cx q[0], q[1]; // decomposed MXX
+barrier q;
+
+measure q[2] -> m[2];
+measure q[3] -> m[3]; reset q[3]; // decomposed MR
+h q[4]; measure q[4] -> m[4]; reset q[4]; h q[4]; // decomposed MRX
+)QASM");
+}
+
+TEST(export_circuit, export_qasm_all_operations) {
+ Circuit c = generate_test_circuit_with_all_operations();
+ c = c.without_noise();
+ c = circuit_with_inlined_feedback(c);
+
+ std::stringstream out;
+ export_open_qasm(c, out, 3, false);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 3.0;
+
+include "stdgates.inc";
+gate cxyz q0 { U(pi/2, 0, pi/2) q0; }
+gate czyx q0 { U(pi/2, pi/2, pi/2) q0; }
+gate sy q0 { U(pi/2, 0, 0) q0; }
+gate sydg q0 { U(pi/2, pi/2, pi/2) q0; }
+gate hxy q0 { U(pi/2, 0, pi/2) q0; }
+gate hyz q0 { U(pi/2, pi/2, pi/2) q0; }
+gate xcx q0, q1 { h q0; cx q0, q1; h q0; }
+gate xcy q0, q1 { h q0; s q1; s q1; s q1; cx q0, q1; h q0; s q1; }
+gate xcz q0, q1 { cx q1, q0; }
+gate ycx q0, q1 { s q0; s q0; s q0; h q1; cx q1, q0; s q0; h q1; }
+gate ycy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q0; s q0; s q1; }
+gate ycz q0, q1 { s q0; s q0; s q0; cx q1, q0; s q0; }
+gate sxx q0, q1 { h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; }
+gate sxxdg q0, q1 { h q0; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; h q0; h q1; }
+gate syy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; }
+gate syydg q0, q1 { s q0; s q0; s q0; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; s q1; s q1; }
+gate szz q0, q1 { h q1; cx q0, q1; h q1; s q0; s q1; }
+gate szzdg q0, q1 { h q1; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; }
+gate iswap q0, q1 { h q0; cx q0, q1; cx q1, q0; h q1; s q1; s q0; }
+gate cxswap q0, q1 { cx q1, q0; cx q0, q1; }
+gate swapcx q0, q1 { cx q0, q1; cx q1, q0; }
+gate iswapdg q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q1; cx q1, q0; cx q0, q1; h q0; }
+def mx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; h q0; return b; }
+def my(qubit q0) -> bit { bit b; s q0; s q0; s q0; h q0; measure q0 -> b; h q0; s q0; return b; }
+def mrx(qubit q0) -> bit { bit b; h q0; measure q0 -> b; reset q0; h q0; return b; }
+def mry(qubit q0) -> bit { bit b; s q0; s q0; s q0; h q0; measure q0 -> b; reset q0; h q0; s q0; return b; }
+def mr(qubit q0) -> bit { bit b; measure q0 -> b; reset q0; return b; }
+def rx(qubit q0) { reset q0; h q0; }
+def ry(qubit q0) { reset q0; h q0; s q0; }
+def mxx(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; h q0; measure q0 -> b; h q0; cx q0, q1; return b; }
+def myy(qubit q0, qubit q1) -> bit { bit b; s q0; s q1; cx q0, q1; h q0; measure q0 -> b; s q1; s q1; h q0; cx q0, q1; s q0; s q1; return b; }
+def mzz(qubit q0, qubit q1) -> bit { bit b; cx q0, q1; measure q1 -> b; cx q0, q1; return b; }
+
+qubit q[18];
+bit m[20];
+bit dets[1];
+bit obs[1];
+
+id q[0];
+x q[1];
+y q[2];
+z q[3];
+barrier q;
+
+cxyz q[0];
+czyx q[1];
+hxy q[2];
+h q[3];
+hyz q[4];
+sx q[0];
+sxdg q[1];
+sy q[2];
+sydg q[3];
+s q[4];
+sdg q[5];
+barrier q;
+
+cxswap q[0], q[1];
+iswap q[2], q[3];
+iswapdg q[4], q[5];
+swap q[6], q[7];
+swapcx q[8], q[9];
+sxx q[0], q[1];
+sxxdg q[2], q[3];
+syy q[4], q[5];
+syydg q[6], q[7];
+szz q[8], q[9];
+szzdg q[10], q[11];
+xcx q[0], q[1];
+xcy q[2], q[3];
+xcz q[4], q[5];
+ycx q[6], q[7];
+ycy q[8], q[9];
+ycz q[10], q[11];
+cx q[12], q[13];
+cy q[14], q[15];
+cz q[16], q[17];
+barrier q;
+
+barrier q;
+
+h q[0];sx q[1];cx q[1],q[0];cx q[2],q[0];measure q[0] -> m[2];cx q[1],q[0];cx q[2],q[0];sxdg q[1];h q[0]; // decomposed MPP
+cx q[1],q[0];measure q[0] -> m[3];cx q[1],q[0]; // decomposed MPP
+m[4] = mrx(q[0]);
+m[5] = mry(q[1]);
+m[6] = mr(q[2]);
+m[7] = mx(q[3]);
+m[8] = my(q[4]);
+measure q[5] -> m[9];
+measure q[6] -> m[10];
+rx(q[7]);
+ry(q[8]);
+reset q[9];
+barrier q;
+
+m[11] = mxx(q[0], q[1]);
+m[12] = mxx(q[2], q[3]);
+m[13] = myy(q[4], q[5]);
+m[14] = mzz(q[6], q[7]);
+barrier q;
+
+h q[0];
+cx q[0], q[1];
+s q[1];
+barrier q;
+
+h q[0];
+cx q[0], q[1];
+s q[1];
+barrier q;
+
+h q[0];
+cx q[0], q[1];
+s q[1];
+barrier q;
+
+barrier q;
+
+m[15] = mr(q[0]);
+m[16] = mr(q[0]);
+dets[0] = rec[16] ^ 0;
+obs[0] = obs[0] ^ rec[16] ^ 0;
+barrier q;
+
+)QASM");
+
+ out.str("");
+ export_open_qasm(c, out, 2, true);
+ ASSERT_EQ(out.str(), R"QASM(OPENQASM 2.0;
+
+include "qelib1.inc";
+gate cxyz q0 { U(pi/2, 0, pi/2) q0; }
+gate czyx q0 { U(pi/2, pi/2, pi/2) q0; }
+gate sy q0 { U(pi/2, 0, 0) q0; }
+gate sydg q0 { U(pi/2, pi/2, pi/2) q0; }
+gate hxy q0 { U(pi/2, 0, pi/2) q0; }
+gate hyz q0 { U(pi/2, pi/2, pi/2) q0; }
+gate xcx q0, q1 { h q0; cx q0, q1; h q0; }
+gate xcy q0, q1 { h q0; s q1; s q1; s q1; cx q0, q1; h q0; s q1; }
+gate xcz q0, q1 { cx q1, q0; }
+gate ycx q0, q1 { s q0; s q0; s q0; h q1; cx q1, q0; s q0; h q1; }
+gate ycy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q0; s q0; s q1; }
+gate ycz q0, q1 { s q0; s q0; s q0; cx q1, q0; s q0; }
+gate sxx q0, q1 { h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; }
+gate sxxdg q0, q1 { h q0; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; h q0; h q1; }
+gate syy q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; }
+gate syydg q0, q1 { s q0; s q0; s q0; s q1; h q0; cx q0, q1; h q1; s q0; s q1; h q0; h q1; s q0; s q1; s q1; s q1; }
+gate szz q0, q1 { h q1; cx q0, q1; h q1; s q0; s q1; }
+gate szzdg q0, q1 { h q1; cx q0, q1; h q1; s q0; s q0; s q0; s q1; s q1; s q1; }
+gate iswap q0, q1 { h q0; cx q0, q1; cx q1, q0; h q1; s q1; s q0; }
+gate cxswap q0, q1 { cx q1, q0; cx q0, q1; }
+gate swapcx q0, q1 { cx q0, q1; cx q1, q0; }
+gate iswapdg q0, q1 { s q0; s q0; s q0; s q1; s q1; s q1; h q1; cx q1, q0; cx q0, q1; h q0; }
+
+qreg q[18];
+creg m[20];
+
+id q[0];
+x q[1];
+y q[2];
+z q[3];
+barrier q;
+
+cxyz q[0];
+czyx q[1];
+hxy q[2];
+h q[3];
+hyz q[4];
+sx q[0];
+sxdg q[1];
+sy q[2];
+sydg q[3];
+s q[4];
+sdg q[5];
+barrier q;
+
+cxswap q[0], q[1];
+iswap q[2], q[3];
+iswapdg q[4], q[5];
+swap q[6], q[7];
+swapcx q[8], q[9];
+sxx q[0], q[1];
+sxxdg q[2], q[3];
+syy q[4], q[5];
+syydg q[6], q[7];
+szz q[8], q[9];
+szzdg q[10], q[11];
+xcx q[0], q[1];
+xcy q[2], q[3];
+xcz q[4], q[5];
+ycx q[6], q[7];
+ycy q[8], q[9];
+ycz q[10], q[11];
+cx q[12], q[13];
+cy q[14], q[15];
+cz q[16], q[17];
+barrier q;
+
+barrier q;
+
+h q[0];sx q[1];cx q[1],q[0];cx q[2],q[0];measure q[0] -> m[2];cx q[1],q[0];cx q[2],q[0];sxdg q[1];h q[0]; // decomposed MPP
+cx q[1],q[0];measure q[0] -> m[3];cx q[1],q[0]; // decomposed MPP
+h q[0]; measure q[0] -> m[4]; reset q[0]; h q[0]; // decomposed MRX
+s q[1]; s q[1]; s q[1]; h q[1]; measure q[1] -> m[5]; reset q[1]; h q[1]; s q[1]; // decomposed MRY
+measure q[2] -> m[6]; reset q[2]; // decomposed MR
+h q[3]; measure q[3] -> m[7]; h q[3]; // decomposed MX
+s q[4]; s q[4]; s q[4]; h q[4]; measure q[4] -> m[8]; h q[4]; s q[4]; // decomposed MY
+measure q[5] -> m[9];
+measure q[6] -> m[10];
+reset q[7]; h q[7]; // decomposed RX
+reset q[8]; h q[8]; s q[8]; // decomposed RY
+reset q[9];
+barrier q;
+
+cx q[0], q[1]; h q[0]; measure q[0] -> m[11]; h q[0]; cx q[0], q[1]; // decomposed MXX
+cx q[2], q[3]; h q[2]; measure q[2] -> m[12]; h q[2]; cx q[2], q[3]; // decomposed MXX
+s q[4]; s q[5]; cx q[4], q[5]; h q[4]; measure q[4] -> m[13]; s q[5]; s q[5]; h q[4]; cx q[4], q[5]; s q[4]; s q[5]; // decomposed MYY
+cx q[6], q[7]; measure q[7] -> m[14]; cx q[6], q[7]; // decomposed MZZ
+barrier q;
+
+h q[0];
+cx q[0], q[1];
+s q[1];
+barrier q;
+
+h q[0];
+cx q[0], q[1];
+s q[1];
+barrier q;
+
+h q[0];
+cx q[0], q[1];
+s q[1];
+barrier q;
+
+barrier q;
+
+measure q[0] -> m[15]; reset q[0]; // decomposed MR
+measure q[0] -> m[16]; reset q[0]; // decomposed MR
+barrier q;
+
+)QASM");
+}
diff --git a/src/stim/circuit/gate_data_collapsing.cc b/src/stim/circuit/gate_data_collapsing.cc
index 3f9a8b896..9eb273a41 100644
--- a/src/stim/circuit/gate_data_collapsing.cc
+++ b/src/stim/circuit/gate_data_collapsing.cc
@@ -379,7 +379,6 @@ Parens Arguments:
.unitary_data = {},
.flow_data = {"1 -> +X"},
.h_s_cx_m_r_decomposition = R"CIRCUIT(
-H 0
R 0
H 0
)CIRCUIT",
@@ -417,10 +416,6 @@ Parens Arguments:
.unitary_data = {},
.flow_data = {"1 -> +Y"},
.h_s_cx_m_r_decomposition = R"CIRCUIT(
-S 0
-S 0
-S 0
-H 0
R 0
H 0
S 0