Source code for qc_executor.qulacs.qulacs_executor

"""Qulacs executor implementation."""

from __future__ import annotations

import re
from collections import Counter
from itertools import product
from typing import Any, List, Tuple, cast

import numpy as np
from qiskit.circuit import ParameterVector
from qiskit.circuit.parametervector import ParameterVectorElement
from qulacs import ParametricQuantumCircuit  # pylint: disable=no-name-in-module
from qulacs import QuantumState  # pylint: disable=no-name-in-module

from ..base import ExecutorBase, QuantumCircuitBase, QuantumOperatorBase
from ..quantum_circuit import QuantumCircuit
from ..utils.data_preprocessing import adjust_features, to_tuple
from .qulacs_circuit import QulacsCircuit
from .qulacs_operator import QulacsOperator


[docs] class QulacsExecutor(ExecutorBase): """Qulacs backend executor implementation. Args: shots (int | None, optional): Number of shots for sampling. seed (int | None, optional): Random seed for reproducibility. log_file (str | None, optional): Path to the log file. log_level (str, optional): Logging level. caching (bool | None, optional): Whether to use in-memory caching. cache_dir (str, optional): Directory for caching. max_cache_size (int | None, optional): Maximum number of entries kept in each in-memory cache. """ _native_circuit_class = QulacsCircuit _native_operator_class = QulacsOperator def __init__( self, shots: int | None = None, seed: int | None = None, log_file: str | None = None, log_level: str = "WARNING", caching: bool | None = None, cache_dir: str = "cache", max_cache_size: int | None = None, ): super().__init__( shots=shots, seed=seed, log_file=log_file, log_level=log_level, caching=caching, cache_dir=cache_dir, max_cache_size=max_cache_size, ) self._circuit_cache = self._make_cache() self._operator_cache = self._make_cache() self.result_container = {} if seed is not None: self._random = np.random.default_rng(seed) else: self._random = np.random.default_rng() @property def shots(self) -> int | None: """Return the number of shots.""" return self._shots @shots.setter def shots(self, value: int | None) -> None: """Set the number of shots.""" raise NotImplementedError @property def remote(self) -> bool: """Return True if the execution access a remote backend.""" return False def _preprocess_circuits( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase] ) -> Tuple[List[QulacsCircuit], bool]: """Preprocess the circuit(s) and convert them to Qulacs format. Args: circuit (QuantumCircuitBase | List[QuantumCircuitBase]): The quantum circuit(s) to preprocess. Returns: Tuple[List[QulacsCircuit], bool]: A tuple containing a list of QulacsCircuit objects and a boolean indicating whether multiple circuits were provided. """ multiple_circuits = True circuits: List[QuantumCircuitBase] = circuit if isinstance(circuit, list) else [circuit] if not isinstance(circuit, list): multiple_circuits = False qulacs_circuits = [] # Check the cache for already converted circuits for circ in circuits: if isinstance(circ, self._native_circuit_class): qulacs_circuits.append(circ) continue if circ in self._circuit_cache: self._logger.debug("Circuit cache hit for %s", circ) qulacs_circuits.append(self._circuit_cache[circ]) else: self._logger.debug("Circuit cache miss – converting circuit %s", circ) qulacs_circuit = QulacsCircuit(cast(QuantumCircuit, circ)) self._circuit_cache[circ] = qulacs_circuit qulacs_circuits.append(qulacs_circuit) return qulacs_circuits, multiple_circuits def _preprocess_operators( self, operator: QuantumOperatorBase | List[QuantumOperatorBase] ) -> Tuple[List[QulacsOperator], bool]: """Preprocess the operator(s) and convert them to Qulacs format. Args: operator (QuantumOperatorBase | List[QuantumOperatorBase]): The quantum operator(s) to preprocess. Returns: Tuple[List[QulacsOperator], bool]: A tuple containing a list of QulacsOperator objects and a boolean indicating whether multiple operators were provided. """ multiple_operators = True operators: List[QuantumOperatorBase] = ( operator if isinstance(operator, list) else [operator] ) if not isinstance(operator, list): multiple_operators = False qulacs_operators = [] for op in operators: if isinstance(op, self._native_operator_class): qulacs_operators.append(op) continue if op in self._operator_cache: self._logger.debug("Operator cache hit for %s", op) qulacs_operators.append(self._operator_cache[op]) else: self._logger.debug("Operator cache miss – converting operator %s", op) qulacs_operator = QulacsOperator(op) self._operator_cache[op] = qulacs_operator qulacs_operators.append(qulacs_operator) return qulacs_operators, multiple_operators def _expectation_value( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], observable: QuantumOperatorBase | List[QuantumOperatorBase], **parameter_values, ) -> float | np.ndarray: """ Calculate the expectation value of the observable with respect to the circuit. Args: circuit (QuantumCircuitBase | List[QuantumCircuitBase]): The quantum circuit(s). observable (QuantumOperatorBase | List[QuantumOperatorBase]): The quantum observable(s). Returns: float | np.ndarray: The expectation value(s). """ qulacs_circuits, multiple_circuits = self._preprocess_circuits(circuit) qulacs_observables, multiple_observables = self._preprocess_operators(observable) values = [] # Multiple circuits and multiple observables are supported and handled by the # nested loops below. Only a single parameter set per call is supported, i.e. # batched/multiple parameter sets are not implemented (this is consistent with # the PennyLane backend). for qulacs_circuit in qulacs_circuits: circuit_parameters = [] multiple_circuit_parameters = [] circuit_parameters_dimension = [] circuit_values = [] for param in qulacs_circuit.parameter_names: if param not in parameter_values: raise ValueError( f"Parameter '{param}' not found in provided parameter values." ) param_values, multiple_params = adjust_features( parameter_values[param], qulacs_circuit.parameter_dimensions[param] ) circuit_parameters.append(param_values) multiple_circuit_parameters.append(multiple_params) circuit_parameters_dimension.append(qulacs_circuit.parameter_dimensions[param]) circuit_parameter_tuples = product(*circuit_parameters) for cp in circuit_parameter_tuples: cp_values = [] qulacs_circuit_object = qulacs_circuit.get_circuit_func()(*cp) state = QuantumState(qulacs_circuit.num_qubits) qulacs_circuit_object.update_quantum_state(state) for qulacs_observable in qulacs_observables: observable_values = [] observable_parameters = [] multiple_observable_parameters = [] observable_parameters_dimension = [] for param in qulacs_observable.parameter_names: if param not in parameter_values: raise ValueError( f"Parameter '{param}' not found in provided parameter values." ) param_values, multiple_params = adjust_features( parameter_values[param], qulacs_observable.parameter_dimensions[param] ) observable_parameters.append(param_values) multiple_observable_parameters.append(multiple_params) observable_parameters_dimension.append( qulacs_observable.parameter_dimensions[param] ) observable_parameter_tuples = product(*observable_parameters) for obs in observable_parameter_tuples: qulacs_observable_object = qulacs_observable.get_operator_func()( *obs[0] if obs else () ) # not sure about the [0] here, but it works for single observables observable_values.append( np.real_if_close( np.array( [ o.get_expectation_value(state) for o in qulacs_observable_object ][0] ) ) ) # check for multiple parameter sets cp_values.append(observable_values) circuit_values.append(cp_values) values.append(circuit_values) values = np.array(values) # Remove the parameter dimension list (has to be fixed for multiple parameters) shape = list(values.shape) shape.pop(1) shape.pop(-1) values = values.reshape(shape) if not multiple_circuits: values = values[0] if not multiple_observables: values = values[0] else: if not multiple_observables: values = values.reshape(-1) return values def _expectation_value_derivatives( self, circuit: QuantumCircuitBase, observable: QuantumOperatorBase, *values: str | ParameterVector | ParameterVectorElement | tuple, **parameter_values, ) -> float | np.ndarray | dict: """ Calculate the derivatives of the expectation value with respect to the parameters Args: circuit (QuantumCircuitBase): The quantum circuit. observable (QuantumOperatorBase): The quantum observable. values: Values for which the derivatives are calculated. Can be strings (e.g. "expectation_value" or the name of parameters), or ParameterVectors, ParameterVectorElements. Tuples are used for higher order derivatives. parameter_values: Parameters to evaluate the circuit and observable given as keyword arguments. Returns: np.array | dict: The derivatives of the expectation value. If a single value is provided, a numpy array is returned. If multiple values are provided, a dictionary with the values as keys and the derivatives as values is returned. """ def evaluate_circuit_gradient( circuit: QulacsCircuit, observable: QulacsOperator, arguments_circuit, arguments_observable, parameters: ParameterVectorElement | List[ParameterVectorElement] | None = None, ) -> np.ndarray: """ Function to evaluate the Qulacs Circuits with the given parameters. Computes the gradient of the expectation values of the observables defined in the circuit data structure. Args: circuit (QulacsCircuit): Qulacs circuit to evaluate observable (QulacsOperator): Qulacs observable to evaluate arguments_circuit: Arguments for the circuit arguments_observable: Arguments for the observable parameters (List[float]): List of circuit parameters wrt. the gradient is computed Returns: np.ndarray: Result of the evaluation """ qulacs_circuit = circuit.get_circuit_func(parameters)(*arguments_circuit) outer_jacobian = circuit.get_gradient_outer_jacobian(parameters)(*arguments_circuit) observable_args = arguments_observable[0] if len(arguments_observable) > 0 else [] qulacs_observable = observable.get_operator_func()(*observable_args) if isinstance(parameters, ParameterVectorElement): parameters = [parameters] parameters = list(parameters) if parameters is not None else [] is_parameterized = len(parameters) if is_parameterized: param_circuit = cast(ParametricQuantumCircuit, qulacs_circuit) param_values = np.array( [ outer_jacobian.T @ np.array(param_circuit.backprop(o)) for o in qulacs_observable ] ) else: param_values = np.array([[]]) values = np.real_if_close(param_values) if not observable.multiple_operators: return values[0] return values def evaluate_observable_gradient( circuit: QulacsCircuit, observable: QulacsOperator, arguments_circuit, arguments_observable, parameters: ParameterVectorElement | List[ParameterVectorElement] | None = None, ) -> np.ndarray: """ Function to evaluate the Qulacs Observables with the given parameters. Computes the gradient of the expectation values of the observables defined in the circuit data structure. Args: circuit (QulacsCircuit): Qulacs circuit to evaluate observable (QulacsOperator): Qulacs observable to evaluate arguments_circuit: Arguments for the circuit arguments_observable: Arguments for the observable parameters (List[float]): List of observable parameters wrt. the gradient is computed Returns: np.ndarray: Result of the evaluation """ qulacs_circuit = circuit.get_circuit_func(parameters)(*arguments_circuit) state = QuantumState(circuit.num_qubits) qulacs_circuit.update_quantum_state(state) operators = observable.get_operators_for_gradient(parameters)(*arguments_observable[0]) outer_jacobian = observable.get_gradient_outer_jacobian_operators_new(parameters)( *arguments_observable ) param_obs_values = [ outer_jacobian[i].T @ np.array( [ o if isinstance(o, float) else o.get_expectation_value(state) for o in operator ] ) for i, operator in enumerate(operators) ] values = np.real_if_close(param_obs_values) if not observable.multiple_operators: return values[0] return values def remove_brackets(s: str) -> str: return re.sub(r"\[.*?\]", "", s) qulacs_circuits, _ = self._preprocess_circuits(circuit) qulacs_observables, _ = self._preprocess_operators(observable) # Derivatives for multiple circuits or observables are not implemented. Raise an # explicit error instead of silently dropping all but the first element, which # would otherwise return a plausible but wrong result. if len(qulacs_circuits) > 1 or len(qulacs_observables) > 1: raise NotImplementedError( "Derivatives for multiple circuits or observables are not supported. " "Please call expectation_value_derivatives with a single circuit and " "a single observable." ) qulacs_circuit = qulacs_circuits[0] qulacs_observable = qulacs_observables[0] circuit_parameters = [] multiple_circuit_parameters = [] circuit_parameters_dimension = [] # preprocess the parameter values for param in qulacs_circuit.parameter_names: if param not in parameter_values: raise ValueError(f"Parameter '{param}' not found in provided parameter values.") param_values, multiple_params = adjust_features( parameter_values[param], qulacs_circuit.parameter_dimensions[param] ) circuit_parameters.append(param_values[0]) multiple_circuit_parameters.append(multiple_params) circuit_parameters_dimension.append(qulacs_circuit.parameter_dimensions[param]) observable_parameters = [] multiple_observable_parameters = [] observable_parameters_dimension = [] for param in qulacs_observable.parameter_names: if param not in parameter_values: raise ValueError(f"Parameter '{param}' not found in provided parameter values.") param_values, multiple_params = adjust_features( parameter_values[param], qulacs_observable.parameter_dimensions[param] ) observable_parameters.append(param_values[0]) multiple_observable_parameters.append(multiple_params) observable_parameters_dimension.append(qulacs_observable.parameter_dimensions[param]) result_dict = {} if values is None or len(values) == 0: values = ("expectation_value",) # Convert and sort the values value_list: list = list(values) indices = np.argsort([str(t) for t in value_list]) value_list = [value_list[i] for i in indices] value_list = [to_tuple(cast(Any, v)) for v in value_list] # Loop over all requested derivatives for todo in value_list: if len(todo) > 1: raise ValueError( "Higher order derivatives are not supported with qulacs, " "please use pennylane" ) # get the parameter objects for the requested circuit derivatives parameter_vector = [] for param in qulacs_circuit.free_parameters: if isinstance(todo[0], str): if todo[0] == "" or todo[0] == "expectation_value": pass elif todo[0] == remove_brackets(param.name): parameter_vector.append(param) elif todo[0] == param.name: parameter_vector.append(param) elif isinstance(todo[0], ParameterVectorElement): if param == todo[0]: parameter_vector.append(param) else: raise ValueError("Unknown parameter type:", type(todo[0])) if len(parameter_vector) > 0: indices = np.argsort([str(t) for t in parameter_vector]) parameter_vector = [parameter_vector[i] for i in indices] # get the parameter objects for the requested observable derivatives observable_vector = [] for param in qulacs_observable.free_parameters: if isinstance(todo[0], str): if todo[0] == "" or todo[0] == "expectation_value": pass elif todo[0] == remove_brackets(param.name): observable_vector.append(param) elif todo[0] == param.name: observable_vector.append(param) elif isinstance(todo[0], ParameterVectorElement): if param == todo[0]: observable_vector.append(param) else: raise ValueError("Unknown parameter type:", type(todo[0])) if len(observable_vector) > 0: indices = np.argsort([str(t) for t in observable_vector]) observable_vector = [observable_vector[i] for i in indices] # Compute the requested derivatives if len(parameter_vector) == 0 and len(observable_vector) == 0: if todo[0] == "fischer": raise NotImplementedError( "Fischer information is not implemented for qulacs executor." ) if todo[0] != "" and todo[0] != "expectation_value": raise ValueError(f"Unknown derivative: {todo[0]}") # compute expectation value result = self._expectation_value(circuit, observable, **parameter_values) elif len(parameter_vector) > 0 and len(observable_vector) == 0: # compute gradient w.r.t. circuit parameters result = evaluate_circuit_gradient( qulacs_circuit, qulacs_observable, tuple(circuit_parameters), tuple(observable_parameters), parameter_vector, ) elif len(parameter_vector) == 0 and len(observable_vector) > 0: # compute gradient w.r.t. observable parameters result = evaluate_observable_gradient( qulacs_circuit, qulacs_observable, tuple(circuit_parameters), tuple(observable_parameters), observable_vector, ) else: raise ValueError( "Higher order derivatives are not supported with qulacs, " "please use pennylane" ) if len(values) == 1: return result if len(todo) == 1: if todo[0] == "": result_dict["expectation_value"] = result else: result_dict[todo[0]] = result else: result_dict[todo] = result return result_dict def _sample( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], **parameter_values ) -> dict | List[dict]: """ Sample the circuit. Args: circuit (QuantumCircuitBase): The quantum circuit. Returns: dict: The samples from the circuit. """ statevector = self.statevector(circuit, **parameter_values) # Get the probabilities probabilities = np.square(np.abs(statevector)) # Generate bitstrings corresponding to each basis state assert not isinstance(circuit, list), "_sample only supports a single circuit" num_qubits = circuit.num_qubits bitstrings = [format(i, f"0{num_qubits}b") for i in range(len(statevector))] # Sample from the distribution samples = self._random.choice(bitstrings, size=self._shots, p=probabilities) # Count occurrences counts = dict(Counter(samples)) return counts def _statevector( self, circuit: QuantumCircuitBase | List[QuantumCircuitBase], **parameter_values ) -> np.ndarray: """ Get the statevector of the circuit. Args: circuit (QuantumCircuitBase): The quantum circuit. Returns: np.ndarray: The statevector of the circuit. """ def reverse_bits_array(n: int, num_bits: int) -> np.ndarray: """Return indices that map backend bit order to public API order.""" indices = np.arange(n) reversed_indices = np.zeros_like(indices) for _ in range(num_bits): reversed_indices = (reversed_indices << 1) | (indices & 1) indices >>= 1 return reversed_indices qulacs_circuits, multiple_circuits = self._preprocess_circuits(circuit) state_vectors = [] for qulacs_circuit in qulacs_circuits: circuit_parameters = [] multiple_circuit_parameters = [] circuit_parameters_dimension = [] circuit_values = [] for param in qulacs_circuit.parameter_names: if param not in parameter_values: raise ValueError( f"Parameter '{param}' not found in provided parameter values." ) param_values, multiple_params = adjust_features( parameter_values[param], qulacs_circuit.parameter_dimensions[param] ) circuit_parameters.append(param_values) multiple_circuit_parameters.append(multiple_params) circuit_parameters_dimension.append(qulacs_circuit.parameter_dimensions[param]) circuit_parameter_tuples = product(*circuit_parameters) for cp in circuit_parameter_tuples: qulacs_circuit_object = qulacs_circuit.get_circuit_func()(*cp) state = QuantumState(qulacs_circuit.num_qubits) qulacs_circuit_object.update_quantum_state(state) state_vector = np.array(state.get_vector()) indices = reverse_bits_array(len(state_vector), qulacs_circuit.num_qubits) circuit_values.append(state_vector[indices]) state_vectors.append(circuit_values) state_vectors = np.array(state_vectors) # Remove the parameter dimension list (has to be fixed for multiple parameters) shape = list(state_vectors.shape) shape.pop(1) state_vectors = state_vectors.reshape(shape) if not multiple_circuits: state_vectors = state_vectors[0] return state_vectors def _transpile_circuit(self, circuit: QuantumCircuitBase) -> QuantumCircuitBase: """Transpile a generic QuantumCircuit to a QulacsCircuit. Args: circuit (QuantumCircuitBase): The generic QuantumCircuit to transpile. Returns: QuantumCircuitBase: The corresponding QulacsCircuit. """ if isinstance(circuit, self._native_circuit_class): return circuit return cast( QuantumCircuitBase, self._native_circuit_class.from_quantum_circuit(cast(QuantumCircuit, circuit)), ) def _transpile_operator(self, operator: QuantumOperatorBase) -> QuantumOperatorBase: """Transpile a generic QuantumOperator to a Qulacs QuantumOperator. Args: operator (QuantumOperatorBase): The generic QuantumOperator to transpile. Returns: QuantumOperatorBase: The corresponding QulacsOperator. """ if isinstance(operator, self._native_operator_class): return operator return cast( QuantumOperatorBase, self._native_operator_class.from_quantum_operator(operator) )
[docs] @classmethod def get_accepted_backend_types(cls) -> List[type]: """Return all object types accepted as backend in factory auto-detection. QulacsExecutor does not accept backend objects during initialization. """ return []
[docs] @classmethod def get_accepted_backend_aliases(cls) -> List[str]: """Return string aliases accepted by this executor in ``Executor.create``.""" return []