Source code for qc_executor.qulacs.qulacs_operator

"""Qulacs operator implementation for quantum expectation value computation."""

from __future__ import annotations

from typing import Any, List, cast

import numpy as np
import sympy as sp
from qiskit.circuit import ParameterExpression
from qiskit.circuit.parametervector import ParameterVectorElement
from qiskit.quantum_info import SparsePauliOp
from qulacs import GeneralQuantumOperator, PauliOperator  # pylint: disable=no-name-in-module
from sympy import lambdify

from ..base import QuantumOperatorBase
from ..utils.qiskit_compat import _param_free_symbols, _param_to_sympy


[docs] class QulacsOperator: """Qulacs native operator wrapper for expectation value and gradient computation."""
[docs] @classmethod def from_quantum_operator( cls, operator: QuantumOperatorBase | List[QuantumOperatorBase] ) -> "QulacsOperator": """Create a Qulacs native operator from generic operator(s).""" return cls(operator)
def __init__( self, operator: QuantumOperatorBase | List[QuantumOperatorBase], ) -> None: if isinstance(operator, QuantumOperatorBase): self._qiskit_operator = cast(Any, operator).qiskit_operator self._num_qubits = self._qiskit_operator.num_qubits elif isinstance(operator, list): if all(isinstance(obs, QuantumOperatorBase) for obs in operator): self._qiskit_operator = [cast(Any, obs).qiskit_operator for obs in operator] else: raise ValueError("Unsupported operator type") self._num_qubits = self._qiskit_operator[0].num_qubits else: raise ValueError("Unsupported operator type") self.new_operators = [] self.new_operators_coeff = [] self.new_operators_coeff_grad = [] self.new_operators_used_parameters = [] self._qulacs_op_parameters = {} self._free_parameters = set() self.build_operator_instructions(self._qiskit_operator) self._outer_jacobi_obs_cache = {} @property def num_qubits(self) -> int: """Number of qubits of the circuit""" return self._num_qubits @property def parameter_names(self) -> list: """List of operator parameter names""" return list(self._qulacs_op_parameters.keys()) @property def parameter_dimensions(self) -> dict: """Dictionary with the dimension of each circuit parameter""" return self._qulacs_op_parameters @property def hash(self) -> str: """Hashable object of the circuit and operator for caching""" return str(self._qiskit_operator) @property def free_parameters(self) -> set: """Return the set of free (non-bound) parameters in the operator.""" return self._free_parameters def _build_coeff_functions(self, c): """Build coefficient function and gradient functions for a single operator term. Args: c: The coefficient, which may be a ParameterVectorElement, ParameterExpression, or a plain numeric value. Returns: Tuple of (coeff_func, grad_funcs, used_params). """ if isinstance(c, ParameterVectorElement): self._free_parameters.add(c) return lambdify(self._symbol_tuple_obs, _param_to_sympy(c)), [lambda *arg: 1.0], [c] if isinstance(c, ParameterExpression): coeff_func = lambdify(self._symbol_tuple_obs, _param_to_sympy(c)) grad_funcs = [] used_params = [] for param_element in _param_free_symbols(c): self._free_parameters.add(param_element) used_params.append(param_element) try: param_grad = c.gradient(param_element) except ( TypeError, ValueError, AttributeError, NotImplementedError, ): c_sym = _param_to_sympy(c) p_sym = _param_to_sympy(param_element) param_grad = c_sym.diff(p_sym) if isinstance(param_grad, complex) and param_grad.imag == 0: param_grad = param_grad.real if isinstance(param_grad, (float, complex)): grad_funcs.append(lambda *arg, param_grad=param_grad: param_grad) elif isinstance(param_grad, sp.Basic): grad_funcs.append(lambdify(self._symbol_tuple_obs, param_grad)) else: grad_funcs.append( lambdify(self._symbol_tuple_obs, _param_to_sympy(param_grad)) ) return coeff_func, grad_funcs, used_params return lambda *arg, c=c: c, [lambda *arg: 0.0], []
[docs] def build_operator_instructions(self, operator: List[SparsePauliOp] | SparsePauliOp): """ Function to build the instructions for the Qulacs operator from the Qiskit operator. This functions converts the Qiskit SparsePauli and parameter expressions to Qulacs compatible Pauli words and functions. Args: operator (List[SparsePauliOp] | SparsePauliOp): Qiskit operator to convert to Qulacs Returns: Tuple with lists of Qulacs operator parameter functions, Qulacs Pauli words, Qulacs operator parameters and Qulacs operator parameter dimensions """ self.multiple_operators = False if isinstance(operator, SparsePauliOp): operator = [operator] elif isinstance(operator, list): self.multiple_operators = True else: raise ValueError("Unsupported operator type") self._symbol_tuple_obs = tuple() self._qulacs_op_parameters = {} for op in operator: for param in op.parameters: name = param.vector.name if name not in self._qulacs_op_parameters: self._qulacs_op_parameters[name] = 1 else: self._qulacs_op_parameters[name] += 1 self._symbol_tuple_obs = tuple( sum( [ [ _param_to_sympy(p) for p in sorted(op.parameters, key=lambda param: param.index) ] for op in operator ], [], ) ) # new version self.new_operators = [] self.new_operators_coeff = [] self.new_operators_coeff_grad = [] self.new_operators_used_parameters = [] for op in operator: paulis = [str(p) for p in op.paulis] coeff = list(np.real_if_close(np.asarray(cast(Any, op.coeffs)))) new_operator = [] new_operators_coeff = [] new_operators_coeff_grad = [] new_operators_used_parameters = [] for c, p in zip(coeff, paulis): string = " ".join(f"{p_} {i}" for i, p_ in enumerate(p)) + " " coeff_func, grad_funcs, used_params = self._build_coeff_functions(c) new_operator.append(string) new_operators_coeff.append(coeff_func) new_operators_coeff_grad.append(grad_funcs) new_operators_used_parameters.append(used_params) self.new_operators.append(new_operator) self.new_operators_coeff.append(new_operators_coeff) self.new_operators_coeff_grad.append(new_operators_coeff_grad) self.new_operators_used_parameters.append(new_operators_used_parameters)
[docs] def get_operator_func(self): """Returns the Qulacs operator function for the operator depending on parameters.""" def operator_func(*args): list_operators = [] for i, operator in enumerate(self.new_operators): new_operator = GeneralQuantumOperator(self.num_qubits) for j, op in enumerate(operator): new_operator.add_operator(self.new_operators_coeff[i][j](*args), op) list_operators.append(new_operator) return list_operators return operator_func
[docs] def get_gradient_outer_jacobian_operators_new( self, gradient_parameters: ParameterVectorElement | List[ParameterVectorElement] | None = None, ): """Returns the outer jacobian needed for the chain rule in circuit derivatives. Qulacs does not support multiple parameters and parameter expressions, so we need to calculate a transformation which also includes the gradient of the parameter expression. Args: gradient_parameters (ParameterVectorElement | List[ParameterVectorElement] | None): Parameters to calculate the gradient for. """ if isinstance(gradient_parameters, ParameterVectorElement): gradient_parameters = [gradient_parameters] gradient_parameters = list(gradient_parameters) if gradient_parameters is not None else [] gradient_param_dict = {p: i for i, p in enumerate(gradient_parameters)} def outer_jacobian(*args): # Collects the args values connected to the operator parameters op_param_list = sum([list(args[i]) for i in range(len(self.parameter_names))], []) outer_jacobians = [] for iop, operator in enumerate(self.new_operators_coeff_grad): relevant_operations = [ i for i in range(len(operator)) if any( param in gradient_parameters for param in self.new_operators_used_parameters[iop][i] ) ] outer_jacobian = np.zeros((len(relevant_operations), len(gradient_parameters))) for i, operation in enumerate(relevant_operations): for j, param in enumerate(self.new_operators_used_parameters[iop][operation]): if param in gradient_parameters: outer_jacobian[i, gradient_param_dict[param]] = ( self.new_operators_coeff_grad[iop][operation][j](*op_param_list) ) outer_jacobians.append(outer_jacobian) return outer_jacobians return outer_jacobian
[docs] def get_operators_for_gradient( self, gradient_parameters: ParameterVectorElement | List[ParameterVectorElement] | None = None, ): """Returns the Qulacs operator function for the operators depending on parameters.""" if isinstance(gradient_parameters, ParameterVectorElement): gradient_parameters = [gradient_parameters] gradient_parameters = list(gradient_parameters) if gradient_parameters is not None else [] def operator_func(*_args): list_operators = [] for iop, operator in enumerate(self.new_operators): relevant_operations = [ i for i in range(len(operator)) if any( param in gradient_parameters for param in self.new_operators_used_parameters[iop][i] ) ] list_paulis = [] for op in relevant_operations: list_paulis.append(PauliOperator(operator[op], 1.0)) list_operators.append(list_paulis) return list_operators return operator_func