Source code for qiskit_qulacs.adapter

"""Util functions for provider"""

import re
import warnings
from math import log2
from typing import Any, Dict, Iterable, List, Set, Tuple

import numpy as np
import psutil
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterExpression
from qiskit.circuit import library as lib
from qiskit.circuit.parametervector import ParameterVectorElement
from qiskit.quantum_info import SparsePauliOp
from scipy.sparse import diags
from sympy import lambdify

import qulacs.gate as qg
from qulacs import Observable, ParametricQuantumCircuit, PauliOperator

_EPS = 1e-10  # global variable used to chop very small numbers to zero

# Available system memory
SYSTEM_MEMORY_GB = psutil.virtual_memory().total / (1024**3)

# Max number of qubits
MAX_QUBITS = int(log2(SYSTEM_MEMORY_GB * (1024**3) / 16))


# Defintions of some gates that are not directly defined in qulacs
# The `args` argument is of the form *qubits, *parameters
# The gates defined below currently support only single parameter only


[docs] def qgUnitary(*args): """ The function `qgUnitary` takes qubits and parameters as input and returns a dense matrix. Returns: The function `qgUnitary` is returning a `qg.DenseMatrix` object created with the provided `qubits` and `parameters`. """ qubits = args[:-1] parameters = args[-1] return qg.DenseMatrix(qubits, parameters) # pylint: disable=no-member
IsingXX = lambda *args: qg.ParametricPauliRotation(args[:-1], [1, 1], args[-1].real) IsingYY = lambda *args: qg.ParametricPauliRotation(args[:-1], [2, 2], args[-1].real) IsingZZ = lambda *args: qg.ParametricPauliRotation(args[:-1], [3, 3], args[-1].real) ecr_mat = np.array( [[0, 1, 0, 1j], [1, 0, -1j, 0], [0, 1j, 0, 1], [-1j, 0, 1, 0]] ) / np.sqrt(2) qgECR = lambda *args: qg.DenseMatrix(args, matrix=ecr_mat) # These gates in qulacs have positive rotation directions. # Angles of these gates need to be multiplied by -1 during conversion. # https://docs.qulacs.org/en/latest/guide/2.0_python_advanced.html#1-qubit-rotating-gate neg_gates = {"RXGate", "RYGate", "RZGate", "RXXGate", "RYYGate", "RZZGate"} # Only these gates support trainable parameters parametric_gates = neg_gates # Gate addition type # based on the type of the, one of these two will be used in the qulacs circuit gate_addition = ["add_gate", "add_parametric_gate"] QISKIT_OPERATION_MAP = { qg.X: lib.XGate, qg.Y: lib.YGate, qg.Z: lib.ZGate, qg.H: lib.HGate, qg.CNOT: lib.CXGate, qg.CZ: lib.CZGate, qg.SWAP: lib.SwapGate, qg.FREDKIN: lib.CSwapGate, qg.ParametricRX: lib.RXGate, # -theta qg.ParametricRY: lib.RYGate, # -theta qg.ParametricRZ: lib.RZGate, # -theta qg.Identity: lib.IGate, qg.TOFFOLI: lib.CCXGate, qg.U1: lib.U1Gate, # deprecated in qiskit, use p gate qg.U2: lib.U2Gate, # deprecated in qiskit, use u gate qg.U3: lib.U3Gate, # deprecated in qiskit, use u gate IsingXX: lib.RXXGate, # -theta IsingYY: lib.RYYGate, # -theta IsingZZ: lib.RZZGate, # -theta qg.S: lib.SGate, qg.Sdag: lib.SdgGate, qg.T: lib.TGate, qg.Tdag: lib.TdgGate, qg.sqrtX: lib.SXGate, qg.sqrtXdag: lib.SXdgGate, qgUnitary: lib.UnitaryGate, qgECR: lib.ECRGate, } inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} # Gates with different names but same operation duplicate_gates = {"UGate": "U3Gate"}
[docs] def convert_qiskit_to_qulacs_circuit(qc: QuantumCircuit): """ The function `convert_qiskit_to_qulacs_circuit` converts a Qiskit QuantumCircuit to a Qulacs ParametricQuantumCircuit while handling parameter mapping and gate operations. Args: qc (QuantumCircuit): The `qc` is expected to be a QuantumCircuit object from Qiskit. Returns: The `convert_qiskit_to_qulacs_circuit` function returns a nested function `circuit_builder` that takes an optional `params_values` argument. Inside `circuit_builder`, it constructs a `ParametricQuantumCircuit` based on the input `QuantumCircuit` `qc` provided to the outer function. """ def circuit_builder(params_values=[]) -> Tuple[ParametricQuantumCircuit, Dict]: """ The `circuit_builder` function converts a Qiskit quantum circuit into a ParametricQuantumCircuit, handling parameter mapping and supporting trainable parameters. Args: params_values: The `params_values` parameter in the `circuit_builder` function is a list that contains the values of the parameters that will be used to build a quantum circuit. These values will be used to replace the symbolic parameters in the quantum circuit with concrete numerical values during the circuit construction process. Returns: The `circuit_builder` function returns a ParametricQuantumCircuit and a dictionary containing information about parameter mapping and parameter expressions. """ circuit = ParametricQuantumCircuit(qc.num_qubits) # parameter mapping # dictionary from qiskit's quantum circuit parameters to a two element tuple. # the tuple has an element params_values and its index # Currently not supporting qiskit's parameter expression var_ref_map = dict( zip(qc.parameters, list(zip(params_values, range(qc.num_parameters)))), ) # Wires from a qiskit circuit have unique IDs, so their hashes are unique too qc_wires = [hash(q) for q in qc.qubits] wire_map = dict(zip(qc_wires, range(len(qc_wires)))) # Holds the indices of parameter as they occur during # circuit conversion. This is used during circuit gradient computation. param_mapping = [] param_exprs = [] f_args: List[Any] = [] f_params: List[Any] = [] indices: List[int] = [] f_param_names: Set[Any] = set() flag = False # indicates whether the instruction is parametric for instruction, qargs, _ in qc.data: # the new Singleton classes have different names than the objects they represent, # but base_class.__name__ still matches instruction_name = getattr( instruction, "base_class", instruction.__class__ ).__name__ instruction_name = duplicate_gates.get(instruction_name, instruction_name) sign = 1.0 - 2 * (instruction_name in neg_gates) operation_wires = [wire_map[hash(qubit)] for qubit in qargs] operation_params = [] flag = False for p in instruction.params: if isinstance(p, ParameterExpression) and p.parameters: f_args = [] f_params = [] indices = [] # Ensure duplicate subparameters are only appended once. f_param_names = set() for subparam in p.parameters: try: parameter = subparam argument, index = var_ref_map.get(subparam) except: raise ValueError( "The number of circuit parameters does not match", " the number of parameter values passed.", ) if isinstance(subparam, ParameterVectorElement): # Unfortunately, the names of parameter vector elements # include square brackets, making them invalid Python # identifiers and causing compatibility problems with SymPy. # To solve this issue, we generate a temporary parameter # that replaces square bracket by underscores. subparam_name = re.sub(r"\[|\]", "_", str(subparam)) parameter = Parameter(subparam_name) argument, index = var_ref_map.get(subparam) # Update the subparam in `p` p = p.assign(subparam, parameter) if parameter.name not in f_param_names: f_param_names.add(parameter.name) f_params.append(parameter) f_args.append(argument) indices.append(index) f_expr = getattr(p, "_symbol_expr") if isinstance(p, Parameter): # If `p` is an instance of `Parameter` then we can # we do not need to calculate the expression value operation_params += list(map(lambda x: x * sign, f_args)) else: # Calculate the expression value using sympy f = lambdify(f_params, f_expr) operation_params += [f(*f_args) * sign] param_mapping += indices param_exprs += [(f_params, f_expr)] flag = True else: operation_params.append(p * sign) operation_class = inv_map.get(instruction_name) try: getattr(circuit, gate_addition[flag])( operation_class(*operation_wires, *operation_params) # type: ignore ) except: if flag: raise ValueError( f"{__name__}: The {instruction_name} does not support trainable parameter.", f" Consider decomposing {instruction_name} into {parametric_gates}.", ) warnings.warn( f"{__name__}: The {instruction_name} instruction is not supported" " by Qiskit-Qulacs and has not been added to the circuit.", UserWarning, ) if qc.global_phase > _EPS: # add the gphase_mat to the circuit circuit.add_gate( qg.SparseMatrix( # pylint: disable=no-member list(range(qc.num_qubits)), diags(np.exp(1j * qc.global_phase) * np.ones(2**qc.num_qubits)), ) ) return circuit, { "parameter_mapping": param_mapping, "parameter_exprs": param_exprs, } return circuit_builder
[docs] def qiskit_to_qulacs( circuits: List[QuantumCircuit], ) -> Iterable[ParametricQuantumCircuit]: """ The function `qiskit_to_qulacs` converts a list of Qiskit quantum circuits into a generator of Qulacs circuits. Args: circuits (List[QuantumCircuit]): The `circuits` parameter is a list of `QuantumCircuit` objects. """ for circuit in circuits: yield convert_qiskit_to_qulacs_circuit(circuit)()[0]
[docs] def convert_sparse_pauliop_to_qulacs_obs(sparse_pauliop: SparsePauliOp): """ The function `convert_sparse_pauliop_to_qulacs_obs` converts a sparse Pauli operator to a Qulacs observable. Args: sparse_pauliop: The `sparse_pauliop` parameter is a sparse representation of a Pauli operator. It is an object that contains information about the Pauli terms and their coefficients. Each term is represented by a `PauliTerm` object, which consists of a list of Pauli operators and their corresponding Returns: a Qulacs Observable object. """ qulacs_observable = Observable(sparse_pauliop.num_qubits) for op in sparse_pauliop: term, coefficient = str(op.paulis[0])[::-1], op.coeffs[0] pauli_string = "" for qubit, pauli in enumerate(term): pauli_string += f"{pauli} {qubit} " qulacs_observable.add_operator(PauliOperator(pauli_string, coefficient.real)) return qulacs_observable