Source code for tequila_code.simulators.simulator_qulacs

import qulacs
import numbers, numpy
import warnings

from tequila import TequilaException, TequilaWarning
from tequila.utils.bitstrings import BitNumbering, BitString, BitStringLSB
from tequila.wavefunction.qubit_wavefunction import QubitWaveFunction
from tequila.simulators.simulator_base import BackendCircuit, BackendExpectationValue, QCircuit, change_basis
from tequila.utils.keymap import KeyMapRegisterToSubregister

"""
Developer Note:
    Qulacs uses different Rotational Gate conventions: Rx(angle) = exp(i angle/2 X) instead of exp(-i angle/2 X)
    And the same for MultiPauli rotational gates
    The angles are scaled with -1.0 to keep things consistent with the rest of tequila
"""

[docs] class TequilaQulacsException(TequilaException): def __str__(self): return "Error in qulacs backend:" + self.message
[docs] class BackendCircuitQulacs(BackendCircuit): """ Class representing circuits compiled to qulacs. See BackendCircuit for documentation of features and methods inherited therefrom Attributes ---------- counter: counts how many distinct sympy.Symbol objects are employed in the circuit. has_noise: whether or not the circuit is noisy. needed by the expectationvalue to do sampling properly. noise_lookup: dict: dict mapping strings to lists of constructors for cirq noise channel objects. op_lookup: dict: dictionary mapping strings (tequila gate names) to cirq.ops objects. variables: list: a list of the qulacs variables of the circuit. Methods ------- add_noise_to_circuit: apply a tequila NoiseModel to a qulacs circuit, by translating the NoiseModel's instructions into noise gates. """ compiler_arguments = { "trotterized": True, "swap": False, "multitarget": True, "controlled_rotation": True, # needed for gates depending on variables "generalized_rotation": True, "exponential_pauli": False, "controlled_exponential_pauli": True, "phase": True, "power": True, "hadamard_power": True, "controlled_power": True, "controlled_phase": True, "toffoli": False, "phase_to_z": True, "cc_max": False } numbering = BitNumbering.LSB def __init__(self, abstract_circuit, noise=None, *args, **kwargs): """ Parameters ---------- abstract_circuit: QCircuit: the circuit to compile to qulacs noise: optional: noise to apply to the circuit. args kwargs """ self.op_lookup = { 'I': qulacs.gate.Identity, 'X': qulacs.gate.X, 'Y': qulacs.gate.Y, 'Z': qulacs.gate.Z, 'H': qulacs.gate.H, 'Rx': (lambda c: c.add_parametric_RX_gate, qulacs.gate.RX), 'Ry': (lambda c: c.add_parametric_RY_gate, qulacs.gate.RY), 'Rz': (lambda c: c.add_parametric_RZ_gate, qulacs.gate.RZ), 'SWAP': qulacs.gate.SWAP, 'Measure': qulacs.gate.Measurement, 'Exp-Pauli': None } self.measurements = None self.variables = [] super().__init__(abstract_circuit=abstract_circuit, noise=noise, *args, **kwargs) self.has_noise=False if noise is not None: warnings.warn("Warning: noise in qulacs module will be dropped. Currently only works for qulacs version 0.5 or lower", TequilaWarning) self.has_noise=True self.noise_lookup = { 'bit flip': [qulacs.gate.BitFlipNoise], 'phase flip': [lambda target, prob: qulacs.gate.Probabilistic([prob],[qulacs.gate.Z(target)])], 'phase damp': [lambda target, prob: qulacs.gate.DephasingNoise(target,(1/2)*(1-numpy.sqrt(1-prob)))], 'amplitude damp': [qulacs.gate.AmplitudeDampingNoise], 'phase-amplitude damp': [qulacs.gate.AmplitudeDampingNoise, lambda target, prob: qulacs.gate.DephasingNoise(target,(1/2)*(1-numpy.sqrt(1-prob))) ], 'depolarizing': [lambda target,prob: qulacs.gate.DepolarizingNoise(target,3*prob/4)] } self.circuit=self.add_noise_to_circuit(noise)
[docs] def initialize_state(self, n_qubits:int=None) -> qulacs.QuantumState: if n_qubits is None: n_qubits = self.n_qubits return qulacs.QuantumState(n_qubits)
[docs] def update_variables(self, variables): """ set new variable values for the circuit. Parameters ---------- variables: dict: the variables to supply to the circuit. Returns ------- None """ for k, angle in enumerate(self.variables): self.circuit.set_parameter(k, angle(variables))
[docs] def do_simulate(self, variables, initial_state, *args, **kwargs): """ Helper function to perform simulation. Parameters ---------- variables: dict: variables to supply to the circuit. initial_state: information indicating the initial state on which the circuit should act. args kwargs Returns ------- QubitWaveFunction: QubitWaveFunction representing result of the simulation. """ state = self.initialize_state(self.n_qubits) lsb = BitStringLSB.from_int(initial_state, nbits=self.n_qubits) state.set_computational_basis(BitString.from_binary(lsb.binary).integer) self.circuit.update_quantum_state(state) wfn = QubitWaveFunction.from_array(arr=state.get_vector(), numbering=self.numbering) return wfn
[docs] def convert_measurements(self, backend_result, target_qubits=None) -> QubitWaveFunction: """ Transform backend evaluation results into QubitWaveFunction Parameters ---------- backend_result: the return value of backend simulation. Returns ------- QubitWaveFunction results transformed to tequila native QubitWaveFunction """ result = QubitWaveFunction() # todo there are faster ways for k in backend_result: converted_key = BitString.from_binary(BitStringLSB.from_int(integer=k, nbits=self.n_qubits).binary) if converted_key in result._state: result._state[converted_key] += 1 else: result._state[converted_key] = 1 if target_qubits is not None: mapped_target = [self.qubit_map[q].number for q in target_qubits] mapped_full = [self.qubit_map[q].number for q in self.abstract_qubits] keymap = KeyMapRegisterToSubregister(subregister=mapped_target, register=mapped_full) result = result.apply_keymap(keymap=keymap) return result
[docs] def do_sample(self, samples, circuit, noise_model=None, initial_state=0, *args, **kwargs) -> QubitWaveFunction: """ Helper function for performing sampling. Parameters ---------- samples: int: the number of samples to be taken. circuit: the circuit to sample from. noise_model: optional: noise model to be applied to the circuit. initial_state: sampling supports initial states for qulacs. Indicates the initial state to which circuit is applied. args kwargs Returns ------- QubitWaveFunction: the results of sampling, as a Qubit Wave Function. """ state = self.initialize_state(self.n_qubits) lsb = BitStringLSB.from_int(initial_state, nbits=self.n_qubits) state.set_computational_basis(BitString.from_binary(lsb.binary).integer) circuit.update_quantum_state(state) sampled = state.sampling(samples) return self.convert_measurements(backend_result=sampled, target_qubits=self.measurements)
[docs] def no_translation(self, abstract_circuit): """ Todo: what is this for? Parameters ---------- abstract_circuit Returns ------- """ return False
[docs] def initialize_circuit(self, *args, **kwargs): """ return an empty circuit. Parameters ---------- args kwargs Returns ------- qulacs.ParametricQuantumCircuit """ return qulacs.ParametricQuantumCircuit(self.n_qubits)
[docs] def add_exponential_pauli_gate(self, gate, circuit, variables, *args, **kwargs): """ Add a native qulacs Exponential Pauli gate to a circuit. Parameters ---------- gate: ExpPauliGateImpl: the gate to add circuit: the qulacs circuit, to which the gate is to be added. variables: dict containing values of the parameters appearing in the pauli gate. args kwargs Returns ------- None """ assert not gate.is_controlled() convert = {'x': 1, 'y': 2, 'z': 3} pind = [convert[x.lower()] for x in gate.paulistring.values()] qind = [self.qubit(x) for x in gate.paulistring.keys()] if len(gate.extract_variables()) > 0: self.variables.append(-gate.parameter * gate.paulistring.coeff) circuit.add_parametric_multi_Pauli_rotation_gate(qind, pind, -gate.parameter(variables) * gate.paulistring.coeff) else: circuit.add_multi_Pauli_rotation_gate(qind, pind, -gate.parameter(variables) * gate.paulistring.coeff)
[docs] def add_parametrized_gate(self, gate, circuit, variables, *args, **kwargs): """ add a parametrized gate. Parameters ---------- gate: QGateImpl: the gate to add to the circuit. circuit: the circuit to which the gate is to be added variables: dict that tells values of variables; needed IFF the gate is an ExpPauli gate. args kwargs Returns ------- None """ op = self.op_lookup[gate.name] if gate.name == 'Exp-Pauli': self.add_exponential_pauli_gate(gate, circuit, variables) return else: if len(gate.extract_variables()) > 0: op = op[0] self.variables.append(-gate.parameter) op(circuit)(self.qubit(gate.target[0]), -gate.parameter(variables=variables)) if gate.is_controlled(): raise TequilaQulacsException("Gates which depend on variables can not be controlled! Gate was:\n{}".format(gate)) return else: op = op[1] qulacs_gate = op(self.qubit(gate.target[0]), -gate.parameter(variables=variables)) if gate.is_controlled(): qulacs_gate = qulacs.gate.to_matrix_gate(qulacs_gate) for c in gate.control: qulacs_gate.add_control_qubit(self.qubit(c), 1) circuit.add_gate(qulacs_gate)
[docs] def add_basic_gate(self, gate, circuit, *args, **kwargs): """ add an unparametrized gate to the circuit. Parameters ---------- gate: QGateImpl: the gate to be added to the circuit. circuit: the circuit, to which a gate is to be added. args kwargs Returns ------- None """ op = self.op_lookup[gate.name] qulacs_gate = op(*[self.qubit(t) for t in gate.target]) if gate.is_controlled(): qulacs_gate = qulacs.gate.to_matrix_gate(qulacs_gate) for c in gate.control: qulacs_gate.add_control_qubit(self.qubit(c), 1) circuit.add_gate(qulacs_gate)
[docs] def add_measurement(self, circuit, target_qubits, *args, **kwargs): """ Add a measurement operation to a circuit. Parameters ---------- circuit: a circuit, to which the measurement is to be added. target_qubits: List[int] abstract target qubits args kwargs Returns ------- None """ self.measurements = sorted(target_qubits) return circuit
[docs] def add_noise_to_circuit(self,noise_model): """ Apply noise from a NoiseModel to a circuit. Parameters ---------- noise_model: NoiseModel: the noisemodel to apply to the circuit. Returns ------- qulacs.ParametrizedQuantumCircuit: self.circuit, with noise added on. """ c=self.circuit n=noise_model g_count=c.get_gate_count() new=self.initialize_circuit() for i in range(g_count): g=c.get_gate(i) new.add_gate(g) qubits=g.get_target_index_list() + g.get_control_index_list() for noise in n.noises: if len(qubits) == noise.level: for j,channel in enumerate(self.noise_lookup[noise.name]): for q in qubits: chan=channel(q,noise.probs[j]) new.add_gate(chan) return new
[docs] def optimize_circuit(self, circuit, max_block_size: int = 4, silent: bool = True, *args, **kwargs): """ reduce circuit depth using the native qulacs optimizer. Parameters ---------- circuit max_block_size: int: Default = 4: the maximum block size for use by the qulacs internal optimizer. silent: bool: whether or not to print the resullt of having optimized. args kwargs Returns ------- qulacs.QuantumCircuit: optimized qulacs circuit. """ old = circuit.calculate_depth() opt = qulacs.circuit.QuantumCircuitOptimizer() opt.optimize(circuit, max_block_size) if not silent: print("qulacs: optimized circuit depth from {} to {} with max_block_size {}".format(old, circuit.calculate_depth(), max_block_size)) return circuit
[docs] class BackendExpectationValueQulacs(BackendExpectationValue): """ Class representing Expectation Values compiled for Qulacs. Ovverrides some methods of BackendExpectationValue, which should be seen for details. """ use_mapping = True BackendCircuitType = BackendCircuitQulacs
[docs] def simulate(self, variables, *args, **kwargs) -> numpy.array: """ Perform simulation of this expectationvalue. Parameters ---------- variables: variables, to be supplied to the underlying circuit. args kwargs Returns ------- numpy.array: the result of simulation as an array. """ # fast return if possible if self.H is None: return numpy.asarray([0.0]) elif len(self.H) == 0: return numpy.asarray([0.0]) elif isinstance(self.H, numbers.Number): return numpy.asarray[self.H] self.U.update_variables(variables) state = self.U.initialize_state(self.n_qubits) self.U.circuit.update_quantum_state(state) result = [] for H in self.H: if isinstance(H, numbers.Number): result.append(H) # those are accumulated unit strings, e.g 0.1*X(3) in wfn on qubits 0,1 else: result.append(H.get_expectation_value(state)) return numpy.asarray(result)
[docs] def initialize_hamiltonian(self, hamiltonians): """ Convert reduced hamiltonians to native Qulacs types for efficient expectation value evaluation. Parameters ---------- hamiltonians: an interable set of hamiltonian objects. Returns ------- list: initialized hamiltonian objects. """ # map the reduced operators to the potentially smaller qubit system qubit_map = {} for i,q in enumerate(self.U.abstract_circuit.qubits): qubit_map[q] = i result = [] for H in hamiltonians: qulacs_H = qulacs.Observable(self.n_qubits) for ps in H.paulistrings: string = "" for k, v in ps.items(): string += v.upper() + " " + str(qubit_map[k]) qulacs_H.add_operator(ps.coeff, string) result.append(qulacs_H) return result
[docs] def sample(self, variables, samples, *args, **kwargs) -> numpy.array: """ Sample this Expectation Value. Parameters ---------- variables: variables, to supply to the underlying circuit. samples: int: the number of samples to take. args kwargs Returns ------- numpy.ndarray: the result of sampling as a number. """ self.update_variables(variables) state = self.U.initialize_state(self.n_qubits) self.U.circuit.update_quantum_state(state) result = [] for H in self._reduced_hamiltonians: # those are the hamiltonians which where non-used qubits are already traced out E = 0.0 if H.is_all_z() and not self.U.has_noise: E = super().sample(samples=samples, variables=variables, *args, **kwargs) else: for ps in H.paulistrings: # change basis, measurement is destructive so the state will be copied # to avoid recomputation (except when noise was required) bc = QCircuit() for idx, p in ps.items(): bc += change_basis(target=idx, axis=p) qbc = self.U.create_circuit(abstract_circuit=bc, variables=None) Esamples = [] for sample in range(samples): if self.U.has_noise and sample>0: state = self.U.initialize_state(self.n_qubits) self.U.circuit.update_quantum_state(state) state_tmp = state else: state_tmp = state.copy() if len(bc.gates) > 0: # otherwise there is no basis change (empty qulacs circuit does not work out) qbc.update_quantum_state(state_tmp) ps_measure = 1.0 for idx in ps.keys(): assert idx in self.U.abstract_qubits # assert that the hamiltonian was really reduced M = qulacs.gate.Measurement(self.U.qubit(idx), self.U.qubit(idx)) M.update_quantum_state(state_tmp) measured = state_tmp.get_classical_value(self.U.qubit(idx)) ps_measure *= (-2.0 * measured + 1.0) # 0 becomes 1 and 1 becomes -1 Esamples.append(ps_measure) E += ps.coeff * sum(Esamples) / len(Esamples) result.append(E) return numpy.asarray(result)