def test_set_parameters_with_double_variationallayer(backend, accelerators, nqubits): """Check updating parameters of variational layer.""" original_backend = qibo.get_backend() qibo.set_backend(backend) theta = np.random.random((3, nqubits)) c = Circuit(nqubits, accelerators) pairs = [(i, i + 1) for i in range(0, nqubits - 1, 2)] c.add(gates.VariationalLayer(range(nqubits), pairs, gates.RY, gates.CZ, theta[0], theta[1])) c.add((gates.RX(i, theta[2, i]) for i in range(nqubits))) target_c = Circuit(nqubits) target_c.add((gates.RY(i, theta[0, i]) for i in range(nqubits))) target_c.add((gates.CZ(i, i + 1) for i in range(0, nqubits - 1, 2))) target_c.add((gates.RY(i, theta[1, i]) for i in range(nqubits))) target_c.add((gates.RX(i, theta[2, i]) for i in range(nqubits))) np.testing.assert_allclose(c(), target_c()) new_theta = np.random.random(3 * nqubits) c.set_parameters(np.copy(new_theta)) target_c.set_parameters(np.copy(new_theta)) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
class TrotterCircuit: """Object that caches the Trotterized evolution circuit. This object holds a reference to the circuit models and updates its parameters if a different time step ``dt`` is given without recreating every gate from scratch. Args: groups (list): List of :class:`qibo.core.terms.TermGroup` objects that correspond to the Trotter groups of terms in the time evolution exponential operator. dt (float): Time step for the Trotterization. nqubits (int): Number of qubits in the system that evolves. accelerators (dict): Dictionary with accelerators for distributed circuits. """ def __init__(self, groups, dt, nqubits, accelerators): from qibo.models import Circuit self.gates = {} self.dt = dt self.circuit = Circuit(nqubits, accelerators=accelerators) for group in itertools.chain(groups, groups[::-1]): gate = group.term.expgate(dt / 2.0) self.gates[gate] = group self.circuit.add(gate) def set(self, dt): if self.dt != dt: params = { gate: group.term.exp(dt / 2.0) for gate, group in self.gates.items() } self.dt = dt self.circuit.set_parameters(params)
def test_parallel_parametrized_circuit(): """Evaluate circuit for multiple parameters.""" if 'GPU' in get_device(): # pragma: no cover pytest.skip("unsupported configuration") original_threads = get_threads() set_threads(1) nqubits = 5 nlayers = 10 c = Circuit(nqubits) for l in range(nlayers): c.add((gates.RY(q, theta=0) for q in range(nqubits))) c.add((gates.CZ(q, q + 1) for q in range(0, nqubits - 1, 2))) c.add((gates.RY(q, theta=0) for q in range(nqubits))) c.add((gates.CZ(q, q + 1) for q in range(1, nqubits - 2, 2))) c.add(gates.CZ(0, nqubits - 1)) c.add((gates.RY(q, theta=0) for q in range(nqubits))) size = len(c.get_parameters()) np.random.seed(0) parameters = [np.random.uniform(0, 2 * np.pi, size) for i in range(10)] state = None r1 = [] for params in parameters: c.set_parameters(params) r1.append(c(state)) r2 = parallel_parametrized_execution(c, parameters=parameters, initial_state=state, processes=2) np.testing.assert_allclose(r1, r2) set_threads(original_threads)
def test_set_parameters_with_gate_fusion(backend, accelerators): """Check updating parameters of fused circuit.""" original_backend = qibo.get_backend() qibo.set_backend(backend) params = np.random.random(9) c = Circuit(5, accelerators) c.add(gates.RX(0, theta=params[0])) c.add(gates.RY(1, theta=params[1])) c.add(gates.CZ(0, 1)) c.add(gates.RX(2, theta=params[2])) c.add(gates.RY(3, theta=params[3])) c.add(gates.fSim(2, 3, theta=params[4], phi=params[5])) c.add(gates.RX(4, theta=params[6])) c.add(gates.RZ(0, theta=params[7])) c.add(gates.RZ(1, theta=params[8])) fused_c = c.fuse() np.testing.assert_allclose(c(), fused_c()) new_params = np.random.random(9) new_params_list = list(new_params[:4]) new_params_list.append((new_params[4], new_params[5])) new_params_list.extend(new_params[6:]) c.set_parameters(new_params_list) fused_c.set_parameters(new_params_list) np.testing.assert_allclose(c(), fused_c()) qibo.set_backend(original_backend)
def test_circuit_set_parameters_with_dictionary(backend, accelerators): """Check updating parameters of circuit with list.""" original_backend = qibo.get_backend() qibo.set_backend(backend) c = Circuit(3, accelerators) c.add(gates.X(0)) c.add(gates.X(2)) c.add(gates.U1(0, theta=0)) c.add(gates.RZ(1, theta=0)) c.add(gates.CZ(1, 2)) c.add(gates.CU1(0, 2, theta=0)) c.add(gates.H(2)) c.add(gates.Unitary(np.eye(2), 1)) final_state = c() params = [0.123, 0.456, 0.789, np.random.random((2, 2))] target_c = Circuit(3, accelerators) target_c.add(gates.X(0)) target_c.add(gates.X(2)) target_c.add(gates.U1(0, theta=params[0])) target_c.add(gates.RZ(1, theta=params[1])) target_c.add(gates.CZ(1, 2)) target_c.add(gates.CU1(0, 2, theta=params[2])) target_c.add(gates.H(2)) target_c.add(gates.Unitary(params[3], 1)) param_dict = {c.queue[i]: p for i, p in zip([2, 3, 5, 7], params)} c.set_parameters(param_dict) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
def test_circuit_set_parameters_with_unitary(backend, accelerators): """Check updating parameters of circuit that contains ``Unitary`` gate.""" original_backend = qibo.get_backend() qibo.set_backend(backend) c = Circuit(3, accelerators) c.add(gates.RX(0, theta=0)) c.add(gates.Unitary(np.zeros((4, 4)), 1, 2)) # execute once final_state = c() params = [0.1234, np.random.random((4, 4))] target_c = Circuit(3) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.Unitary(params[1], 1, 2)) c.set_parameters(params) np.testing.assert_allclose(c(), target_c()) # Attempt using a flat list / np.ndarray params = np.random.random(17) target_c = Circuit(3) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.Unitary(params[1:].reshape((4, 4)), 1, 2)) c.set_parameters(params) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
def test_set_parameters_with_gate_fusion(backend, trainable): """Check updating parameters of fused circuit.""" params = np.random.random(9) c = Circuit(5) c.add(gates.RX(0, theta=params[0], trainable=trainable)) c.add(gates.RY(1, theta=params[1])) c.add(gates.CZ(0, 1)) c.add(gates.RX(2, theta=params[2])) c.add(gates.RY(3, theta=params[3], trainable=trainable)) c.add(gates.fSim(2, 3, theta=params[4], phi=params[5])) c.add(gates.RX(4, theta=params[6])) c.add(gates.RZ(0, theta=params[7], trainable=trainable)) c.add(gates.RZ(1, theta=params[8])) fused_c = c.fuse() final_state = fused_c() target_state = c() K.assert_allclose(final_state, target_state) if trainable: new_params = np.random.random(9) new_params_list = list(new_params[:4]) new_params_list.append((new_params[4], new_params[5])) new_params_list.extend(new_params[6:]) else: new_params = np.random.random(9) new_params_list = list(new_params[1:3]) new_params_list.append((new_params[4], new_params[5])) new_params_list.append(new_params[6]) new_params_list.append(new_params[8]) c.set_parameters(new_params_list) fused_c.set_parameters(new_params_list) K.assert_allclose(c(), fused_c())
def test_circuit_set_parameters_ungates(backend, accelerators, trainable): """Check updating parameters of circuit with list.""" original_backend = qibo.get_backend() qibo.set_backend(backend) params = [0.1, 0.2, 0.3, (0.4, 0.5), (0.6, 0.7, 0.8)] if trainable: trainable_params = list(params) else: trainable_params = [0.1, 0.3, (0.4, 0.5)] c = Circuit(3, accelerators) c.add(gates.RX(0, theta=0)) if trainable: c.add(gates.CRY(0, 1, theta=0, trainable=trainable)) else: c.add(gates.CRY(0, 1, theta=params[1], trainable=trainable)) c.add(gates.CZ(1, 2)) c.add(gates.U1(2, theta=0)) c.add(gates.CU2(0, 2, phi=0, lam=0)) if trainable: c.add(gates.U3(1, theta=0, phi=0, lam=0, trainable=trainable)) else: c.add(gates.U3(1, *params[4], trainable=trainable)) # execute once final_state = c() target_c = Circuit(3) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.CRY(0, 1, theta=params[1])) target_c.add(gates.CZ(1, 2)) target_c.add(gates.U1(2, theta=params[2])) target_c.add(gates.CU2(0, 2, *params[3])) target_c.add(gates.U3(1, *params[4])) c.set_parameters(trainable_params) np.testing.assert_allclose(c(), target_c()) # Attempt using a flat list npparams = np.random.random(8) if trainable: trainable_params = np.copy(npparams) else: npparams[1] = params[1] npparams[5:] = params[4] trainable_params = np.delete(npparams, [1, 5, 6, 7]) target_c = Circuit(3) target_c.add(gates.RX(0, theta=npparams[0])) target_c.add(gates.CRY(0, 1, theta=npparams[1])) target_c.add(gates.CZ(1, 2)) target_c.add(gates.U1(2, theta=npparams[2])) target_c.add(gates.CU2(0, 2, *npparams[3:5])) target_c.add(gates.U3(1, *npparams[5:])) c.set_parameters(trainable_params) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
def test_set_parameters_with_variationallayer(backend, accelerators, nqubits): """Check updating parameters of variational layer.""" original_backend = qibo.get_backend() qibo.set_backend(backend) theta = np.random.random(nqubits) c = Circuit(nqubits, accelerators) pairs = [(i, i + 1) for i in range(0, nqubits - 1, 2)] c.add( gates.VariationalLayer(range(nqubits), pairs, gates.RY, gates.CZ, theta)) target_c = Circuit(nqubits) target_c.add((gates.RY(i, theta[i]) for i in range(nqubits))) target_c.add((gates.CZ(i, i + 1) for i in range(0, nqubits - 1, 2))) np.testing.assert_allclose(c(), target_c()) # Test setting VariationalLayer using a list new_theta = np.random.random(nqubits) c.set_parameters([np.copy(new_theta)]) target_c.set_parameters(np.copy(new_theta)) np.testing.assert_allclose(c(), target_c()) # Test setting VariationalLayer using an array new_theta = np.random.random(nqubits) c.set_parameters(np.copy(new_theta)) target_c.set_parameters(np.copy(new_theta)) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
def test_set_parameters_fusion(backend): """Check gate fusion when ``circuit.set_parameters`` is used.""" c = Circuit(2) c.add(gates.RX(0, theta=0.1234)) c.add(gates.RX(1, theta=0.1234)) c.add(gates.CNOT(0, 1)) c.add(gates.RY(0, theta=0.1234)) c.add(gates.RY(1, theta=0.1234)) fused_c = c.fuse() K.assert_allclose(fused_c(), c()) c.set_parameters(4 * [0.4321]) fused_c.set_parameters(4 * [0.4321]) K.assert_allclose(fused_c(), c())
def test_circuit_set_parameters_with_list(backend, accelerators, trainable): """Check updating parameters of circuit with list.""" original_backend = qibo.get_backend() qibo.set_backend(backend) params = [0.123, 0.456, (0.789, 0.321)] c = Circuit(3, accelerators) if trainable: c.add(gates.RX(0, theta=0, trainable=trainable)) else: c.add(gates.RX(0, theta=params[0], trainable=trainable)) c.add(gates.RY(1, theta=0)) c.add(gates.CZ(1, 2)) c.add(gates.fSim(0, 2, theta=0, phi=0)) c.add(gates.H(2)) # execute once final_state = c() target_c = Circuit(3) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.RY(1, theta=params[1])) target_c.add(gates.CZ(1, 2)) target_c.add(gates.fSim(0, 2, theta=params[2][0], phi=params[2][1])) target_c.add(gates.H(2)) if trainable: c.set_parameters(params) else: c.set_parameters(params[1:]) np.testing.assert_allclose(c(), target_c()) # Attempt using a flat np.ndarray/list for new_params in (np.random.random(4), list(np.random.random(4))): if trainable: c.set_parameters(new_params) else: new_params[0] = params[0] c.set_parameters(new_params[1:]) target_params = [ new_params[0], new_params[1], (new_params[2], new_params[3]) ] target_c.set_parameters(target_params) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
def test_circuit_set_parameters_errors(): """Check updating parameters errors.""" c = Circuit(2) c.add(gates.RX(0, theta=0.789)) c.add(gates.RX(1, theta=0.789)) c.add(gates.fSim(0, 1, theta=0.123, phi=0.456)) with pytest.raises(ValueError): c.set_parameters({gates.RX(0, theta=1.0): 0.568}) with pytest.raises(ValueError): c.set_parameters([0.12586]) with pytest.raises(ValueError): c.set_parameters(np.random.random(5)) with pytest.raises(ValueError): import tensorflow as tf c.set_parameters(tf.random.uniform((6,), dtype=tf.float64)) with pytest.raises(TypeError): c.set_parameters({0.3568}) fused_c = c.fuse() with pytest.raises(TypeError): fused_c.set_parameters({gates.RX(0, theta=1.0): 0.568})
def test_circuit_set_parameters_with_unitary(backend, trainable, accelerators): """Check updating parameters of circuit that contains ``Unitary`` gate.""" params = [0.1234, np.random.random((4, 4))] c = Circuit(4, accelerators) c.add(gates.RX(0, theta=0)) if trainable: c.add(gates.Unitary(np.zeros((4, 4)), 1, 2, trainable=trainable)) trainable_params = list(params) else: c.add(gates.Unitary(params[1], 1, 2, trainable=trainable)) trainable_params = [params[0]] # execute once final_state = c() target_c = Circuit(4) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.Unitary(params[1], 1, 2)) c.set_parameters(trainable_params) K.assert_allclose(c(), target_c()) # Attempt using a flat list / np.ndarray new_params = np.random.random(17) if trainable: c.set_parameters(new_params) else: c.set_parameters(new_params[:1]) new_params[1:] = params[1].ravel() target_c = Circuit(4) target_c.add(gates.RX(0, theta=new_params[0])) target_c.add(gates.Unitary(new_params[1:].reshape((4, 4)), 1, 2)) K.assert_allclose(c(), target_c())
def test_set_parameters_with_double_variationallayer(backend, nqubits, trainable, accelerators): """Check updating parameters of variational layer.""" theta = np.random.random((3, nqubits)) c = Circuit(nqubits, accelerators) pairs = [(i, i + 1) for i in range(0, nqubits - 1, 2)] c.add( gates.VariationalLayer(range(nqubits), pairs, gates.RY, gates.CZ, theta[0], theta[1], trainable=trainable)) c.add((gates.RX(i, theta[2, i]) for i in range(nqubits))) target_c = Circuit(nqubits) target_c.add((gates.RY(i, theta[0, i]) for i in range(nqubits))) target_c.add((gates.CZ(i, i + 1) for i in range(0, nqubits - 1, 2))) target_c.add((gates.RY(i, theta[1, i]) for i in range(nqubits))) target_c.add((gates.RX(i, theta[2, i]) for i in range(nqubits))) K.assert_allclose(c(), target_c()) new_theta = np.random.random(3 * nqubits) if trainable: c.set_parameters(np.copy(new_theta)) else: c.set_parameters(np.copy(new_theta[2 * nqubits:])) new_theta[:2 * nqubits] = theta[:2].ravel() target_c.set_parameters(np.copy(new_theta)) K.assert_allclose(c(), target_c())
def test_circuit_set_parameters_ungates(backend, accelerators): """Check updating parameters of circuit with list.""" original_backend = qibo.get_backend() qibo.set_backend(backend) c = Circuit(3, accelerators) c.add(gates.RX(0, theta=0)) c.add(gates.CRY(0, 1, theta=0)) c.add(gates.CZ(1, 2)) c.add(gates.U1(2, theta=0)) c.add(gates.CU2(0, 2, phi=0, lam=0)) c.add(gates.U3(1, theta=0, phi=0, lam=0)) # execute once final_state = c() params = [0.1, 0.2, 0.3, (0.4, 0.5), (0.6, 0.7, 0.8)] target_c = Circuit(3) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.CRY(0, 1, theta=params[1])) target_c.add(gates.CZ(1, 2)) target_c.add(gates.U1(2, theta=params[2])) target_c.add(gates.CU2(0, 2, *params[3])) target_c.add(gates.U3(1, *params[4])) c.set_parameters(params) np.testing.assert_allclose(c(), target_c()) # Attempt using a flat list params = np.random.random(8) target_c = Circuit(3) target_c.add(gates.RX(0, theta=params[0])) target_c.add(gates.CRY(0, 1, theta=params[1])) target_c.add(gates.CZ(1, 2)) target_c.add(gates.U1(2, theta=params[2])) target_c.add(gates.CU2(0, 2, *params[3:5])) target_c.add(gates.U3(1, *params[5:])) c.set_parameters(params) np.testing.assert_allclose(c(), target_c()) qibo.set_backend(original_backend)
class TrotterHamiltonian(Hamiltonian): """Hamiltonian operator used for Trotterized time evolution. The Hamiltonian represented by this class has the form of Eq. (57) in `arXiv:1901.05824 <https://arxiv.org/abs/1901.05824>`_. Args: *parts (dict): Dictionary whose values are :class:`qibo.base.hamiltonians.Hamiltonian` objects representing the h operators of Eq. (58) in the reference. The keys of the dictionary are tuples of qubit ids (int) that represent the targets of each h term. ground_state (Callable): Optional callable with no arguments that returns the ground state of this ``TrotterHamiltonian``. Specifying this method is useful if the ``TrotterHamiltonian`` is used as the easy Hamiltonian of the adiabatic evolution and its ground state is used as the initial condition. Example: :: from qibo import matrices, hamiltonians # Create h term for critical TFIM Hamiltonian matrix = -np.kron(matrices.Z, matrices.Z) - np.kron(matrices.X, matrices.I) term = hamiltonians.Hamiltonian(2, matrix) # TFIM with periodic boundary conditions is translationally # invariant and therefore the same term can be used for all qubits # Create even and odd Hamiltonian parts (Eq. (43) in arXiv:1901.05824) even_part = {(0, 1): term, (2, 3): term} odd_part = {(1, 2): term, (3, 0): term} # Create a ``TrotterHamiltonian`` object using these parts h = hamiltonians.TrotterHamiltonian(even_part, odd_part) # Alternatively the precoded TFIM model may be used with the # ``trotter`` flag set to ``True`` h = hamiltonians.TFIM(nqubits, h=1.0, trotter=True) """ def __init__(self, *parts, ground_state=None): self.dtype = None # maps each distinct ``Hamiltonian`` term to the set of gates that # are associated with it self.expgate_sets = {} targets_set = set() for part in parts: if not isinstance(part, dict): raise_error(TypeError, "``TrotterHamiltonian`` part should be " "dictionary but is {}." "".format(type(part))) for targets, term in part.items(): if not issubclass(type(term), Hamiltonian): raise_error(TypeError, "Invalid term type {}." "".format(type(term))) if len(targets) != term.nqubits: raise_error(ValueError, "Term targets {} but supports {} " "qubits." "".format(targets, term.nqubits)) if targets in targets_set: raise_error(ValueError, "Targets {} are given in more than " "one term.".format(targets)) targets_set.add(targets) if term not in self.expgate_sets: self.expgate_sets[term] = set() if self.dtype is None: self.dtype = term.matrix.dtype elif term.matrix.dtype != self.dtype: raise_error(TypeError, "Terms of different types {} and {} " "were given.".format( term.matrix.dtype, self.dtype)) self.parts = parts self.nqubits = len({t for targets in targets_set for t in targets}) self.nterms = sum(len(part) for part in self.parts) # Function that creates the ground state of this Hamiltonian # can be ``None`` self.ground_state_func = ground_state # Circuit that implements on Trotter dt step self._circuit = None # List of gates that implement each Hamiltonian term. Useful for # calculating expectation self._terms = None # Define dense Hamiltonian attributes self._matrix = None self._dense = None self._eigenvalues = None self._eigenvectors = None self._exp = {"a": None, "result": None} @classmethod def from_dictionary(cls, terms, ground_state=None): parts = cls._split_terms(terms) return cls(*parts, ground_state=ground_state) @classmethod def from_symbolic(cls, symbolic_hamiltonian, symbol_map, ground_state=None): """Creates a ``TrotterHamiltonian`` from a symbolic Hamiltonian. We refer to the :ref:`How to define custom Hamiltonians using symbols? <symbolicham-example>` example for more details. Args: symbolic_hamiltonian (sympy.Expr): The full Hamiltonian written with symbols. symbol_map (dict): Dictionary that maps each symbol that appears in the Hamiltonian to a pair of (target, matrix). ground_state (Callable): Optional callable with no arguments that returns the ground state of this ``TrotterHamiltonian``. See :class:`qibo.base.hamiltonians.TrotterHamiltonian` for more details. Returns: A :class:`qibo.base.hamiltonians.TrotterHamiltonian` object that implements the given symbolic Hamiltonian. """ from qibo.hamiltonians import Hamiltonian terms, constant = _SymbolicHamiltonian( symbolic_hamiltonian, symbol_map).trotter_terms() terms = {k: Hamiltonian(len(k), v, numpy=True) for k, v in terms.items()} return cls.from_dictionary(terms, ground_state=ground_state) + constant @staticmethod def _split_terms(terms): """Splits a dictionary of terms to multiple parts. Each qubit should not appear in more that one terms in each part to ensure commutation relations in the definition of :class:`qibo.base.hamiltonians.TrotterHamiltonian`. Args: terms (dict): Dictionary that maps tuples of targets to the matrix that acts on these on targets. Returns: List of dictionary parts to be used for the creation of a ``TrotterHamiltonian``. The parts are such that no qubit appears twice in each part. """ groups, singles = [set()], [set()] for targets in terms.keys(): flag = True t = set(targets) for g, s in zip(groups, singles): if not t & s: s |= t g.add(targets) flag = False break if flag: groups.append({targets}) singles.append(t) return [{k: terms[k] for k in g} for g in groups] def is_compatible(self, o): """Checks if a ``TrotterHamiltonian`` has the same part structure. ``TrotterHamiltonian``s with the same part structure can be add. Args: o: The second Hamiltonian to check. Returns: ``True`` if ``o`` has the same structure as ``self`` otherwise ``False``. """ if isinstance(o, self.__class__): if len(self.parts) != len(o.parts): return False for part1, part2 in zip(self.parts, o.parts): if set(part1.keys()) != set(part2.keys()): return False return True return False def make_compatible(self, o): """Makes given ``TrotterHamiltonian`` compatible to the current one. Args: o: The ``TrotterHamiltonian`` to make compatible to the current. Should be non-interacting (contain only one-qubit terms). Returns: A new :class:`qibo.base.hamiltonians.TrotterHamiltonian` object that is equivalent to ``o`` but has the same part structure as ``self``. """ if not isinstance(o, self.__class__): raise TypeError("Only ``TrotterHamiltonians`` can be made " "compatible but {} was given.".format(type(o))) if self.is_compatible(o): return o oterms = {} for part in o.parts: for t, m in part.items(): if len(t) > 1: raise_error(NotImplementedError, "Only non-interacting Hamiltonians can be " "transformed using the ``make_compatible`` " "method.") oterms[t[0]] = m new_parts = [] for part in self.parts: new_parts.append(dict()) for targets in part.keys(): if targets[0] in oterms: n = len(targets) h = oterms.pop(targets[0]) m = h.matrix eye = np.eye(2 ** (n - 1), dtype=m.dtype) m = np.kron(h.matrix, eye) new_parts[-1][targets] = h.__class__(n, m, numpy=True) if oterms: raise_error(ValueError, "Given non-interacting Hamiltonian cannot " "be made compatible. The following terms " "are remaining: {}".format(oterms.keys())) return self.__class__(*new_parts, ground_state=o.ground_state_func) def _calculate_dense_matrix(self, a): # pragma: no cover # abstract method raise_error(NotImplementedError) @property def dense(self): """Creates an equivalent Hamiltonian model that holds the full matrix. Returns: A :class:`qibo.base.hamiltonians.Hamiltonian` object that is equivalent to this local Hamiltonian. """ if self._dense is None: from qibo import hamiltonians matrix = self._calculate_dense_matrix() # pylint: disable=E1111 self.dense = hamiltonians.Hamiltonian(self.nqubits, matrix) return self._dense @dense.setter def dense(self, hamiltonian): self._dense = hamiltonian self._eigenvalues = hamiltonian._eigenvalues self._eigenvectors = hamiltonian._eigenvectors self._exp = hamiltonian._exp @property def matrix(self): return self.dense.matrix def eigenvalues(self): """Computes the eigenvalues for the Hamiltonian.""" return self.dense.eigenvalues() def eigenvectors(self): """Computes a tensor with the eigenvectors for the Hamiltonian.""" return self.dense.eigenvectors() def ground_state(self): """Computes the ground state of the Hamiltonian. If this method is needed it should be implemented efficiently for the particular Hamiltonian by passing the ``ground_state`` argument during initialization. If this argument is not passed then this method will diagonalize the full (dense) Hamiltonian matrix which is computationally and memory intensive. """ if self.ground_state_func is None: log.info("Ground state function not available for ``TrotterHamiltonian``." "Using dense Hamiltonian eigenvectors.") return self.eigenvectors()[:, 0] return self.ground_state_func() def exp(self, a): """Computes a tensor corresponding to exp(-1j * a * H). Args: a (complex): Complex number to multiply Hamiltonian before exponentiation. """ return self.dense.exp(a) def expectation(self, state, normalize=False): # pragma: no cover """Computes the real expectation value for a given state. Args: state (array): the expectation state. normalize (bool): If ``True`` the expectation value is divided with the state's norm squared. Returns: Real number corresponding to the expectation value. """ # abstract method raise_error(NotImplementedError) def __iter__(self): """Helper iteration method to loop over the Hamiltonian terms.""" for part in self.parts: for targets, term in part.items(): yield targets, term def _create_circuit(self, dt, accelerators=None, memory_device="/CPU:0"): """Creates circuit that implements the Trotterized evolution.""" from qibo.models import Circuit self._circuit = Circuit(self.nqubits, accelerators=accelerators, memory_device=memory_device) self._circuit.check_initial_state_shape = False self._circuit.dt = None for part in itertools.chain(self.parts, self.parts[::-1]): for targets, term in part.items(): gate = gates.Unitary(term.exp(dt / 2.0), *targets) self.expgate_sets[term].add(gate) self._circuit.add(gate) def terms(self): if self._terms is None: self._terms = [gates.Unitary(term.matrix, *targets) for targets, term in self] return self._terms def circuit(self, dt, accelerators=None, memory_device="/CPU:0"): """Circuit implementing second order Trotter time step. Args: dt (float): Time step to use for Trotterization. Returns: :class:`qibo.base.circuit.BaseCircuit` that implements a single time step of the second order Trotterized evolution. """ if self._circuit is None: self._create_circuit(dt, accelerators, memory_device) elif dt != self._circuit.dt: self._circuit.dt = dt self._circuit.set_parameters({ gate: term.exp(dt / 2.0) for term, expgates in self.expgate_sets.items() for gate in expgates}) return self._circuit def _scalar_op(self, op, o): """Helper method for implementing operations with scalars. Args: op (str): String that defines the operation, such as '__add__' or '__mul__'. o: Scalar to perform operation for. """ new_parts = [] new_terms = {term: getattr(term, op)(o) for term in self.expgate_sets.keys()} new_parts = ({targets: new_terms[term] for targets, term in part.items()} for part in self.parts) new = self.__class__(*new_parts) if self._dense is not None: new.dense = getattr(self.dense, op)(o) if self._circuit is not None: new._circuit = self._circuit new._circuit.dt = None new.expgate_sets = {new_terms[term]: gate_set for term, gate_set in self.expgate_sets.items()} return new def _hamiltonian_op(self, op, o): """Helper method for implementing operations between local Hamiltonians. Args: op (str): String that defines the operation, such as '__add__'. o (:class:`qibo.base.hamiltonians.TrotterHamiltonian`): Other local Hamiltonian to perform the operation. """ if len(self.parts) != len(o.parts): raise_error(ValueError, "Cannot add local Hamiltonians if their " "parts are not compatible.") new_terms = {} def new_parts(): for part1, part2 in zip(self.parts, o.parts): if set(part1.keys()) != set(part2.keys()): raise_error(ValueError, "Cannot add local Hamiltonians " "if their parts are not " "compatible.") new_part = {} for targets in part1.keys(): term_tuple = (part1[targets], part2[targets]) if term_tuple not in new_terms: new_terms[term_tuple] = getattr(part1[targets], op)( part2[targets]) new_part[targets] = new_terms[term_tuple] yield new_part new = self.__class__(*new_parts()) if self._circuit is not None: new.expgate_sets = {new_term: self.expgate_sets[t1] for (t1, _), new_term in new_terms.items()} new._circuit = self._circuit new._circuit.dt = None return new def __add__(self, o): """Add operator.""" if isinstance(o, self.__class__): return self._hamiltonian_op("__add__", o) else: return self._scalar_op("__add__", o / self.nterms) def __radd__(self, o): """Right operator addition.""" return self.__add__(o) def __sub__(self, o): """Subtraction operator.""" if isinstance(o, self.__class__): return self._hamiltonian_op("__sub__", o) else: return self._scalar_op("__sub__", o / self.nterms) def __rsub__(self, o): """Right subtraction operator.""" return self._scalar_op("__rsub__", o / self.nterms) def __mul__(self, o): """Multiplication to scalar operator.""" return self._scalar_op("__mul__", o) def __rmul__(self, o): """Right scalar multiplication.""" return self.__mul__(o) def __matmul__(self, state): # pragma: no cover """Matrix multiplication with state vectors.""" # abstract method raise_error(NotImplementedError)
class TrotterHamiltonian(Hamiltonian): """Hamiltonian operator used for Trotterized time evolution. The Hamiltonian represented by this class has the form of Eq. (57) in `arXiv:1901.05824 <https://arxiv.org/abs/1901.05824>`_. Args: *parts (dict): Dictionary whose values are :class:`qibo.base.hamiltonians.Hamiltonian` objects representing the h operators of Eq. (58) in the reference. The keys of the dictionary are tuples of qubit ids (int) that represent the targets of each h term. ground_state (Callable): Optional callable with no arguments that returns the ground state of this ``TrotterHamiltonian``. Specifying this method is useful if the ``TrotterHamiltonian`` is used as the easy Hamiltonian of the adiabatic evolution and its ground state is used as the initial condition. Example: :: from qibo import matrices, hamiltonians # Create h term for critical TFIM Hamiltonian matrix = -np.kron(matrices.Z, matrices.Z) - np.kron(matrices.X, matrices.I) term = hamiltonians.Hamiltonian(2, matrix) # TFIM with periodic boundary conditions is translationally # invariant and therefore the same term can be used for all qubits # Create even and odd Hamiltonian parts (Eq. (43) in arXiv:1901.05824) even_part = {(0, 1): term, (2, 3): term} odd_part = {(1, 2): term, (3, 0): term} # Create a ``TrotterHamiltonian`` object using these parts h = hamiltonians.TrotterHamiltonian(even_part, odd_part) # Alternatively the precoded TFIM model may be used with the # ``trotter`` flag set to ``True`` h = hamiltonians.TFIM(nqubits, h=1.0, trotter=True) """ def __init__(self, *parts, ground_state=None): self.dtype = None # maps each distinct ``Hamiltonian`` term to the set of gates that # are associated with it self.expgate_sets = {} targets_set = set() for part in parts: if not isinstance(part, dict): raise_error(TypeError, "``TrotterHamiltonian`` part should be " "dictionary but is {}." "".format(type(part))) for targets, term in part.items(): if not issubclass(type(term), Hamiltonian): raise_error(TypeError, "Invalid term type {}." "".format(type(term))) if len(targets) != term.nqubits: raise_error(ValueError, "Term targets {} but supports {} " "qubits." "".format(targets, term.nqubits)) if targets in targets_set: raise_error(ValueError, "Targets {} are given in more than " "one term.".format(targets)) targets_set.add(targets) if term not in self.expgate_sets: self.expgate_sets[term] = set() if self.dtype is None: self.dtype = term.matrix.dtype elif term.matrix.dtype != self.dtype: raise_error(TypeError, "Terms of different types {} and {} " "were given.".format( term.matrix.dtype, self.dtype)) self.parts = parts self.nqubits = len({t for targets in targets_set for t in targets}) self.nterms = sum(len(part) for part in self.parts) # Function that creates the ground state of this Hamiltonian # can be ``None`` self.ground_state_func = ground_state # Circuit that implements on Trotter dt step self._circuit = None # List of gates that implement each Hamiltonian term. Useful for # calculating expectation self._terms = None # Define dense Hamiltonian attributes self._matrix = None self._dense = None self._eigenvalues = None self._eigenvectors = None self._exp = {"a": None, "result": None} @classmethod def from_twoqubit_term(cls, nqubits, term, ground_state=None): """:class:`qibo.base.hamiltonians.TrotterHamiltonian` for translationally invariant models. It is assumed that the system has periodic boundary conditions and the local term acts on exactly two qubits. Args: nqubits (int): Number of qubits in the system. term (:class:`qibo.base.hamiltonians.Hamiltonian`): Hamiltonian object representing the local operator. The total Hamiltonian is sum of this term acting on each of the qubits. ground_state (Callable): Optional callable with no arguments that returns the ground state of this ``TrotterHamiltonian``. See ``__init__`` documentation for more details. """ if not isinstance(nqubits, int) or nqubits < 1: raise_error(ValueError, "nqubits must be a positive integer but is " "{}".format(nqubits)) if term.nqubits != 2: raise_error(ValueError, "Term in translationally invariant local " "Hamiltonians should act on two qubits " "but acts on {}.".format(term.nqubits)) even_terms = {(2 * i, (2 * i + 1) % nqubits): term for i in range(nqubits // 2 + nqubits % 2)} odd_terms = {(2 * i + 1, (2 * i + 2) % nqubits): term for i in range(nqubits // 2)} return cls(even_terms, odd_terms, ground_state=ground_state) def _calculate_dense_matrix(self, a): # pragma: no cover # abstract method raise_error(NotImplementedError) @property def dense(self): """Creates an equivalent Hamiltonian model that holds the full matrix. Returns: A :class:`qibo.base.hamiltonians.Hamiltonian` object that is equivalent to this local Hamiltonian. """ if self._dense is None: from qibo import hamiltonians matrix = self._calculate_dense_matrix() # pylint: disable=E1111 print("abc") self.dense = hamiltonians.Hamiltonian(self.nqubits, matrix) return self._dense @dense.setter def dense(self, hamiltonian): self._dense = hamiltonian self._eigenvalues = hamiltonian._eigenvalues self._eigenvectors = hamiltonian._eigenvectors self._exp = hamiltonian._exp @property def matrix(self): return self.dense.matrix def eigenvalues(self): """Computes the eigenvalues for the Hamiltonian.""" return self.dense.eigenvalues() def eigenvectors(self): """Computes a tensor with the eigenvectors for the Hamiltonian.""" return self.dense.eigenvectors() def ground_state(self): """Computes the ground state of the Hamiltonian. If this method is needed it should be implemented efficiently for the particular Hamiltonian by passing the ``ground_state`` argument during initialization. If this argument is not passed then this method will diagonalize the full (dense) Hamiltonian matrix which is computationally and memory intensive. """ if self.ground_state_func is None: log.info("Ground state function not available for ``TrotterHamiltonian``." "Using dense Hamiltonian eigenvectors.") return self.eigenvectors()[:, 0] return self.ground_state_func() def exp(self, a): """Computes a tensor corresponding to exp(-1j * a * H). Args: a (complex): Complex number to multiply Hamiltonian before exponentiation. """ return self.dense.exp(a) def expectation(self, state, normalize=False): # pragma: no cover """Computes the real expectation value for a given state. Args: state (array): the expectation state. normalize (bool): If ``True`` the expectation value is divided with the state's norm squared. Returns: Real number corresponding to the expectation value. """ # abstract method raise_error(NotImplementedError) def __iter__(self): """Helper iteration method to loop over the Hamiltonian terms.""" for part in self.parts: for targets, term in part.items(): yield targets, term def _create_circuit(self, dt, accelerators=None, memory_device="/CPU:0"): """Creates circuit that implements the Trotterized evolution.""" from qibo.models import Circuit self._circuit = Circuit(self.nqubits, accelerators=accelerators, memory_device=memory_device) self._circuit.check_initial_state_shape = False self._circuit.dt = None for part in itertools.chain(self.parts, self.parts[::-1]): for targets, term in part.items(): gate = gates.Unitary(term.exp(dt / 2.0), *targets) self.expgate_sets[term].add(gate) self._circuit.add(gate) def terms(self): if self._terms is None: self._terms = [gates.Unitary(term.matrix, *targets) for targets, term in self] return self._terms def circuit(self, dt, accelerators=None, memory_device="/CPU:0"): """Circuit implementing second order Trotter time step. Args: dt (float): Time step to use for Trotterization. Returns: :class:`qibo.base.circuit.BaseCircuit` that implements a single time step of the second order Trotterized evolution. """ if self._circuit is None: self._create_circuit(dt, accelerators, memory_device) elif dt != self._circuit.dt: self._circuit.dt = dt self._circuit.set_parameters({ gate: term.exp(dt / 2.0) for term, expgates in self.expgate_sets.items() for gate in expgates}) return self._circuit def _scalar_op(self, op, o): """Helper method for implementing operations with scalars. Args: op (str): String that defines the operation, such as '__add__' or '__mul__'. o: Scalar to perform operation for. """ new_parts = [] new_terms = {term: getattr(term, op)(o) for term in self.expgate_sets.keys()} new_parts = ({targets: new_terms[term] for targets, term in part.items()} for part in self.parts) new = self.__class__(*new_parts) if self._dense is not None: new.dense = getattr(self.dense, op)(o) if self._circuit is not None: new._circuit = self._circuit new._circuit.dt = None new.expgate_sets = {new_terms[term]: gate_set for term, gate_set in self.expgate_sets.items()} return new def _hamiltonian_op(self, op, o): """Helper method for implementing operations between local Hamiltonians. Args: op (str): String that defines the operation, such as '__add__'. o (:class:`qibo.base.hamiltonians.TrotterHamiltonian`): Other local Hamiltonian to perform the operation. """ if len(self.parts) != len(o.parts): raise_error(ValueError, "Cannot add local Hamiltonians if their " "parts are not compatible.") new_terms = {} def new_parts(): for part1, part2 in zip(self.parts, o.parts): if set(part1.keys()) != set(part2.keys()): raise_error(ValueError, "Cannot add local Hamiltonians " "if their parts are not " "compatible.") new_part = {} for targets in part1.keys(): term_tuple = (part1[targets], part2[targets]) if term_tuple not in new_terms: new_terms[term_tuple] = getattr(part1[targets], op)( part2[targets]) new_part[targets] = new_terms[term_tuple] yield new_part new = self.__class__(*new_parts()) if self._circuit is not None: new.expgate_sets = {new_term: self.expgate_sets[t1] for (t1, _), new_term in new_terms.items()} new._circuit = self._circuit new._circuit.dt = None return new def __add__(self, o): """Add operator.""" if isinstance(o, self.__class__): return self._hamiltonian_op("__add__", o) else: return self._scalar_op("__add__", o / self.nterms) def __radd__(self, o): """Right operator addition.""" return self.__add__(o) def __sub__(self, o): """Subtraction operator.""" if isinstance(o, self.__class__): return self._hamiltonian_op("__sub__", o) else: return self._scalar_op("__sub__", o / self.nterms) def __rsub__(self, o): """Right subtraction operator.""" return self._scalar_op("__rsub__", o / self.nterms) def __mul__(self, o): """Multiplication to scalar operator.""" return self._scalar_op("__mul__", o) def __rmul__(self, o): """Right scalar multiplication.""" return self.__mul__(o) def __matmul__(self, state): # pragma: no cover """Matrix multiplication with state vectors.""" # abstract method raise_error(NotImplementedError)