Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions qumat/amazon_braket_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,32 @@ def apply_u_gate(circuit, qubit_index, theta, phi, lambd):
circuit.rx(qubit_index, theta)
circuit.ry(qubit_index, phi)
circuit.rz(qubit_index, lambd)


def calculate_prob_zero(results, ancilla_qubit, num_qubits):
"""
Calculate the probability of measuring the ancilla qubit in |0> state.

Amazon Braket uses big-endian qubit ordering with string format results,
where the leftmost bit corresponds to qubit 0.

Args:
results: Measurement results from execute_circuit() (dict with string keys)
ancilla_qubit: Index of the ancilla qubit
num_qubits: Total number of qubits in the circuit

Returns:
float: Probability of measuring ancilla in |0> state
"""
if isinstance(results, list):
results = results[0]

total_shots = sum(results.values())
count_zero = 0

for state, count in results.items():
# Braket: big-endian, leftmost bit is qubit 0
if len(state) > ancilla_qubit and state[ancilla_qubit] == "0":
count_zero += count

return count_zero / total_shots if total_shots > 0 else 0.0
29 changes: 29 additions & 0 deletions qumat/cirq_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,32 @@ def get_final_state_vector(circuit, backend, backend_config):
simulator = cirq.Simulator()
result = simulator.simulate(circuit)
return result.final_state_vector


def calculate_prob_zero(results, ancilla_qubit, num_qubits):
"""
Calculate the probability of measuring the ancilla qubit in |0> state.

Cirq uses big-endian qubit ordering with integer format results,
where qubit i corresponds to bit (num_qubits - 1 - i).

Args:
results: Measurement results from execute_circuit() (list of dicts with integer keys)
ancilla_qubit: Index of the ancilla qubit
num_qubits: Total number of qubits in the circuit

Returns:
float: Probability of measuring ancilla in |0> state
"""
if isinstance(results, list):
results = results[0]

total_shots = sum(results.values())
count_zero = 0

for state, count in results.items():
bit_position = num_qubits - 1 - ancilla_qubit
if ((state >> bit_position) & 1) == 0:
count_zero += count

return count_zero / total_shots if total_shots > 0 else 0.0
30 changes: 30 additions & 0 deletions qumat/qiskit_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,33 @@ def apply_rz_gate(circuit, qubit_index, angle):
def apply_u_gate(circuit, qubit_index, theta, phi, lambd):
# Apply the U gate directly with specified parameters
circuit.u(theta, phi, lambd, qubit_index)


def calculate_prob_zero(results, ancilla_qubit, num_qubits):
"""
Calculate the probability of measuring the ancilla qubit in |0> state.

Qiskit uses little-endian qubit ordering with string format results,
where the rightmost bit corresponds to qubit 0.

Args:
results: Measurement results from execute_circuit() (dict with string keys)
ancilla_qubit: Index of the ancilla qubit
num_qubits: Total number of qubits in the circuit

Returns:
float: Probability of measuring ancilla in |0> state
"""
# Handle different result formats from different backends
if isinstance(results, list):
results = results[0]

total_shots = sum(results.values())
count_zero = 0

for state, count in results.items():
# Qiskit: little-endian, rightmost bit is qubit 0
if len(state) > ancilla_qubit and state[-(ancilla_qubit + 1)] == "0":
count_zero += count

return count_zero / total_shots if total_shots > 0 else 0.0
54 changes: 54 additions & 0 deletions qumat/qumat.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ def __init__(self, backend_config):
)
self.backend = self.backend_module.initialize_backend(backend_config)
self.circuit = None
self.num_qubits = None
self.parameters = {}

def create_empty_circuit(self, num_qubits: int | None = None):
self.num_qubits = num_qubits
self.circuit = self.backend_module.create_empty_circuit(num_qubits)

def apply_not_gate(self, qubit_index):
Expand Down Expand Up @@ -130,3 +132,55 @@ def swap_test(self, ancilla_qubit, qubit1, qubit2):

# Apply Hadamard to ancilla qubit again
self.apply_hadamard_gate(ancilla_qubit)

def measure_overlap(self, qubit1, qubit2, ancilla_qubit=0):
"""
Measures the overlap (fidelity) between two quantum states using the swap test.

This method creates a swap test circuit to calculate the similarity between
the quantum states on qubit1 and qubit2. It returns the squared overlap |<ψ|φ>|²,
which represents the fidelity between the two states.

The swap test measures P(ancilla=0), which is related to overlap as:
P(0) = (1 + |<ψ|φ>|²) / 2

However, for certain states (especially identical excited states), global phase
effects may cause the ancilla to measure predominantly |1> instead of |0>.
This method handles both cases by taking the measurement probability closer to 1.

Args:
qubit1: Index of the first qubit containing state |ψ>
qubit2: Index of the second qubit containing state |φ>
ancilla_qubit: Index of the ancilla qubit (default: 0, should be initialized to |0>)

Returns:
float: The squared overlap |<ψ|φ>|² between the two states (fidelity)
"""
# Perform the swap test
self.swap_test(ancilla_qubit, qubit1, qubit2)
results = self.execute_circuit()

