"""
A collection of utility functions that can be used for preprocessing
and postprocessing of the test process.
"""
from qiskit.quantum_info import Statevector, partial_trace, DensityMatrix
import numpy as np
[docs]
def int_to_gate_list(value: int, n_bits: int) -> list[str]:
"""
Convert a decimal integer to gate list representation
of a quantum state using binary encoding.
Args:
value: Non-negative integer to convert
n_bits: Number of bits in the output gate list
Returns:
Representation of a gate list
Raises:
ValueError: if ``value`` is negative, or if ``n_bits`` is not positive
TypeError: if ``value`` is not an integer, or if ``n_bits`` is not an integer
Example:
>>> int_to_gate_list(3, 4)
['x', 'x', 'i', 'i']
# 3 = 0b0011
# (q0, q1, q2, q3) -> ('x', 'x', 'i', 'i')
"""
if value < 0:
raise ValueError("value must be non-negative")
if not isinstance(n_bits, int) or n_bits <= 0:
raise ValueError("n_bits must be a positive integer")
if not isinstance(value, int):
raise TypeError("value must be an integer")
gates = []
for i in range(n_bits):
bit = (value >> i) & 1
gates.append("x" if bit == 1 else "i")
return gates
[docs]
def frac_to_gate_list(value: float, n_bits: int, tol: float = 1e-9) -> list[str]:
r"""
Convert a fractional decimal number to a quantum gate list
using fixed-point binary representation, with precision check.
Args:
value: Fractional decimal number to convert (must be in :math:`[0, 1)`)
n_bits: Number of bits in the output gate list (must be positive)
tol: Tolerance for precision check (default: 1e-9)
Returns:
Representation of a gate list
Raises:
ValueError: if ``value`` is not in :math:`[0, 1)`, or
if ``n_bits`` is not positive
TypeError: if ``value`` is not a float, or if ``n_bits``
is not an integer
Example:
>>> frac_to_gate_list(0.375, 4) # 0.25 + 0.125 = 0.011 in binary
['i', 'x', 'x', 'i']
"""
if not (0 <= value < 1):
raise ValueError("value must be in [0, 1)")
if n_bits <= 0:
raise ValueError("n_bits must be positive")
gates = []
remaining = value
for i in range(1, n_bits + 1):
weight = 2 ** (-i)
if remaining >= weight:
gates.append("x")
remaining -= weight
else:
gates.append("i")
# Precision check
if remaining > tol:
raise ValueError(
f"Precision loss too large: remaining={remaining}, tol={tol}"
)
return gates
[docs]
def counts_to_probabilities(counts: dict[str, int]) -> dict[str, float]:
"""
Convert counts to probabilities.
Args:
counts: Measurement counts as a dictionary mapping bitstrings to their counts
Returns:
A dictionary mapping bitstrings to their probabilities.
Example:
>>> counts_to_probabilities({'00': 500, '01': 300, '10': 100, '11': 100})
{'00': 0.5, '01': 0.3, '10': 0.1, '11': 0.1}
"""
probabilities = {}
shots = sum(counts.values())
for bitstring, count in counts.items():
probabilities[bitstring] = count / shots
return probabilities
[docs]
def counts_to_expectation(
counts: dict[str, int],
observable: dict[str, float]
) -> float:
"""
Convert counts to expectation value of a given observable.
Args:
counts: Measurement counts as a dictionary mapping bitstrings to their counts
observable: A dictionary mapping bitstrings to their observable values
Returns:
The expectation value as a float.
Example:
>>> counts_to_expectation({'00': 500, '01': 300, '10': 100, '11': 100},
... {'00': 1.0, '01': -1.0, '10': -1.0, '11': 1.0})
0.2 # (0.5*1.0 + 0.3*(-1.0) + 0.1*(-1.0) + 0.1*1.0 = 0.2)
"""
expectation = 0.0
shots = sum(counts.values())
for bitstring, count in counts.items():
prob = count / shots
obs_value = observable.get(bitstring, 0.0)
expectation += prob * obs_value
return expectation
[docs]
def decompose_joint_state(
statevector: Statevector,
target_subsystem: list[int],
atol: float = 1e-10,
) -> tuple[str, Statevector | DensityMatrix]:
r"""
Return the reduced density matrix of the given subsystem.
Args:
statevector: Joint pure state :math:`\ket{\psi_{AB}}` of the entire system
consisting of subsystems :math:`A` and :math:`B`, represented as a Statevector object.
target_subsystem: List of qubit indices for the target subsystem :math:`A`
atol: Absolute tolerance for numerical operations
Returns:
A tuple containing a string indicating the type (``'pure_state'``
or ``'mixed_state'``) and the corresponding ``Statevector``
or ``DensityMatrix`` of the target subsystem :math:`A`.
Raises:
ValueError: if ``target_subsystem`` indices exceed the number of qubits
in statevector
Example:
>>> from qiskit.quantum_info import Statevector
>>> import numpy as np
>>> psi_AB = Statevector.from_label('00') + Statevector.from_label('11')
>>> psi_AB = psi_AB / np.linalg.norm(psi_AB.data) # Normalize
>>> decompose_joint_state(psi_AB, target_subsystem=[0])
('mixed_state', DensityMatrix([[0.5+0.j, 0. +0.j],
[0. +0.j, 0.5+0.j]],
dims=(2,)))
"""
num_qubits: int = statevector.num_qubits if statevector.num_qubits is not None else 0
if num_qubits <= max(target_subsystem):
raise ValueError("target_subsystem indices exceed number of qubits in statevector")
# The qubits to be traced out
trace_out = [i for i in range(num_qubits) if i not in target_subsystem]
# Partial trace
rho_subsystem = partial_trace(statevector, trace_out)
# Determine whether it is in a pure state
if abs(rho_subsystem.purity() - 1.0) < atol:
# Extract the state vector of the pure state
# For a pure state, rho = |psi><psi|, the largest eigenvalue is 1
eigenvals, eigenvecs = np.linalg.eigh(rho_subsystem.data)
psi = eigenvecs[:, np.argmax(eigenvals)]
return "pure_state", Statevector(psi)
# Mixed state: directly return the density matrix
return "mixed_state", DensityMatrix(rho_subsystem)