From 4063588dc3763dcc39cb773dd5a607c07660a8a8 Mon Sep 17 00:00:00 2001 From: vinayswamik Date: Wed, 18 Jun 2025 02:40:04 -0500 Subject: [PATCH 1/4] update visitor.py, base.py, test_depth.py - Introduced a new property in QasmModule to control gate decomposition during depth calculations. - Added tests to validate depth calculations for decomposable gates --- src/pyqasm/modules/base.py | 9 ++++++--- src/pyqasm/visitor.py | 26 +++++++++++++++++--------- tests/qasm3/test_depth.py | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index c8c1e2f5..52d3b1b2 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -56,6 +56,7 @@ def __init__(self, name: str, program: Program): self._validated_program = False self._unrolled_ast = Program(statements=[]) self._external_gates: list[str] = [] + self._decompose_gates: Optional[bool] = None @property def name(self) -> str: @@ -259,11 +260,13 @@ def remove_includes(self, in_place=True) -> Optional["QasmModule"]: return curr_module - def depth(self): + def depth(self, decompose_gates=True): """Calculate the depth of the unrolled openqasm program. Args: - None + decompose_gates (bool): If True, calculate depth after decomposing gates. + If False, treat all decompsable gates as a single gate operation. + Defaults to True. Returns: int: The depth of the current "unrolled" openqasm program @@ -279,7 +282,7 @@ def depth(self): qasm_module = self.copy() qasm_module._qubit_depths = {} qasm_module._clbit_depths = {} - + qasm_module._decompose_gates = decompose_gates # Unroll using any external gates that have been recorded for this # module qasm_module.unroll(external_gates=self._external_gates) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index bf5e769b..0ab29099 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -960,16 +960,24 @@ def _visit_basic_gate_operation( result.extend( self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, ctrls) ) - # if gate is not in branching statement - if not self._in_branching_statement: - self._update_qubit_depth_for_gate(unrolled_targets, ctrls) + if self._module._decompose_gates and len(result) > 1: + for gate in result: + if isinstance(gate, qasm3_ast.QuantumGate): + self._visit_basic_gate_operation(gate) else: - for qubit_subset in unrolled_targets + [ctrls]: # get qreg in branching operations - for qubit in qubit_subset: - assert isinstance(qubit.indices, list) and len(qubit.indices) > 0 - assert isinstance(qubit.indices[0], list) and len(qubit.indices[0]) > 0 - qubit_idx = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] - self._is_branch_qubits.add((qubit.name.name, qubit_idx)) + # if gate is not in branching statement + if not self._in_branching_statement: + self._update_qubit_depth_for_gate(unrolled_targets, ctrls) + else: + # get qreg in branching operations + for qubit_subset in unrolled_targets + [ctrls]: + for qubit in qubit_subset: + assert isinstance(qubit.indices, list) and len(qubit.indices) > 0 + assert isinstance(qubit.indices[0], list) and len(qubit.indices[0]) > 0 + qubit_idx = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[ + 0 + ] + self._is_branch_qubits.add((qubit.name.name, qubit_idx)) # check for duplicate bits for final_gate in result: diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 68dca449..23c381a2 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -624,3 +624,26 @@ def test_qasm3_depth_branching_for_external_gates(): result = loads(qasm3_string) result._external_gates = ["my_gate", "my_gate_two"] assert result.depth() == 2 + + +QASM3_DECOMPOSE_GATE_DEPTH_1 = """ +OPENQASM 3.0; +qubit[2] q1; +qreg q[3]; +creg c[3]; +crx (0.1) q[0], q[2]; +rccx q[0], q[1], q1[0]; +""" + + +@pytest.mark.parametrize( + ["input_qasm_str", "before_decompose", "after_decompose"], + [ + (QASM3_DECOMPOSE_GATE_DEPTH_1, 2, 25), + ], +) +def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_decompose): + result = loads(input_qasm_str) + assert result.depth(decompose_gates=False) == before_decompose + # by default its true + assert result.depth() == after_decompose From 24e89a58a81fbdcad3331d5bb87d2b8c37751478 Mon Sep 17 00:00:00 2001 From: vinayswamik Date: Wed, 18 Jun 2025 02:41:04 -0500 Subject: [PATCH 2/4] Refactor depth calculation in QasmVisitor to handle branching statements - Updated logic to increment qubit depth and reset counts only when not in a branching statement. - Added a new test case to validate depth calculations for qubit resets within branching statements. --- src/pyqasm/visitor.py | 10 ++++++---- tests/qasm3/test_depth.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 0ab29099..f0ef72cf 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -690,10 +690,12 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> list[qasm3_ast.Quan unrolled_reset = qasm3_ast.QuantumReset(qubits=qid) qubit_name, qubit_id = qid.name.name, qid.indices[0][0].value # type: ignore - qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] - - qubit_node.depth += 1 - qubit_node.num_resets += 1 + if not self._in_branching_statement: + qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] + qubit_node.depth += 1 + qubit_node.num_resets += 1 + else: + self._is_branch_qubits.add((qubit_name, qubit_id)) unrolled_resets.append(unrolled_reset) diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 23c381a2..5b9e443c 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -417,6 +417,23 @@ def test_qasm3_depth_measurement_indirect(): """, 6, ), + ( + """ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[3] q; +bit[2] mid; +bit[3] out; +measure q[0] -> mid[0]; +measure q[1] -> mid[1]; +if (mid[0]) { +reset q[0]; +reset q[1]; +} +out = measure q; +""", + 3, + ), ], ) def test_qasm3_depth_no_branching(program, expected_depth): From 2dfaea944efcabc4be69bff36f75da3b76226c7b Mon Sep 17 00:00:00 2001 From: vinayswamik Date: Sun, 22 Jun 2025 13:48:15 -0500 Subject: [PATCH 3/4] Update depth calculation logic and tests for decomposable gates - Refactored QasmVisitor to use `_decompose_native_gates` instead of `_decompose_gates`. - Enhanced depth calculation for decomposable gates by ensuring accurate depth updates. - Updated tests in `test_depth.py` and `test_depth.py` to reflect changes in depth calculation logic. - Added a new test for custom decomposable gates to validate depth calculations. --- CHANGELOG.md | 1 + src/pyqasm/modules/base.py | 4 ++-- src/pyqasm/visitor.py | 35 ++++++++++++++++++------------- tests/qasm2/test_depth.py | 2 +- tests/qasm3/test_depth.py | 42 +++++++++++++++++++++++++++++++------- 5 files changed, 60 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e00232ab..3b77a015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Types of changes: ### Fixed - Fixed multiple axes error in circuit visualization of decomposable gates in `draw` method. ([#209](https://github.com/qBraid/pyqasm/pull/210)) +- Fixed depth calculation for decomposable gates by computing depth of each constituent quantum gate.([#211](https://github.com/qBraid/pyqasm/pull/211)) ### Dependencies ### Other diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 52d3b1b2..1074e655 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -56,7 +56,7 @@ def __init__(self, name: str, program: Program): self._validated_program = False self._unrolled_ast = Program(statements=[]) self._external_gates: list[str] = [] - self._decompose_gates: Optional[bool] = None + self._decompose_native_gates: Optional[bool] = None @property def name(self) -> str: @@ -282,7 +282,7 @@ def depth(self, decompose_gates=True): qasm_module = self.copy() qasm_module._qubit_depths = {} qasm_module._clbit_depths = {} - qasm_module._decompose_gates = decompose_gates + qasm_module._decompose_native_gates = decompose_gates # Unroll using any external gates that have been recorded for this # module qasm_module.unroll(external_gates=self._external_gates) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index f0ef72cf..0b809a42 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -962,7 +962,7 @@ def _visit_basic_gate_operation( result.extend( self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, ctrls) ) - if self._module._decompose_gates and len(result) > 1: + if self._module._decompose_native_gates and len(result) > 1: for gate in result: if isinstance(gate, qasm3_ast.QuantumGate): self._visit_basic_gate_operation(gate) @@ -1087,20 +1087,27 @@ def _visit_custom_gate_operation( error_node=gate_op, span=gate_op.span, ) - - # Update the depth only once for the entire custom gate - if self._recording_ext_gate_depth: + if self._module._decompose_native_gates and len(result) > 1: self._recording_ext_gate_depth = False - if not self._in_branching_statement: # if custom gate is not in branching statement - self._update_qubit_depth_for_gate([op_qubits], ctrls) - else: - # get qubit registers in branching operations - for qubit_subset in [op_qubits] + [ctrls]: - for qubit in qubit_subset: - assert isinstance(qubit.indices, list) and len(qubit.indices) > 0 - assert isinstance(qubit.indices[0], list) and len(qubit.indices[0]) > 0 - qubit_idx = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] - self._is_branch_qubits.add((qubit.name.name, qubit_idx)) + for gate in result: + if isinstance(gate, qasm3_ast.QuantumGate): + self._visit_basic_gate_operation(gate) + else: + # Update the depth only once for the entire custom gate + if self._recording_ext_gate_depth: + self._recording_ext_gate_depth = False + if not self._in_branching_statement: # if custom gate is not in branching statement + self._update_qubit_depth_for_gate([op_qubits], ctrls) + else: + # get qubit registers in branching operations + for qubit_subset in [op_qubits] + [ctrls]: + for qubit in qubit_subset: + assert isinstance(qubit.indices, list) and len(qubit.indices) > 0 + assert isinstance(qubit.indices[0], list) and len(qubit.indices[0]) > 0 + qubit_idx = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[ + 0 + ] + self._is_branch_qubits.add((qubit.name.name, qubit_idx)) self._restore_context() diff --git a/tests/qasm2/test_depth.py b/tests/qasm2/test_depth.py index f84b2bb7..5c74cb3d 100644 --- a/tests/qasm2/test_depth.py +++ b/tests/qasm2/test_depth.py @@ -44,7 +44,7 @@ def test_gate_depth(): result.unroll() assert result.num_qubits == 1 assert result.num_clbits == 0 - assert result.depth() == 5 + assert result.depth(decompose_gates=False) == 5 def test_qubit_depth_with_unrelated_measure_op(): diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 5b9e443c..62aedce9 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -48,7 +48,7 @@ def test_gate_depth(): result.unroll() assert result.num_qubits == 1 assert result.num_clbits == 0 - assert result.depth() == 5 + assert result.depth(decompose_gates=False) == 5 QASM3_STRING_1 = """ @@ -102,12 +102,12 @@ def test_gate_depth_external_function(input_qasm_str, first_depth, second_depth, assert result._qubit_depths[("q", i)].num_gates == 1 assert result.num_clbits == 0 - assert result.depth() == first_depth + assert result.depth(decompose_gates=False) == 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 + assert result.depth(decompose_gates=False) == second_depth def test_pow_gate_depth(): @@ -309,7 +309,7 @@ def test_qasm3_depth_sparse_operations(): result = loads(qasm_string) result.unroll() - assert result.depth() == 8 + assert result.depth(decompose_gates=False) == 8 def test_qasm3_depth_measurement_direct(): @@ -328,7 +328,7 @@ def test_qasm3_depth_measurement_direct(): result = loads(qasm_string) result.unroll() - assert result.depth() == 8 + assert result.depth(decompose_gates=False) == 8 def test_qasm3_depth_measurement_indirect(): @@ -605,7 +605,7 @@ def test_qasm3_depth_branching(program, expected_depth): result = loads(program) result.unroll() result.remove_barriers() - assert result.depth() == expected_depth + assert result.depth(decompose_gates=False) == expected_depth def test_qasm3_depth_branching_for_external_gates(): @@ -640,7 +640,7 @@ def test_qasm3_depth_branching_for_external_gates(): """ result = loads(qasm3_string) result._external_gates = ["my_gate", "my_gate_two"] - assert result.depth() == 2 + assert result.depth(decompose_gates=False) == 2 QASM3_DECOMPOSE_GATE_DEPTH_1 = """ @@ -664,3 +664,31 @@ def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_d assert result.depth(decompose_gates=False) == before_decompose # by default its true assert result.depth() == after_decompose + + +QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH_1 = """ +OPENQASM 3.0; +include "stdgates.inc"; +gate custom_crx a, b, { + crx (0.1) a, b; +} +gate custom_rccx a, b, c{ + rccx a, b, c; +} +qubit[2] q1; +qreg q[3]; +custom_crx q[0], q[2]; +custom_rccx q[0], q[1], q1[0]; +""" + + +@pytest.mark.parametrize( + ["input_qasm_str", "before_decompose", "after_decompose"], + [(QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH_1, 2, 25)], +) +def test_gate_depth_decomposable_custom_gates(input_qasm_str, before_decompose, after_decompose): + result = loads(input_qasm_str) + result._external_gates = ["custom_crx", "custom_rccx"] + assert result.depth(decompose_gates=False) == before_decompose + # by default its true + assert result.depth() == after_decompose From f493266074be8034883f839a542158d2dbb8cee2 Mon Sep 17 00:00:00 2001 From: vinayswamik Date: Mon, 23 Jun 2025 12:24:33 -0500 Subject: [PATCH 4/4] Refactor depth calculation to use decompose_native_gates - Updated QasmVisitor and QasmModule to replace decompose_gates with decompose_native_gates for depth calculations. - Adjusted logic in QasmVisitor to ensure accurate depth updates for decomposable custom gates. - Modified tests in test_depth.py and test_depth.py to reflect the new parameter and validate depth calculations for decomposable gates. --- src/pyqasm/modules/base.py | 6 ++--- src/pyqasm/visitor.py | 33 ++++++++++--------------- tests/qasm2/test_depth.py | 2 +- tests/qasm3/test_depth.py | 50 ++++++++++++++++++-------------------- 4 files changed, 40 insertions(+), 51 deletions(-) diff --git a/src/pyqasm/modules/base.py b/src/pyqasm/modules/base.py index 1074e655..3b18db71 100644 --- a/src/pyqasm/modules/base.py +++ b/src/pyqasm/modules/base.py @@ -260,11 +260,11 @@ def remove_includes(self, in_place=True) -> Optional["QasmModule"]: return curr_module - def depth(self, decompose_gates=True): + def depth(self, decompose_native_gates=True): """Calculate the depth of the unrolled openqasm program. Args: - decompose_gates (bool): If True, calculate depth after decomposing gates. + decompose_native_gates (bool): If True, calculate depth after decomposing gates. If False, treat all decompsable gates as a single gate operation. Defaults to True. @@ -282,7 +282,7 @@ def depth(self, decompose_gates=True): qasm_module = self.copy() qasm_module._qubit_depths = {} qasm_module._clbit_depths = {} - qasm_module._decompose_native_gates = decompose_gates + qasm_module._decompose_native_gates = decompose_native_gates # Unroll using any external gates that have been recorded for this # module qasm_module.unroll(external_gates=self._external_gates) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 0b809a42..b6e47960 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1087,27 +1087,20 @@ def _visit_custom_gate_operation( error_node=gate_op, span=gate_op.span, ) - if self._module._decompose_native_gates and len(result) > 1: + + # Update the depth only once for the entire custom gate + if self._recording_ext_gate_depth: self._recording_ext_gate_depth = False - for gate in result: - if isinstance(gate, qasm3_ast.QuantumGate): - self._visit_basic_gate_operation(gate) - else: - # Update the depth only once for the entire custom gate - if self._recording_ext_gate_depth: - self._recording_ext_gate_depth = False - if not self._in_branching_statement: # if custom gate is not in branching statement - self._update_qubit_depth_for_gate([op_qubits], ctrls) - else: - # get qubit registers in branching operations - for qubit_subset in [op_qubits] + [ctrls]: - for qubit in qubit_subset: - assert isinstance(qubit.indices, list) and len(qubit.indices) > 0 - assert isinstance(qubit.indices[0], list) and len(qubit.indices[0]) > 0 - qubit_idx = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[ - 0 - ] - self._is_branch_qubits.add((qubit.name.name, qubit_idx)) + if not self._in_branching_statement: # if custom gate is not in branching statement + self._update_qubit_depth_for_gate([op_qubits], ctrls) + else: + # get qubit registers in branching operations + for qubit_subset in [op_qubits] + [ctrls]: + for qubit in qubit_subset: + assert isinstance(qubit.indices, list) and len(qubit.indices) > 0 + assert isinstance(qubit.indices[0], list) and len(qubit.indices[0]) > 0 + qubit_idx = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] + self._is_branch_qubits.add((qubit.name.name, qubit_idx)) self._restore_context() diff --git a/tests/qasm2/test_depth.py b/tests/qasm2/test_depth.py index 5c74cb3d..b2a67929 100644 --- a/tests/qasm2/test_depth.py +++ b/tests/qasm2/test_depth.py @@ -44,7 +44,7 @@ def test_gate_depth(): result.unroll() assert result.num_qubits == 1 assert result.num_clbits == 0 - assert result.depth(decompose_gates=False) == 5 + assert result.depth(decompose_native_gates=False) == 5 def test_qubit_depth_with_unrelated_measure_op(): diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 62aedce9..a796a50a 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -48,7 +48,7 @@ def test_gate_depth(): result.unroll() assert result.num_qubits == 1 assert result.num_clbits == 0 - assert result.depth(decompose_gates=False) == 5 + assert result.depth(decompose_native_gates=False) == 5 QASM3_STRING_1 = """ @@ -102,12 +102,12 @@ def test_gate_depth_external_function(input_qasm_str, first_depth, second_depth, assert result._qubit_depths[("q", i)].num_gates == 1 assert result.num_clbits == 0 - assert result.depth(decompose_gates=False) == first_depth + 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(decompose_gates=False) == second_depth + assert result.depth() == second_depth def test_pow_gate_depth(): @@ -309,7 +309,7 @@ def test_qasm3_depth_sparse_operations(): result = loads(qasm_string) result.unroll() - assert result.depth(decompose_gates=False) == 8 + assert result.depth(decompose_native_gates=False) == 8 def test_qasm3_depth_measurement_direct(): @@ -328,7 +328,7 @@ def test_qasm3_depth_measurement_direct(): result = loads(qasm_string) result.unroll() - assert result.depth(decompose_gates=False) == 8 + assert result.depth(decompose_native_gates=False) == 8 def test_qasm3_depth_measurement_indirect(): @@ -605,7 +605,7 @@ def test_qasm3_depth_branching(program, expected_depth): result = loads(program) result.unroll() result.remove_barriers() - assert result.depth(decompose_gates=False) == expected_depth + assert result.depth(decompose_native_gates=False) == expected_depth def test_qasm3_depth_branching_for_external_gates(): @@ -640,10 +640,10 @@ def test_qasm3_depth_branching_for_external_gates(): """ result = loads(qasm3_string) result._external_gates = ["my_gate", "my_gate_two"] - assert result.depth(decompose_gates=False) == 2 + assert result.depth() == 2 -QASM3_DECOMPOSE_GATE_DEPTH_1 = """ +QASM3_DECOMPOSE_GATE_DEPTH = """ OPENQASM 3.0; qubit[2] q1; qreg q[3]; @@ -651,22 +651,7 @@ def test_qasm3_depth_branching_for_external_gates(): crx (0.1) q[0], q[2]; rccx q[0], q[1], q1[0]; """ - - -@pytest.mark.parametrize( - ["input_qasm_str", "before_decompose", "after_decompose"], - [ - (QASM3_DECOMPOSE_GATE_DEPTH_1, 2, 25), - ], -) -def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_decompose): - result = loads(input_qasm_str) - assert result.depth(decompose_gates=False) == before_decompose - # by default its true - assert result.depth() == after_decompose - - -QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH_1 = """ +QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH = """ OPENQASM 3.0; include "stdgates.inc"; gate custom_crx a, b, { @@ -684,11 +669,22 @@ def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_d @pytest.mark.parametrize( ["input_qasm_str", "before_decompose", "after_decompose"], - [(QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH_1, 2, 25)], + [(QASM3_DECOMPOSE_GATE_DEPTH, 2, 25), (QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH, 2, 25)], +) +def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_decompose): + result = loads(input_qasm_str) + assert result.depth(decompose_native_gates=False) == before_decompose + # by default its true + assert result.depth() == after_decompose + + +@pytest.mark.parametrize( + ["input_qasm_str", "before_decompose", "after_decompose"], + [(QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH, 2, 2)], ) -def test_gate_depth_decomposable_custom_gates(input_qasm_str, before_decompose, after_decompose): +def test_gate_depth_decomposable_external_gates(input_qasm_str, before_decompose, after_decompose): result = loads(input_qasm_str) result._external_gates = ["custom_crx", "custom_rccx"] - assert result.depth(decompose_gates=False) == before_decompose + assert result.depth(decompose_native_gates=False) == before_decompose # by default its true assert result.depth() == after_decompose