# Calculate the probability of measuring ancilla in |0> state
prob_zero = self.calculate_prob_zero(results, ancilla_qubit)
prob_zero_or_one = max(prob_zero, 1 - prob_zero)
overlap_squared = 2 * prob_zero_or_one - 1
overlap_squared = max(0.0, min(1.0, overlap_squared))

return overlap_squared

def calculate_prob_zero(self, results, ancilla_qubit):
"""
Calculate the probability of measuring the ancilla qubit in |0> state.

Delegates to backend-specific implementation via the backend module.

Args:
results: Measurement results from execute_circuit()
ancilla_qubit: Index of the ancilla qubit

Returns:
float: Probability of measuring ancilla in |0> state
"""
return self.backend_module.calculate_prob_zero(
results, ancilla_qubit, self.num_qubits
)
179 changes: 179 additions & 0 deletions testing/test_overlap_measurement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
#

import pytest
import numpy as np

from .conftest import TESTING_BACKENDS
from qumat import QuMat


class TestOverlapMeasurement:
"""Test overlap measurement functionality across different backends."""

def get_backend_config(self, backend_name):
"""Get backend configuration by name."""
configs = {
"qiskit": {
"backend_name": "qiskit",
"backend_options": {
"simulator_type": "aer_simulator",
"shots": 10000,
},
},
"cirq": {
"backend_name": "cirq",
"backend_options": {
"simulator_type": "default",
"shots": 10000,
},
},
"amazon_braket": {
"backend_name": "amazon_braket",
"backend_options": {
"simulator_type": "local",
"shots": 10000,
},
},
}
return configs.get(backend_name)

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_identical_zero_states(self, backend_name):
"""Test overlap measurement with two identical |0> states."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert overlap > 0.95

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_identical_one_states(self, backend_name):
"""Test overlap measurement with two identical |1> states."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_pauli_x_gate(1)
qumat.apply_pauli_x_gate(2)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert overlap > 0.95

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_orthogonal_states(self, backend_name):
"""Test overlap measurement with orthogonal states |0> and |1>."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_pauli_x_gate(2)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert overlap < 0.05

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_identical_plus_states(self, backend_name):
"""Test overlap measurement with two identical |+> states."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_hadamard_gate(1)
qumat.apply_hadamard_gate(2)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert overlap > 0.95

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_plus_minus_states(self, backend_name):
"""Test overlap measurement with |+> and |-> states."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_hadamard_gate(1)
qumat.apply_pauli_x_gate(2)
qumat.apply_hadamard_gate(2)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert overlap < 0.05

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_partial_overlap_states(self, backend_name):
"""Test overlap measurement with states having partial overlap."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_hadamard_gate(1)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert 0.4 < overlap < 0.6

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_rotated_states(self, backend_name):
"""Test overlap measurement with rotated states."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_ry_gate(1, np.pi / 4)
qumat.apply_ry_gate(2, np.pi / 4)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
assert overlap > 0.95

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_different_rotated_states(self, backend_name):
"""Test overlap measurement with differently rotated states."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
qumat.apply_ry_gate(1, np.pi / 4)
qumat.apply_ry_gate(2, np.pi / 2)
overlap = qumat.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)
expected_overlap = np.cos(np.pi / 8) ** 2
assert abs(overlap - expected_overlap) < 0.05

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_entangled_states_same(self, backend_name):
"""Test overlap measurement with identical entangled states (Bell states)."""
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=5)
qumat.apply_hadamard_gate(1)
qumat.apply_cnot_gate(1, 2)
qumat.apply_hadamard_gate(3)
qumat.apply_cnot_gate(3, 4)
overlap = qumat.measure_overlap(qubit1=1, qubit2=3, ancilla_qubit=0)
assert 0.0 <= overlap <= 1.0

def test_all_backends_consistency(self, testing_backends):
"""Test that all backends produce consistent results."""
results = {}
for backend_name in testing_backends:
qumat = QuMat(self.get_backend_config(backend_name))
qumat.create_empty_circuit(num_qubits=3)
results[backend_name] = qumat.measure_overlap(
qubit1=1, qubit2=2, ancilla_qubit=0
)

overlaps = list(results.values())
for i in range(len(overlaps)):
for j in range(i + 1, len(overlaps)):
assert abs(overlaps[i] - overlaps[j]) < 0.05

@pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
def test_measure_overlap_with_different_ancilla(self, backend_name):
"""Test overlap measurement with different ancilla qubit positions."""
backend_config = self.get_backend_config(backend_name)

qumat1 = QuMat(backend_config)
qumat1.create_empty_circuit(num_qubits=4)
qumat1.apply_hadamard_gate(1)
qumat1.apply_hadamard_gate(2)
overlap1 = qumat1.measure_overlap(qubit1=1, qubit2=2, ancilla_qubit=0)

qumat2 = QuMat(backend_config)
qumat2.create_empty_circuit(num_qubits=4)
qumat2.apply_hadamard_gate(0)
qumat2.apply_hadamard_gate(1)
overlap2 = qumat2.measure_overlap(qubit1=0, qubit2=1, ancilla_qubit=3)

assert overlap1 > 0.95
assert overlap2 > 0.95
assert abs(overlap1 - overlap2) < 0.05