Source code for qc_executor.qulacs.qulacs_circuit

"""Qulacs circuit wrapper for converting Qiskit circuits to Qulacs-compatible circuits."""

from __future__ import annotations

from typing import Any, Callable, Iterable, List, Optional

import numpy as np
from qiskit import QuantumCircuit as QiskitQuantumCircuit
from qiskit import transpile
from qiskit.circuit import ParameterExpression
from qiskit.circuit.parametervector import ParameterVectorElement
from qulacs import ParametricQuantumCircuit  # pylint: disable=no-name-in-module
from qulacs import QuantumCircuit as QulacsQuantumCircuit  # pylint: disable=no-name-in-module
from sympy import lambdify

from ..quantum_circuit import QuantumCircuit
from ..utils.decompose_to_std import decompose_to_std
from ..utils.qiskit_compat import _param_free_symbols, _param_to_sympy
from ..utils.qiskit_hash_functions import _circuit_key
from .qulacs_gates import qiskit_qulacs_gate_dict, qiskit_qulacs_param_gate_dict, qulacs_target


[docs] class QulacsCircuit: """Wrapper class that converts a generic QuantumCircuit into a Qulacs-compatible circuit."""
[docs] @classmethod def from_quantum_circuit(cls, circuit: QuantumCircuit) -> "QulacsCircuit": """Create a Qulacs native circuit from a generic circuit.""" return cls(circuit)
def __init__( self, circuit: QuantumCircuit, ) -> None: # Transpile circuit to supported basis gates and expand blocks automatically self._qiskit_circuit = transpile( decompose_to_std(circuit.qiskit_circuit), target=qulacs_target, optimization_level=0, ) self._num_qubits = self._qiskit_circuit.num_qubits # Build circuit instructions for the qulacs observable from the qiskit circuit self._operation_list = [] self._qubit_list = [] self._func_list = [] self._func_grad_list = [] self._free_parameters = set() self._used_parameters = [] self._qulacs_gates_parameters = {} self._symbol_tuple_circuit = tuple() self._rebuild_circuit_func = True self._circuit_func = None self._num_clbits = self._qiskit_circuit.num_clbits self._build_circuit_instructions(self._qiskit_circuit) self._circuit_func_cache = {} self._outer_jacobi_circuit_cache = {} self._qulacs_circuit = None @property def num_qubits(self) -> int: """Number of qubits of the circuit""" return self._num_qubits @property def qulacs_circuit(self) -> Optional[Callable]: """Qulacs circuit that can be called with parameters""" return self._qulacs_circuit @property def parameter_names(self) -> list: """List of circuit parameter names""" return list(self._qulacs_gates_parameters.keys()) @property def parameter_dimensions(self) -> dict: """Dictionary with the dimension of each circuit parameter""" return self._qulacs_gates_parameters @property def circuit_arguments(self) -> dict: """Dictionary of all circuit and observable parameters names""" return self._qulacs_gates_parameters @property def hash(self) -> int: """Hashable object of the circuit and observable for caching""" return hash(self._qiskit_circuit) @property def free_parameters(self) -> set: """Return the set of free (non-bound) parameters in the circuit.""" return self._free_parameters
[docs] def get_qulacs_circuit(self) -> Callable: """Builds and returns the Qulacs circuit as callable function""" self._qulacs_circuit = self.get_circuit_func() return self._qulacs_circuit
def __call__(self, *args, **kwargs): if self._qulacs_circuit is None: self._qulacs_circuit = self.get_circuit_func() return self._qulacs_circuit(*args, **kwargs) def __hash__(self): return hash(_circuit_key(self._qiskit_circuit)) def _add_parameter_expression( self, angle: float | ParameterVectorElement | ParameterExpression ) -> Any: """ Adds a parameter expression to the circuit and do the pre-processing. Args: angle (ParameterVectorElement or ParameterExpression or float): angle of rotation Returns: int: index of the parameter in the parameter vector callable: function to calculate the parameter expression callable: function to calculate the gradient of the parameter expression """ # In case angle is a float or something similar, we do not need to add a parameter func_list_element = None func_grad_list_element = None parameterized = False used_parameters = None # Change sign because of the way Qulacs defines the rotation gates angle = -angle if isinstance(angle, (float, int)): # Single float value func_list_element = angle func_grad_list_element = None elif isinstance(angle, ParameterVectorElement): # Single parameter vector element parameterized = True func_list_element = lambdify(self._symbol_tuple_circuit, _param_to_sympy(angle)) func_grad_list_element = [lambda x: 1.0] self._free_parameters.add(angle) used_parameters = [angle] elif isinstance(angle, ParameterExpression): # Parameter is in a expression (equation) parameterized = True func_list_element = lambdify(self._symbol_tuple_circuit, _param_to_sympy(angle)) func_grad_list_element = [] used_parameters = [] for param_element in _param_free_symbols(angle): self._free_parameters.add(param_element) used_parameters.append(param_element) # information about the gradient of the parameter expression param_grad = angle.gradient(param_element) if isinstance(param_grad, float): # create a call by value labmda function func_grad_list_element.append(lambda *arg, param_grad=param_grad: param_grad) else: func_grad_list_element.append( lambdify(self._symbol_tuple_circuit, _param_to_sympy(param_grad)) ) else: raise TypeError( f"Unsupported type for angle: {type(angle)}. " "Expected float, int, ParameterVectorElement or ParameterExpression." ) return func_list_element, func_grad_list_element, used_parameters, parameterized def _add_single_qubit_gate(self, gate_name: str, qubits: int | Iterable[int]): """ Adds a single qubit gate to the circuit. Args: gate_name (str): Name of the gate qubits (int or Iterable[int]): qubit indices """ qubits = [qubits] if isinstance(qubits, int) else qubits for q in qubits: self._operation_list.append(gate_name) if q >= self.num_qubits: raise ValueError(f"Qubit index {q} is out of range") self._qubit_list.append([q]) self._func_list.append(None) self._func_grad_list.append(None) self._used_parameters.append([]) self._rebuild_circuit_func = True def _add_two_qubit_gate( self, gate_name: str, qubit1: int | Iterable[int], qubit2: int | Iterable[int] ) -> None: """ Adds a two qubit gate to the circuit. Args: gate_name (str): Name of the gate qubit1 (int or Iterable[int]): qubit indices of the first qubit (e.g. control) qubit2 (int or Iterable[int]): qubit indices of the second qubit (e.g. target) """ qubit1 = [qubit1] if isinstance(qubit1, int) else qubit1 qubit2 = [qubit2] if isinstance(qubit2, int) else qubit2 for control, target in zip(qubit1, qubit2): if control >= self.num_qubits or target >= self.num_qubits: raise ValueError("Qubit index is out of range") self._operation_list.append(gate_name) self._qubit_list.append([control, target]) self._func_list.append(None) self._func_grad_list.append(None) self._used_parameters.append([]) self._rebuild_circuit_func = True def _add_three_qubit_gate(self, gate_name: str, qubit1: int, qubit2: int, qubit3: int) -> None: """ Adds a three qubit gate to the circuit. Args: gate_name (str): Name of the gate qubit1 (int): qubit index of the first qubit (e.g. first control) qubit2 (int): qubit index of the second qubit (e.g. second control) qubit3 (int): qubit index of the third qubit (e.g. target) """ for q in (qubit1, qubit2, qubit3): if q >= self.num_qubits: raise ValueError(f"Qubit index {q} is out of range") self._operation_list.append(gate_name) self._qubit_list.append([qubit1, qubit2, qubit3]) self._func_list.append(None) self._func_grad_list.append(None) self._used_parameters.append([]) self._rebuild_circuit_func = True def _add_parameterized_single_qubit_gate( self, gate_name: str, qubits: int | Iterable[int], angle: float | ParameterVectorElement | ParameterExpression, ): """ Adds a single qubit parameterized gate to the circuit. Args: gate_name (str): Name of the gate qubits (int or Iterable[int]): qubit indices angle (ParameterVectorElement or float): angle of rotation, ca be a parameter """ func_list_element, func_grad_list_element, used_parameters, parameterized = ( self._add_parameter_expression(angle) ) qubits = [qubits] if isinstance(qubits, int) else qubits for q in qubits: if q >= self.num_qubits: raise ValueError(f"Qubit index {q} is out of range") if parameterized: self._operation_list.append(gate_name) else: self._operation_list.append(gate_name) self._qubit_list.append([q]) self._func_list.append(func_list_element) self._func_grad_list.append(func_grad_list_element) if used_parameters is None: self._used_parameters.append([]) else: self._used_parameters.append(used_parameters) self._rebuild_circuit_func = True def _add_parameterized_two_qubit_gate( self, gate_name: str, qubit1: int | Iterable[int], qubit2: int | Iterable[int], angle: float | ParameterVectorElement | ParameterExpression, ): """ Adds a single qubit parameterized gate to the circuit. Args: gate_name (str): Name of the gate qubits (int or Iterable[int]): qubit indices angle (ParameterVectorElement or float): angle of rotation, ca be a parameter """ func_list_element, func_grad_list_element, used_parameters, parameterized = ( self._add_parameter_expression(angle) ) qubit1 = [qubit1] if isinstance(qubit1, int) else qubit1 qubit2 = [qubit2] if isinstance(qubit2, int) else qubit2 for control, target in zip(qubit1, qubit2): if control >= self.num_qubits or target >= self.num_qubits: raise ValueError("Qubit index is out of range") if parameterized: self._operation_list.append(gate_name) else: self._operation_list.append(gate_name) self._qubit_list.append([control, target]) self._func_list.append(func_list_element) self._func_grad_list.append(func_grad_list_element) if used_parameters is None: self._used_parameters.append([]) else: self._used_parameters.append(used_parameters) self._rebuild_circuit_func = True def _build_circuit_instructions(self, circuit: QiskitQuantumCircuit) -> None: """ Function to build the instructions for the Qulacs circuit from the Qiskit circuit. This functions converts the Qiskit gates and parameter expressions to Qulacs compatible gates and functions. Args: circuit (QiskitQuantumCircuit): Qiskit circuit to convert to Qulacs """ self._operation_list = [] self._param_list = [] self._qubit_list = [] self._func_list = [] self._func_grad_list = [] self._free_parameters = set() self._qulacs_gates_parameters = {} self._symbol_tuple_circuit = tuple() for param in circuit.parameters: name = param.vector.name if name not in self._qulacs_gates_parameters: self._qulacs_gates_parameters[name] = 1 else: self._qulacs_gates_parameters[name] += 1 self._symbol_tuple_circuit = tuple(_param_to_sympy(p) for p in circuit.parameters) for gate_operation in circuit.data: # Barriers are visualization/compiler directives with no effect on the # statevector, so they are skipped during conversion. if gate_operation.operation.name == "barrier": continue # catch conditions of the gate # only c_if is supported, the other cases have been caught before if ( hasattr(gate_operation.operation, "condition") and gate_operation.operation.condition is not None or gate_operation.operation.name == "measure" ): raise NotImplementedError( "Conditions are not supported in sQUlearn's Qulacs backend." ) if ( gate_operation.operation.name not in qiskit_qulacs_gate_dict and gate_operation.operation.name not in qiskit_qulacs_param_gate_dict ): raise NotImplementedError( f"Gate {gate_operation.operation.name} is unfortunatly not supported " "in sQUlearn's Qulacs backend." ) paramterized_gate = len(gate_operation.operation.params) >= 1 single_qubit_date = len(gate_operation.qubits) == 1 wires = [ circuit.find_bit(gate_operation.qubits[i]).index for i in range(gate_operation.operation.num_qubits) ] if single_qubit_date: if not paramterized_gate: self._add_single_qubit_gate(gate_operation.operation.name, wires) else: self._add_parameterized_single_qubit_gate( gate_operation.operation.name, wires, gate_operation.operation.params[0] ) elif len(gate_operation.qubits) == 2: if not paramterized_gate: self._add_two_qubit_gate(gate_operation.operation.name, wires[0], wires[1]) else: self._add_parameterized_two_qubit_gate( gate_operation.operation.name, wires[0], wires[1], gate_operation.operation.params[0], ) elif len(gate_operation.qubits) == 3 and not paramterized_gate: self._add_three_qubit_gate( gate_operation.operation.name, wires[0], wires[1], wires[2] ) else: raise NotImplementedError( "Only up to three qubit (non-parameterized) gates are supported " "in sQUlearn's Qulacs backend." )
[docs] def get_circuit_func(self, gradient_param=None): """Returns the Qulacs circuit function for the circuit.""" if isinstance(gradient_param, ParameterVectorElement): gradient_param = [gradient_param] gradient_param = list(gradient_param) if gradient_param is not None else [] is_parameterized = len(gradient_param) parameterized_operations = [ any(param in gradient_param for param in self._used_parameters[i]) for i, _ in enumerate(self._operation_list) ] cache_value = "no_gradient" if is_parameterized: cache_value = tuple(gradient_param) if cache_value in self._circuit_func_cache: return self._circuit_func_cache[cache_value] def qulacs_circuit(*args): # Collects the args values connected to the circuit parameters circ_param_list = sum([list(args[i]) for i in range(len(self.parameter_names))], []) if is_parameterized: circuit = ParametricQuantumCircuit(self.num_qubits) else: circuit = QulacsQuantumCircuit(self.num_qubits) # Build the Qulacs circuit and evaluate the parametric terms for i, qulacs_operation in enumerate(self._operation_list): if self._func_list[i] is None: qiskit_qulacs_gate_dict[qulacs_operation](circuit, *self._qubit_list[i]) elif isinstance(self._func_list[i], (float, int)): qiskit_qulacs_gate_dict[qulacs_operation]( circuit, self._func_list[i], *self._qubit_list[i] ) else: value = self._func_list[i](*circ_param_list) if parameterized_operations[i]: assert isinstance(circuit, ParametricQuantumCircuit) qiskit_qulacs_param_gate_dict[qulacs_operation]( circuit, value, *self._qubit_list[i] ) else: qiskit_qulacs_gate_dict[qulacs_operation]( circuit, value, *self._qubit_list[i] ) return circuit self._circuit_func_cache[cache_value] = qulacs_circuit return qulacs_circuit
[docs] def get_gradient_outer_jacobian( 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)} cache_value = "no_gradient" if len(gradient_parameters) > 0: cache_value = tuple(gradient_parameters) if cache_value in self._outer_jacobi_circuit_cache: return self._outer_jacobi_circuit_cache[cache_value] def outer_jacobian(*args): # Collects the args values connected to the circuit parameters circ_param_list = sum([list(args[i]) for i in range(len(self.parameter_names))], []) relevant_operations = [ i for i in range(len(self._operation_list)) if any(param in gradient_parameters for param in self._used_parameters[i]) ] outer_jacobian = np.zeros((len(relevant_operations), len(gradient_parameters))) for i, operation in enumerate(relevant_operations): for j, param in enumerate(self._used_parameters[operation]): if param in gradient_parameters: outer_jacobian[i, gradient_param_dict[param]] = self._func_grad_list[ operation ][j](*circ_param_list) return outer_jacobian self._outer_jacobi_circuit_cache[cache_value] = outer_jacobian return outer_jacobian