-
Notifications
You must be signed in to change notification settings - Fork 973
MAHOUT-604: add test_multi_qubit_gates.py - part 1 #629
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
Conversation
|
Please help fix the CI error, thanks! |
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.
Pull Request Overview
This PR adds comprehensive test coverage for multi-qubit quantum gates (CNOT and Toffoli gates) as part 1 of a test suite that will eventually include SWAP and CSWAP gates. The tests verify gate behavior across multiple quantum computing backends (Qiskit, Cirq, Amazon Braket).
- Implements parametrized tests for CNOT and Toffoli gate state transitions, double applications (verifying gate self-inverse properties), and behavior on multi-qubit circuits
- Adds backend-aware helper functions to handle different qubit ordering conventions (little-endian vs big-endian)
- Includes edge case testing for uninitialized circuits, invalid qubit indices, and cross-backend consistency
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
testing/test_multi_qubit_gates.py
Outdated
| def get_state_probability(results, target_state, num_qubits=1, backend_name=None): | ||
| """ | ||
| Calculate the probability of measuring a target state. | ||
|
|
||
| Args: | ||
| results: Dictionary of measurement results from execute_circuit() | ||
| target_state: Target state as string (e.g., "0", "1", "101") or int | ||
| num_qubits: Number of qubits in the circuit | ||
| backend_name: Name of the backend (for handling qubit ordering) | ||
|
|
||
| Returns: | ||
| Probability of measuring the target state | ||
| """ | ||
| if isinstance(results, list): | ||
| results = results[0] | ||
|
|
||
| total_shots = sum(results.values()) | ||
| if total_shots == 0: | ||
| return 0.0 | ||
|
|
||
| # Convert target_state to both string and int formats for comparison | ||
| if isinstance(target_state, str): | ||
| target_str = target_state | ||
| # Convert binary string to integer | ||
| target_int = int(target_state, 2) if target_state else 0 | ||
| else: | ||
| target_int = target_state | ||
| # Convert integer to binary string | ||
| target_str = format(target_state, f"0{num_qubits}b") | ||
|
|
||
| # Handle backend-specific qubit ordering | ||
| # Qiskit uses little-endian (rightmost bit is qubit 0) | ||
| # Amazon Braket and Cirq use big-endian (leftmost bit is qubit 0) | ||
| if backend_name == "qiskit" and isinstance(target_str, str) and len(target_str) > 1: | ||
| # Reverse the string for Qiskit (little-endian) | ||
| target_str_qiskit = target_str[::-1] | ||
| else: | ||
| target_str_qiskit = target_str | ||
|
|
||
| target_count = 0 | ||
| for state, count in results.items(): | ||
| if isinstance(state, str): | ||
| # For Qiskit, compare with reversed string | ||
| if backend_name == "qiskit" and len(state) > 1: | ||
| if state == target_str_qiskit: | ||
| target_count = count | ||
| break | ||
| else: | ||
| if state == target_str: | ||
| target_count = count | ||
| break | ||
| else: | ||
| # For Cirq, use integer comparison | ||
| # Cirq uses big-endian, so the integer representation matches | ||
| if backend_name == "cirq": | ||
| if state == target_int: | ||
| target_count = count | ||
| break | ||
| else: | ||
| # For other backends, also try integer comparison | ||
| if state == target_int: | ||
| target_count = count | ||
| break | ||
|
|
||
| return target_count / total_shots | ||
|
|
||
|
|
Copilot
AI
Nov 15, 2025
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.
The get_state_probability function is duplicated from test_single_qubit_gates.py with added backend-specific handling. Consider extracting this function to a shared utility module (e.g., testing/utils/qumat_helpers.py) to avoid code duplication and make it easier to maintain. This would follow the DRY (Don't Repeat Yourself) principle and ensure consistent behavior across test files.
| def get_state_probability(results, target_state, num_qubits=1, backend_name=None): | |
| """ | |
| Calculate the probability of measuring a target state. | |
| Args: | |
| results: Dictionary of measurement results from execute_circuit() | |
| target_state: Target state as string (e.g., "0", "1", "101") or int | |
| num_qubits: Number of qubits in the circuit | |
| backend_name: Name of the backend (for handling qubit ordering) | |
| Returns: | |
| Probability of measuring the target state | |
| """ | |
| if isinstance(results, list): | |
| results = results[0] | |
| total_shots = sum(results.values()) | |
| if total_shots == 0: | |
| return 0.0 | |
| # Convert target_state to both string and int formats for comparison | |
| if isinstance(target_state, str): | |
| target_str = target_state | |
| # Convert binary string to integer | |
| target_int = int(target_state, 2) if target_state else 0 | |
| else: | |
| target_int = target_state | |
| # Convert integer to binary string | |
| target_str = format(target_state, f"0{num_qubits}b") | |
| # Handle backend-specific qubit ordering | |
| # Qiskit uses little-endian (rightmost bit is qubit 0) | |
| # Amazon Braket and Cirq use big-endian (leftmost bit is qubit 0) | |
| if backend_name == "qiskit" and isinstance(target_str, str) and len(target_str) > 1: | |
| # Reverse the string for Qiskit (little-endian) | |
| target_str_qiskit = target_str[::-1] | |
| else: | |
| target_str_qiskit = target_str | |
| target_count = 0 | |
| for state, count in results.items(): | |
| if isinstance(state, str): | |
| # For Qiskit, compare with reversed string | |
| if backend_name == "qiskit" and len(state) > 1: | |
| if state == target_str_qiskit: | |
| target_count = count | |
| break | |
| else: | |
| if state == target_str: | |
| target_count = count | |
| break | |
| else: | |
| # For Cirq, use integer comparison | |
| # Cirq uses big-endian, so the integer representation matches | |
| if backend_name == "cirq": | |
| if state == target_int: | |
| target_count = count | |
| break | |
| else: | |
| # For other backends, also try integer comparison | |
| if state == target_int: | |
| target_count = count | |
| break | |
| return target_count / total_shots | |
| from testing.utils.qumat_helpers import get_state_probability |
| qumat.apply_cnot_gate(*gate_args) | ||
| else: | ||
| qumat.apply_toffoli_gate(*gate_args) | ||
| except (IndexError, ValueError, RuntimeError, Exception): |
Copilot
AI
Nov 15, 2025
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.
Catching bare Exception is overly broad and can mask unexpected errors. This makes debugging more difficult since it will silently catch programming errors like AttributeError or TypeError. Consider catching only the specific exceptions that are expected (e.g., IndexError, ValueError, RuntimeError) or let unexpected exceptions propagate to surface actual bugs.
| except (IndexError, ValueError, RuntimeError, Exception): | |
| except (IndexError, ValueError, RuntimeError): |
| qumat.apply_toffoli_gate(*gate_args) | ||
| results = qumat.execute_circuit() | ||
| assert results is not None | ||
| except (ValueError, RuntimeError, Exception): |
Copilot
AI
Nov 15, 2025
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.
Catching bare Exception is overly broad and can mask unexpected errors. This makes debugging more difficult since it will silently catch programming errors like AttributeError or TypeError. Consider catching only the specific exceptions that are expected (e.g., ValueError, RuntimeError) or let unexpected exceptions propagate to surface actual bugs.
| except (ValueError, RuntimeError, Exception): | |
| except (ValueError, RuntimeError): |
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.
I think this one make sense, wdyt?
| qumat.apply_cnot_gate(*gate_args) | ||
| else: | ||
| qumat.apply_toffoli_gate(*gate_args) | ||
| except (IndexError, ValueError, RuntimeError, Exception): |
Copilot
AI
Nov 15, 2025
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.
'except' clause does nothing but pass and there is no explanatory comment.
| except (IndexError, ValueError, RuntimeError, Exception): | |
| except (IndexError, ValueError, RuntimeError, Exception): | |
| # Intentionally ignore exceptions: test passes if gate raises error for invalid qubit indices |
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.
Also this one
| try: | ||
| if gate_name == "cnot": | ||
| qumat.apply_cnot_gate(*gate_args) | ||
| else: | ||
| qumat.apply_toffoli_gate(*gate_args) | ||
| results = qumat.execute_circuit() | ||
| assert results is not None | ||
| except (ValueError, RuntimeError, Exception): | ||
| pass |
Copilot
AI
Nov 15, 2025
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.
'except' clause does nothing but pass and there is no explanatory comment.
| try: | |
| if gate_name == "cnot": | |
| qumat.apply_cnot_gate(*gate_args) | |
| else: | |
| qumat.apply_toffoli_gate(*gate_args) | |
| results = qumat.execute_circuit() | |
| assert results is not None | |
| except (ValueError, RuntimeError, Exception): | |
| pass | |
| # The gate should either raise an error (invalid qubit indices) or execute gracefully. | |
| # We explicitly check for exceptions using pytest.raises, and if no exception is raised, | |
| # we assert that the results are not None. | |
| exception_types = (ValueError, RuntimeError, Exception) | |
| try: | |
| if gate_name == "cnot": | |
| qumat.apply_cnot_gate(*gate_args) | |
| else: | |
| qumat.apply_toffoli_gate(*gate_args) | |
| except exception_types: | |
| # Exception was raised as expected for invalid/same qubit indices. | |
| return | |
| results = qumat.execute_circuit() | |
| assert results is not None |
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.
This as well
|
what is status here? |
|
I don't know if we need DRY way like copliot said. |
|
Sorry for the late, I got lots of deadline this week🔥 |
guan404ming
left a comment
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.
Overall looks good, left some comments
|
I'm going to merge this one and I think we could refine this if wanted. |
Purpose of PR
Improve Test Coverage for Quantum Gates and Circuit Operations.
This PR provides two gates test of multi qubit gates (CNOT gate , Toffoli gate). This file will includes CNOT gate , Toffoli gate , SWAP gate and CSWAP gate.
Related Issues or PRs
Related to #604
Changes Made
Breaking Changes
Checklist