-
-
Notifications
You must be signed in to change notification settings - Fork 18
Implementation of reading direct qasm files #450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fd1b993
533faff
0a14411
b00541c
0936800
53b5509
9e773ec
0f9edbf
8a5f1a7
08e0ec8
7df4981
70e4204
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Copyright (c) 2025 - 2026 Chair for Design Automation, TUM | ||
| # All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # Licensed under the MIT License | ||
|
|
||
| """QASM loading utilities shared by the Simulator and EquivalenceChecker.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from qiskit import qasm2, qasm3 | ||
|
|
||
| if TYPE_CHECKING: | ||
| from qiskit.circuit import QuantumCircuit | ||
|
|
||
|
|
||
| def _first_non_comment_line(text: str) -> str: | ||
| """Return the first non-empty, non-comment line from QASM-like text. | ||
|
|
||
| Args: | ||
| text: Multiline string to scan. | ||
|
|
||
| Returns: | ||
| The first line that is not empty and does not start with ``//``, | ||
| or an empty string if no such line exists. | ||
| """ | ||
| for line in text.splitlines(): | ||
| stripped = line.strip() | ||
| if stripped and not stripped.startswith("//"): | ||
| return stripped | ||
| return "" | ||
|
|
||
|
|
||
| def load_circuit(circuit: QuantumCircuit | str | Path) -> QuantumCircuit: | ||
| """Load a QuantumCircuit from a QASM string, file path, or return it unchanged. | ||
|
|
||
| Args: | ||
| circuit: A ``QuantumCircuit``, a raw QASM string, or a path to a ``.qasm`` file. | ||
|
|
||
| Returns: | ||
| The corresponding ``QuantumCircuit``. | ||
| """ | ||
| if not isinstance(circuit, (str, Path)): | ||
| return circuit | ||
|
|
||
| if isinstance(circuit, str): | ||
| header = _first_non_comment_line(circuit) | ||
| if header.startswith("OPENQASM"): | ||
| if header.startswith("OPENQASM 3"): # pragma: no cover | ||
| return qasm3.loads(circuit) | ||
| return qasm2.loads(circuit) | ||
|
|
||
| path = Path(circuit) | ||
| content = path.read_text(encoding="utf-8") | ||
| if _first_non_comment_line(content).startswith("OPENQASM 3"): # pragma: no cover | ||
| return qasm3.load(str(path)) | ||
| return qasm2.load(str(path)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| OPENQASM 3.0; | ||
| include "stdgates.inc"; | ||
|
|
||
| qubit[2] q; | ||
| bit[2] c; | ||
|
|
||
| h q[0]; | ||
| cx q[0], q[1]; | ||
| c = measure q; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # Copyright (c) 2025 - 2026 Chair for Design Automation, TUM | ||
| # All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: MIT | ||
| # | ||
| # Licensed under the MIT License | ||
|
|
||
| """Tests for QASM loading utilities.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
| from qiskit import QuantumCircuit | ||
|
|
||
| from mqt.yaqs.digital.utils.qasm_utils import ( | ||
| _first_non_comment_line, # noqa: PLC2701 — tests exercise the private function directly | ||
| load_circuit, | ||
| ) | ||
|
|
||
| QASM2_STRING = """\ | ||
| OPENQASM 2.0; | ||
| include "qelib1.inc"; | ||
| qreg q[1]; | ||
| h q[0]; | ||
| """ | ||
|
|
||
| QASM3_STRING = """\ | ||
| OPENQASM 3.0; | ||
| qubit[1] q; | ||
| h q[0]; | ||
| """ | ||
|
|
||
|
|
||
| def test_first_non_comment_line_skips_comments() -> None: | ||
| """Lines starting with // are skipped; the first real line is returned.""" | ||
| text = "// comment\n// another\nOPENQASM 2.0;" | ||
| assert _first_non_comment_line(text) == "OPENQASM 2.0;" | ||
|
|
||
|
|
||
| def test_first_non_comment_line_empty_returns_empty() -> None: | ||
| """An all-comment or blank text returns an empty string.""" | ||
| assert not _first_non_comment_line("// only comments\n// still comments") | ||
| assert not _first_non_comment_line("") | ||
|
|
||
|
|
||
| def test_load_circuit_passthrough_quantum_circuit() -> None: | ||
| """A QuantumCircuit is returned unchanged.""" | ||
| qc = QuantumCircuit(1) | ||
| qc.h(0) | ||
| result = load_circuit(qc) | ||
| assert result is qc | ||
|
|
||
|
|
||
| def test_load_circuit_qasm2_string() -> None: | ||
| """A raw QASM 2 string is parsed and returned as a QuantumCircuit.""" | ||
| qc = load_circuit(QASM2_STRING) | ||
| assert isinstance(qc, QuantumCircuit) | ||
| assert qc.num_qubits == 1 | ||
|
|
||
|
|
||
| def test_load_circuit_qasm2_path_object() -> None: | ||
| """A QASM 2 file given as a Path is loaded and returned as a QuantumCircuit.""" | ||
| qasm_file = Path(__file__).parent.parent.parent / "circuit.qasm" | ||
| qc = load_circuit(qasm_file) | ||
| assert isinstance(qc, QuantumCircuit) | ||
|
|
||
|
|
||
| def test_load_circuit_qasm2_str_path() -> None: | ||
| """A QASM 2 file given as a str path is loaded and returned as a QuantumCircuit.""" | ||
| qasm_file = str(Path(__file__).parent.parent.parent / "circuit.qasm") | ||
| qc = load_circuit(qasm_file) | ||
| assert isinstance(qc, QuantumCircuit) | ||
|
|
||
|
|
||
| def test_load_circuit_qasm3_string() -> None: | ||
| """A raw QASM 3 string is parsed and returned as a QuantumCircuit.""" | ||
| pytest.importorskip("qiskit_qasm3_import") | ||
| qc = load_circuit(QASM3_STRING) | ||
| assert isinstance(qc, QuantumCircuit) | ||
| assert qc.num_qubits == 1 | ||
|
|
||
|
|
||
| def test_load_circuit_qasm3_path_object() -> None: | ||
| """A QASM 3 file given as a Path is loaded and returned as a QuantumCircuit.""" | ||
| pytest.importorskip("qiskit_qasm3_import") | ||
| qasm_file = Path(__file__).parent.parent.parent / "circuit3.qasm" | ||
| qc = load_circuit(qasm_file) | ||
| assert isinstance(qc, QuantumCircuit) | ||
|
|
||
|
|
||
| def test_load_circuit_qasm3_str_path() -> None: | ||
| """A QASM 3 file given as a str path is loaded and returned as a QuantumCircuit.""" | ||
| pytest.importorskip("qiskit_qasm3_import") | ||
| qasm_file = str(Path(__file__).parent.parent.parent / "circuit3.qasm") | ||
| qc = load_circuit(qasm_file) | ||
| assert isinstance(qc, QuantumCircuit) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -229,30 +229,6 @@ def test_checker_rejects_non_int_max_workers() -> None: | |
| EquivalenceChecker(max_workers=1.5) # ty: ignore[invalid-argument-type] | ||
|
|
||
|
|
||
| def test_checker_rejects_invalid_representation() -> None: | ||
| """Unknown ``representation`` strings are rejected at construction.""" | ||
| with pytest.raises(ValueError, match="representation must be one of"): | ||
| EquivalenceChecker(representation="tensor") # ty: ignore[invalid-argument-type] | ||
|
|
||
|
|
||
| def test_checker_rejects_bool_matrix_max_qubits() -> None: | ||
| """``matrix_max_qubits`` must be a true integer, not a boolean.""" | ||
| with pytest.raises(TypeError, match="matrix_max_qubits"): | ||
| EquivalenceChecker(matrix_max_qubits=True) | ||
|
|
||
|
|
||
| def test_checker_rejects_negative_matrix_max_qubits() -> None: | ||
| """``matrix_max_qubits`` must be non-negative.""" | ||
| with pytest.raises(ValueError, match="non-negative"): | ||
| EquivalenceChecker(matrix_max_qubits=-1) | ||
|
|
||
|
|
||
| def test_check_rejects_mismatched_qubit_counts() -> None: | ||
| """``check`` requires both circuits to have the same width.""" | ||
| with pytest.raises(ValueError, match="same number of qubits"): | ||
| EquivalenceChecker().check(QuantumCircuit(2), QuantumCircuit(3)) | ||
|
|
||
|
|
||
| def test_equivalence_checker_defaults_parallel_true() -> None: | ||
| """``parallel`` defaults to ``True`` (MPO thread pool still gated by qubit count).""" | ||
| assert EquivalenceChecker().parallel is True | ||
|
|
@@ -349,3 +325,70 @@ def test_long_range_mpo_parallel() -> None: | |
| serial = EquivalenceChecker(representation="mpo", parallel=False).check(qc1, qc2) | ||
| parallel = EquivalenceChecker(representation="mpo", parallel=True, max_workers=2).check(qc1, qc2) | ||
| assert serial["equivalent"] == parallel["equivalent"] | ||
|
|
||
|
|
||
| def test_check_accepts_qasm2_path_object() -> None: | ||
| """Check that a QASM 2 file given as a Path object is accepted and returns equivalent.""" | ||
| qasm_path = Path(__file__).parent / "circuit.qasm" | ||
|
|
||
| checker = EquivalenceChecker(representation="mpo") | ||
| result = checker.check(qasm_path, qasm_path) | ||
| assert result["equivalent"] is True | ||
|
|
||
|
|
||
| def test_check_accepts_qasm2_str_path() -> None: | ||
| """Check that a QASM 2 file given as a str path is accepted and returns equivalent.""" | ||
| qasm_path = str(Path(__file__).parent / "circuit.qasm") | ||
|
|
||
| checker = EquivalenceChecker(representation="mpo") | ||
| result = checker.check(qasm_path, qasm_path) | ||
| assert result["equivalent"] is True | ||
|
|
||
|
|
||
| def test_check_qasm_path_vs_quantumcircuit_agree() -> None: | ||
| """Verify that loading via path and via QuantumCircuit gives the same equivalence result.""" | ||
| qasm_path = Path(__file__).parent / "circuit.qasm" | ||
| qc = load(filename=str(qasm_path)) | ||
| checker = EquivalenceChecker(representation="mpo") | ||
| result_path = checker.check(qasm_path, qasm_path) | ||
| result_qc = checker.check(qc, qc) | ||
| assert result_path["equivalent"] == result_qc["equivalent"] | ||
|
|
||
|
|
||
| def test_check_accepts_qasm3_path_object() -> None: | ||
| """Check that a QASM 3 file given as a Path object is accepted and returns equivalent.""" | ||
| pytest.importorskip("qiskit_qasm3_import") | ||
| qasm_file = Path(__file__).parent / "circuit3.qasm" | ||
|
|
||
| checker = EquivalenceChecker(representation="matrix") | ||
| result = checker.check(qasm_file, qasm_file) | ||
| assert result["equivalent"] is True | ||
|
|
||
|
|
||
| def test_check_accepts_qasm3_str_path() -> None: | ||
| """Check that a QASM 3 file given as a str path is accepted and returns equivalent.""" | ||
| pytest.importorskip("qiskit_qasm3_import") | ||
| qasm_file = str(Path(__file__).parent / "circuit3.qasm") | ||
|
|
||
| checker = EquivalenceChecker(representation="matrix") | ||
| result = checker.check(qasm_file, qasm_file) | ||
| assert result["equivalent"] is True | ||
|
Comment on lines
+330
to
+375
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Add integration tests for raw OpenQASM string inputs to These additions verify Proposed test additions+def test_check_accepts_qasm2_raw_string() -> None:
+ """Check that raw OpenQASM 2 text input is accepted."""
+ qasm2 = """\
+OPENQASM 2.0;
+include "qelib1.inc";
+qreg q[1];
+h q[0];
+"""
+ checker = EquivalenceChecker(representation="matrix")
+ result = checker.check(qasm2, qasm2)
+ assert result["equivalent"] is True
+
+
+def test_check_accepts_qasm3_raw_string() -> None:
+ """Check that raw OpenQASM 3 text input is accepted."""
+ pytest.importorskip("qiskit_qasm3_import")
+ qasm3 = """\
+OPENQASM 3.0;
+include "stdgates.inc";
+qubit[1] q;
+h q[0];
+"""
+ checker = EquivalenceChecker(representation="matrix")
+ result = checker.check(qasm3, qasm3)
+ assert result["equivalent"] is True🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_check_accepts_qasm2_raw_string() -> None: | ||
| """Check that a raw QASM 2 string (not a file path) is accepted and returns equivalent.""" | ||
| qasm_str = (Path(__file__).parent / "circuit.qasm").read_text(encoding="utf-8") | ||
|
|
||
| checker = EquivalenceChecker(representation="mpo") | ||
| result = checker.check(qasm_str, qasm_str) | ||
| assert result["equivalent"] is True | ||
|
|
||
|
|
||
| def test_check_accepts_qasm3_raw_string() -> None: | ||
| """Check that a raw QASM 3 string (not a file path) is accepted and returns equivalent.""" | ||
| pytest.importorskip("qiskit_qasm3_import") | ||
| qasm_str = (Path(__file__).parent / "circuit3.qasm").read_text(encoding="utf-8") | ||
|
|
||
| checker = EquivalenceChecker(representation="matrix") | ||
| result = checker.check(qasm_str, qasm_str) | ||
| assert result["equivalent"] is True | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a Google-style docstring to
_first_non_comment_line.The function lacks a docstring. As per coding guidelines, all Python functions should have Google-style docstrings that describe purpose, arguments, and return values.
📝 Suggested docstring
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines