From 1b5ffb91d53730bcaa9879ab377578ecd593dcf6 Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 11:13:07 +0200 Subject: [PATCH 1/9] Add logic to record the depth for a custom gate only once regardless of definition --- src/pyqasm/visitor.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index bb24ef47..6783430b 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -88,6 +88,7 @@ def __init__( self._unroll_barriers: bool = unroll_barriers self._curr_scope: int = 0 self._label_scope_level: dict[int, set] = {self._curr_scope: set()} + self._recording_depth = True self._init_utilities() @@ -760,12 +761,13 @@ def _update_qubit_depth_for_gate( qubit_node.num_gates += 1 max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) - for qubit in qubit_subset + ctrls: - assert isinstance(qubit.indices[0], list) - _qid_ = qubit.indices[0][0] - qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore - qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] - qubit_node.depth = max_involved_depth + if self._recording_depth: + for qubit in qubit_subset + ctrls: + assert isinstance(qubit.indices[0], list) + _qid_ = qubit.indices[0][0] + qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore + qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] + qubit_node.depth = max_involved_depth def _visit_basic_gate_operation( # pylint: disable=too-many-locals self, @@ -913,6 +915,10 @@ def _visit_custom_gate_operation( gate_definition_ops.reverse() self._push_context(Context.GATE) + + # Pause recording the depth of new gates because we are processing the + # definition of a custom gate here - handle the depth separately afterwards + self._recording_depth = False result = [] for gate_op in gate_definition_ops: if isinstance(gate_op, (qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase)): @@ -942,6 +948,16 @@ def _visit_custom_gate_operation( span=gate_op.span, ) + # Update the depth only once for the entire custom gate + self._recording_depth = True + op_qubits: list[qasm3_ast.IndexedIdentifier] = ( + self._get_op_bits( # type: ignore [assignment] + operation, + self._global_qreg_size_map, + ) + ) + self._update_qubit_depth_for_gate([op_qubits], ctrls) + self._restore_context() if self._check_only: From d2e7d04a8bd196460a55307c06b53c895f5301f1 Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 12:02:49 +0200 Subject: [PATCH 2/9] Add logic to store external_gates defs in the module if called with unroll --- src/pyqasm/modules/base.py | 10 +++++++++- src/pyqasm/visitor.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 5b6890cb..d765ad70 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -55,6 +55,7 @@ def __init__(self, name: str, program: Program): self._has_barriers: Optional[bool] = None self._validated_program = False self._unrolled_ast = Program(statements=[]) + self._external_gates = [] @property def name(self) -> str: @@ -278,7 +279,10 @@ def depth(self): qasm_module = self.copy() qasm_module._qubit_depths = {} qasm_module._clbit_depths = {} - qasm_module.unroll() + + # Unroll using any external gates that have been recorded for this + # module + qasm_module.unroll(external_gates = self._external_gates) max_depth = 0 max_qubit_depth, max_clbit_depth = 0, 0 @@ -539,6 +543,10 @@ def unroll(self, **kwargs): kwargs = {} try: self.num_qubits, self.num_clbits = 0, 0 + if ext_gates := kwargs.get("external_gates"): + self._external_gates = ext_gates + else: + self._external_gates = [] visitor = QasmVisitor(module=self, **kwargs) self.accept(visitor) except (ValidationError, UnrollError) as err: diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 6783430b..29b35a6e 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -918,7 +918,7 @@ def _visit_custom_gate_operation( # Pause recording the depth of new gates because we are processing the # definition of a custom gate here - handle the depth separately afterwards - self._recording_depth = False + self._recording_depth = not (operation.name.name in self._external_gates) result = [] for gate_op in gate_definition_ops: if isinstance(gate_op, (qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase)): @@ -949,14 +949,15 @@ def _visit_custom_gate_operation( ) # Update the depth only once for the entire custom gate - self._recording_depth = True - op_qubits: list[qasm3_ast.IndexedIdentifier] = ( - self._get_op_bits( # type: ignore [assignment] - operation, - self._global_qreg_size_map, + if not self._recording_depth: + self._recording_depth = True + op_qubits: list[qasm3_ast.IndexedIdentifier] = ( + self._get_op_bits( # type: ignore [assignment] + operation, + self._global_qreg_size_map, + ) ) - ) - self._update_qubit_depth_for_gate([op_qubits], ctrls) + self._update_qubit_depth_for_gate([op_qubits], ctrls) self._restore_context() @@ -985,7 +986,6 @@ def _visit_external_gate_operation( Returns: list[qasm3_ast.QuantumGate]: The quantum gate that was collected. """ - logger.debug("Visiting external gate operation '%s'", str(operation)) gate_name: str = operation.name.name if ctrls is None: From b560d624d1d44f2a5e5b674276957d8cd5220ccf Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 12:03:06 +0200 Subject: [PATCH 3/9] Test the external gates def logic as per examples --- tests/qasm3/test_depth.py | 62 +++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index aac75b0e..08972b5d 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -51,25 +51,57 @@ def test_gate_depth(): assert result.depth() == 5 -@pytest.mark.skip(reason="Not implemented computing depth of external gates") -def test_gate_depth_external_function(): - qasm3_string = """ - OPENQASM 3; - include "stdgates.inc"; +qasm3_string_1 = """ +OPENQASM 3; +include "stdgates.inc"; - gate my_gate() q { - h q; - x q; - } +gate my_gate() q { + h q; + x q; +} - qubit q; - my_gate() q; - """ - result = loads(qasm3_string) +qubit q; +my_gate() q; +""" + +qasm3_string_2 = """ +OPENQASM 3.0; +include "stdgates.inc"; +gate my_gate q1, q2 { + h q1; + cx q1, q2; + h q2; +} +qubit[2] q; +my_gate q[0], q[1]; +""" + +qasm3_string_3 = """ +OPENQASM 3.0; +include "stdgates.inc"; +gate my_gate q1, q2 { } +qubit[2] q; +my_gate q[0], q[1]; +""" + +@pytest.mark.parametrize(["input_qasm_str", "first_depth", "second_depth", "num_qubits"], + [ + (qasm3_string_1, 1, 2, 1), + (qasm3_string_2, 1, 3, 2), + (qasm3_string_3, 1, 0, 2), + ] + ) +def test_gate_depth_external_function(input_qasm_str, first_depth, second_depth, num_qubits): + result = loads(input_qasm_str) result.unroll(external_gates=["my_gate"]) - assert result.num_qubits == 1 + assert result.num_qubits == num_qubits assert result.num_clbits == 0 - assert result.depth() == 1 + assert result.depth() == first_depth + + # Check that unrolling with no external_gates flushes the internally stored + # external gates and influences the depth calculation + result.unroll() + assert result.depth() == second_depth def test_pow_gate_depth(): From 81e833b2ffb67ce72036657e132e436f5210bd0b Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 12:05:59 +0200 Subject: [PATCH 4/9] Prepare changelog item --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 369f94ff..7c5646bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ Types of changes: ### Fixed +- Fixed the way how depth is calculated when external gates are defined with unrolling a QASM module. ([#XXX](https://github.com/qBraid/pyqasm/pull/XXX)) + ### Dependencies ### Other From 8fb6845a5da23a5c94a43967402e8aa276e42e6d Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 12:19:54 +0200 Subject: [PATCH 5/9] Linting --- src/pyqasm/visitor.py | 2 +- tests/qasm3/test_depth.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 29b35a6e..8f8dcefd 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -918,7 +918,7 @@ def _visit_custom_gate_operation( # Pause recording the depth of new gates because we are processing the # definition of a custom gate here - handle the depth separately afterwards - self._recording_depth = not (operation.name.name in self._external_gates) + self._recording_depth = not operation.name.name in self._external_gates result = [] for gate_op in gate_definition_ops: if isinstance(gate_op, (qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase)): diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 08972b5d..13269c37 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -51,7 +51,7 @@ def test_gate_depth(): assert result.depth() == 5 -qasm3_string_1 = """ +QASM3_STRING_1 = """ OPENQASM 3; include "stdgates.inc"; @@ -64,7 +64,7 @@ def test_gate_depth(): my_gate() q; """ -qasm3_string_2 = """ +QASM3_STRING_2 = """ OPENQASM 3.0; include "stdgates.inc"; gate my_gate q1, q2 { @@ -76,7 +76,7 @@ def test_gate_depth(): my_gate q[0], q[1]; """ -qasm3_string_3 = """ +QASM3_STRING_3 = """ OPENQASM 3.0; include "stdgates.inc"; gate my_gate q1, q2 { } @@ -86,9 +86,9 @@ def test_gate_depth(): @pytest.mark.parametrize(["input_qasm_str", "first_depth", "second_depth", "num_qubits"], [ - (qasm3_string_1, 1, 2, 1), - (qasm3_string_2, 1, 3, 2), - (qasm3_string_3, 1, 0, 2), + (QASM3_STRING_1, 1, 2, 1), + (QASM3_STRING_2, 1, 3, 2), + (QASM3_STRING_3, 1, 0, 2), ] ) def test_gate_depth_external_function(input_qasm_str, first_depth, second_depth, num_qubits): From 6a8735ab84a06bf153f3ffdff1305d9a6fc7d303 Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 12:22:07 +0200 Subject: [PATCH 6/9] Linting --- src/pyqasm/modules/base.py | 2 +- tests/qasm3/test_depth.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index d765ad70..df6594ae 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -282,7 +282,7 @@ def depth(self): # Unroll using any external gates that have been recorded for this # module - qasm_module.unroll(external_gates = self._external_gates) + qasm_module.unroll(external_gates=self._external_gates) max_depth = 0 max_qubit_depth, max_clbit_depth = 0, 0 diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 13269c37..a0f43b51 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -84,13 +84,15 @@ def test_gate_depth(): my_gate q[0], q[1]; """ -@pytest.mark.parametrize(["input_qasm_str", "first_depth", "second_depth", "num_qubits"], - [ - (QASM3_STRING_1, 1, 2, 1), - (QASM3_STRING_2, 1, 3, 2), - (QASM3_STRING_3, 1, 0, 2), - ] - ) + +@pytest.mark.parametrize( + ["input_qasm_str", "first_depth", "second_depth", "num_qubits"], + [ + (QASM3_STRING_1, 1, 2, 1), + (QASM3_STRING_2, 1, 3, 2), + (QASM3_STRING_3, 1, 0, 2), + ], +) def test_gate_depth_external_function(input_qasm_str, first_depth, second_depth, num_qubits): result = loads(input_qasm_str) result.unroll(external_gates=["my_gate"]) From a686d8864df61d26188fe6f3488b4b1a7e3709e7 Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 12:30:07 +0200 Subject: [PATCH 7/9] Linting with mypy --- src/pyqasm/modules/base.py | 2 +- src/pyqasm/visitor.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index df6594ae..e33df77f 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -55,7 +55,7 @@ def __init__(self, name: str, program: Program): self._has_barriers: Optional[bool] = None self._validated_program = False self._unrolled_ast = Program(statements=[]) - self._external_gates = [] + self._external_gates: list[str] = [] @property def name(self) -> str: diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 8f8dcefd..40cbd67e 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -951,12 +951,6 @@ def _visit_custom_gate_operation( # Update the depth only once for the entire custom gate if not self._recording_depth: self._recording_depth = True - op_qubits: list[qasm3_ast.IndexedIdentifier] = ( - self._get_op_bits( # type: ignore [assignment] - operation, - self._global_qreg_size_map, - ) - ) self._update_qubit_depth_for_gate([op_qubits], ctrls) self._restore_context() From f852744663d2989ef55a1f2da069f8e81db10762 Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 14:59:02 +0200 Subject: [PATCH 8/9] Apply suggestions from code review Co-authored-by: Harshit Gupta --- CHANGELOG.md | 2 +- src/pyqasm/visitor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c5646bf..0fa9716d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Types of changes: ### Fixed -- Fixed the way how depth is calculated when external gates are defined with unrolling a QASM module. ([#XXX](https://github.com/qBraid/pyqasm/pull/XXX)) +- Fixed the way how depth is calculated when external gates are defined with unrolling a QASM module. ([#198](https://github.com/qBraid/pyqasm/pull/198)) ### Dependencies diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 40cbd67e..da58b927 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -918,7 +918,7 @@ def _visit_custom_gate_operation( # Pause recording the depth of new gates because we are processing the # definition of a custom gate here - handle the depth separately afterwards - self._recording_depth = not operation.name.name in self._external_gates + self._recording_depth = not gate_name in self._external_gates result = [] for gate_op in gate_definition_ops: if isinstance(gate_op, (qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase)): From d5a4339b7f4293a52b5bfffa2c3b74af4a040071 Mon Sep 17 00:00:00 2001 From: antalszava Date: Thu, 29 May 2025 15:13:17 +0200 Subject: [PATCH 9/9] Update naming and ensure num_gates is set as expected too --- src/pyqasm/visitor.py | 31 ++++++++++++++++--------------- tests/qasm3/test_depth.py | 4 ++++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index da58b927..a454b3a3 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -88,7 +88,7 @@ def __init__( self._unroll_barriers: bool = unroll_barriers self._curr_scope: int = 0 self._label_scope_level: dict[int, set] = {self._curr_scope: set()} - self._recording_depth = True + self._recording_ext_gate_depth = False self._init_utilities() @@ -751,17 +751,17 @@ def _update_qubit_depth_for_gate( Returns: None """ - for qubit_subset in all_targets: - max_involved_depth = 0 - for qubit in qubit_subset + ctrls: - assert isinstance(qubit.indices[0], list) - _qid_ = qubit.indices[0][0] - qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore - qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] - qubit_node.num_gates += 1 - max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) - - if self._recording_depth: + if not self._recording_ext_gate_depth: + for qubit_subset in all_targets: + max_involved_depth = 0 + for qubit in qubit_subset + ctrls: + assert isinstance(qubit.indices[0], list) + _qid_ = qubit.indices[0][0] + qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore + qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] + qubit_node.num_gates += 1 + max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) + for qubit in qubit_subset + ctrls: assert isinstance(qubit.indices[0], list) _qid_ = qubit.indices[0][0] @@ -918,7 +918,8 @@ def _visit_custom_gate_operation( # Pause recording the depth of new gates because we are processing the # definition of a custom gate here - handle the depth separately afterwards - self._recording_depth = not gate_name in self._external_gates + self._recording_ext_gate_depth = gate_name in self._external_gates + result = [] for gate_op in gate_definition_ops: if isinstance(gate_op, (qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase)): @@ -949,8 +950,8 @@ def _visit_custom_gate_operation( ) # Update the depth only once for the entire custom gate - if not self._recording_depth: - self._recording_depth = True + if self._recording_ext_gate_depth: + self._recording_ext_gate_depth = False self._update_qubit_depth_for_gate([op_qubits], ctrls) self._restore_context() diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index a0f43b51..487c1f2c 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -97,6 +97,10 @@ def test_gate_depth_external_function(input_qasm_str, first_depth, second_depth, result = loads(input_qasm_str) result.unroll(external_gates=["my_gate"]) assert result.num_qubits == num_qubits + + for i in range(num_qubits): + assert result._qubit_depths[("q", i)].num_gates == 1 + assert result.num_clbits == 0 assert result.depth() == first_depth