Source code for qc_executor.pennylane.pennylane_operator
"""PennyLane operator conversion utilities for Qiskit-based quantum operators."""
from __future__ import annotations
from typing import TYPE_CHECKING, List, cast
import numpy as np
import pennylane as qml
import pennylane.numpy as pnp
from pennylane import pauli
from qiskit.circuit import ParameterExpression
from qiskit.quantum_info import SparsePauliOp
from sympy import lambdify
from ..base import QuantumOperatorBase
from ..utils.qiskit_compat import _param_is_constant, _param_to_sympy
from ._sympy_interface import _get_sympy_interface
if TYPE_CHECKING:
from ..quantum_operator import QuantumOperator
def _resolve_coefficient(coeff, symbol_tuple, printer, modules):
"""Convert a Qiskit operator coefficient to a float or PennyLane-compatible callable."""
if isinstance(coeff, ParameterExpression):
if not _param_is_constant(coeff):
symbol_expr = _param_to_sympy(coeff)
return lambdify(symbol_tuple, symbol_expr, modules=modules, printer=printer)
coeff = complex(coeff)
if np.imag(coeff) != 0:
raise ValueError("Imaginary part of operator coefficient is not supported")
return float(np.real(coeff))
[docs]
class PennyLaneOperator:
"""Convert generic quantum operators to PennyLane-native operators.
Args:
operator (QuantumOperatorBase | list[QuantumOperatorBase]):
Operator definition(s) to convert.
"""
[docs]
@classmethod
def from_quantum_operator(cls, operator: QuantumOperatorBase) -> "PennyLaneOperator":
"""Create a PennyLane native operator from a generic operator."""
return cls(operator)
def __init__(
self,
operator: QuantumOperatorBase | List[QuantumOperatorBase],
) -> None:
if isinstance(operator, QuantumOperatorBase):
self._qiskit_operator = cast("QuantumOperator", operator).qiskit_operator
self._num_qubits = self._qiskit_operator.num_qubits
elif isinstance(operator, list):
if all(isinstance(op, QuantumOperatorBase) for op in operator):
self._qiskit_operator = [
cast("QuantumOperator", op).qiskit_operator for op in operator
]
else:
raise ValueError("Unsupported operator type")
self._num_qubits = self._qiskit_operator[0].num_qubits
else:
raise ValueError("Unsupported operator type")
self._pennylane_operator_param_functions = []
self._pennylane_operator_parameters = []
self._pennylane_words = []
self._pennylane_operator_parameter_dimensions = {}
self.build_operator_instructions(self._qiskit_operator)
@property
def parameter_names(self) -> list:
"""List of operator parameter names"""
return self._pennylane_operator_parameters
@property
def parameter_dimensions(self) -> dict:
"""Dictionary with the dimension of each operator parameter"""
return self._pennylane_operator_parameter_dimensions
@property
def hash(self) -> int:
"""Hashable object of the circuit and operator for caching"""
return hash(str(self._qiskit_operator))
[docs]
def build_operator_instructions(self, operator: List[SparsePauliOp] | SparsePauliOp):
"""
Function to build the instructions for the PennyLane operator from the Qiskit operator.
This functions converts the Qiskit SparsePauli and parameter expressions to PennyLane
compatible Pauli words and functions.
Args:
operator (List[SparsePauliOp] | SparsePauliOp): Qiskit operator to convert
to PennyLane
Returns:
Tuple with lists of PennyLane operator parameter functions, PennyLane Pauli words,
PennyLane operator parameters and PennyLane operator parameter dimensions
"""
self._pennylane_operator_param_functions = []
self._pennylane_operator_parameters = []
self._pennylane_words = []
self._pennylane_operator_parameter_dimensions = {}
islist = True
if not isinstance(operator, list):
islist = False
operator = [operator]
def sort_parameters_after_index(parameter_vector):
index_list = [p.index for p in parameter_vector]
argsort_list = np.argsort(index_list)
return [parameter_vector[i] for i in argsort_list]
printer, modules = _get_sympy_interface()
for op in operator:
for param in op.parameters:
if param.vector.name not in self._pennylane_operator_parameters:
self._pennylane_operator_parameters.append(param.vector.name)
self._pennylane_operator_parameter_dimensions[param.vector.name] = 1
else:
self._pennylane_operator_parameter_dimensions[param.vector.name] += 1
# Handle operator parameter expressions and convert them to compatible python functions
symbol_tuple = tuple(
sum(
[
[_param_to_sympy(p) for p in sort_parameters_after_index(op.parameters)]
for op in operator
],
[],
)
)
self._pennylane_operator_param_functions = []
for op in operator:
coeffs = op.coeffs if op.coeffs is not None else []
self._pennylane_operator_param_functions.append(
[_resolve_coefficient(coeff, symbol_tuple, printer, modules) for coeff in coeffs]
)
# Convert Pauli strings into PennyLane Pauli words
for op in operator:
self._pennylane_words.append(
[pauli.string_to_pauli_word(p) for p in op.paulis.to_labels()]
)
if not islist:
self._pennylane_operator_param_functions = self._pennylane_operator_param_functions[0]
self._pennylane_words = self._pennylane_words[0]
[docs]
def build_pennylane_observable(self):
"""
Function to build the PennyLane circuit from the Qiskit circuit and observable.
The functions returns a callable PennyLane circuit that can be called with parameters.
The PennyLane circuit is built from the instructions previously generated from the Qiskit
circuit and observable.
Returns:
Callable PennyLane circuit
"""
def pennylane_observable(*args):
"""PennyLane circuit that can be called with parameters"""
# Collects the args values connected to the observable parameters
obs_param_list = sum(
[list(args[i]) for i in range(len(self._pennylane_operator_parameters))],
[],
)
if isinstance(self._qiskit_operator, list):
expval_list = []
for i, obs in enumerate(self._pennylane_words):
if len(obs_param_list) > 0:
coeff_list = [
coeff(*obs_param_list) if callable(coeff) else coeff
for coeff in self._pennylane_operator_param_functions[i]
]
expval_list.append(qml.expval(qml.Hamiltonian(coeff_list, obs)))
else:
# In case no parameters are present in the observable
# Calculate the expectation value of sum of the observables
# since this is more compatible with hardware backends
if len(self._pennylane_words[i]) == 0:
expval_list.append(0.0)
else:
expval_list.append(qml.expval(qml.sum(*self._pennylane_words[i])))
return pnp.stack(tuple(expval_list))
if len(obs_param_list) > 0:
coeff_list = [
coeff(*obs_param_list) if callable(coeff) else coeff
for coeff in self._pennylane_operator_param_functions
]
return qml.expval(qml.Hamiltonian(coeff_list, self._pennylane_words))
# In case no parameters are present in the observable
# Calculate the expectation value of sum of the observables
# since this is more compatible with hardware backends
if len(self._pennylane_words) == 0:
return 0.0
return qml.expval(qml.sum(*self._pennylane_words))
return pennylane_observable