import numpy as np from abc import abstractmethod class AbstractCircuit: def __init__(self, nqubits): self.nqubits = nqubits self.parameters = {} @abstractmethod def __iter__(self): raise NotImplementedError def to_qasm(self, theta=None): """Creates the circuit in OpenQASM format. Args: theta (np.ndarray): If not ``None`` ``RX`` gates with the given angles are added before the actual circuit gates so that the initial state is non-trivial. Useful for testing. Returns: A string with the circuit in OpenQASM format. """ code = ['OPENQASM 2.0;', 'include "qelib1.inc";', f'qreg q[{self.nqubits}];', f'creg m[{self.nqubits}];'] if theta is not None: code.extend(f"rx({t}) q[{i}];" for i, t in enumerate(theta)) code.extend(iter(self)) return "\n".join(code) def __str__(self): return ", ".join(f"{k}={v}" for k, v in self.parameters.items()) class OneQubitGate(AbstractCircuit): """Applies a specific one qubit gate to all qubits.""" def __init__(self, nqubits, nlayers="1", gate="h", angles=""): super().__init__(nqubits) self.gate = gate self.nlayers = int(nlayers) self.angles = angles self.parameters = {"nqubits": nqubits, "nlayers": nlayers, "gate": gate, "params": angles} def base_command(self, i): if self.angles: return "{}({}) q[{}];".format(self.gate, self.angles, i) else: return "{} q[{}];".format(self.gate, i) def __iter__(self): for _ in range(self.nlayers): for i in range(self.nqubits): yield self.base_command(i) class TwoQubitGate(OneQubitGate): """Applies a specific two qubit gate to all pairs of adjacent qubits.""" def __init__(self, nqubits, nlayers="1", gate="cx", angles=""): super().__init__(nqubits, nlayers, gate, angles) def base_command(self, i): if self.angles: return "{}({}) q[{}],q[{}];".format(self.gate, self.angles, i, i + 1) else: return "{} q[{}],q[{}];".format(self.gate, i, i + 1) def __iter__(self): for _ in range(self.nlayers): for i in range(0, self.nqubits - 1, 2): yield self.base_command(i) for i in range(1, self.nqubits - 1, 2): yield self.base_command(i) class QFT(AbstractCircuit): """Applies the Quantum Fourier Transform.""" def __init__(self, nqubits, swaps="True"): super().__init__(nqubits) self.swaps = swaps == "True" self.parameters = {"nqubits": nqubits, "swaps": swaps} def __iter__(self): for i1 in range(self.nqubits): yield f"h q[{i1}];" for i2 in range(i1 + 1, self.nqubits): theta = np.pi / 2 ** (i2 - i1) yield f"cu1({theta}) q[{i2}],q[{i1}];" if self.swaps: for i in range(self.nqubits // 2): yield f"swap q[{i}],q[{self.nqubits - i - 1}];" class VariationalCircuit(AbstractCircuit): """Example variational circuit consisting of alternating layers of RY and CZ gates.""" def __init__(self, nqubits, nlayers="1", seed="123"): super().__init__(nqubits) self.nlayers = int(nlayers) self.seed = int(seed) self.parameters = {"nqubits": nqubits, "nlayers": nlayers, "seed": seed} def __iter__(self): nparams = 2 * self.nlayers * self.nqubits np.random.seed(self.seed) theta = iter(2 * np.pi * np.random.random(nparams)) for l in range(self.nlayers): for i in range(self.nqubits): yield f"ry({next(theta)}) q[{i}];" for i in range(0, self.nqubits - 1, 2): yield f"cz q[{i}],q[{i + 1}];" for i in range(self.nqubits): yield f"ry({next(theta)}) q[{i}];" for i in range(1, self.nqubits - 2, 2): yield f"cz q[{i}],q[{i + 1}];" yield f"cz q[{0}],q[{self.nqubits - 1}];" class BernsteinVazirani(AbstractCircuit): """Applies the Bernstein-Vazirani algorithm from Qiskit/openqasm. See `https://github.com/Qiskit/openqasm/tree/0af8b8489f32d46692b3a3a1421e98c611cd86cc/benchmarks/bv` for the OpenQASM code. Note that `Barrier` gates are excluded for simulation. """ def __init__(self, nqubits): super().__init__(nqubits) self.parameters = {"nqubits": nqubits} def __iter__(self): yield f"x q[{self.nqubits - 1}];" for i in range(self.nqubits): yield f"h q[{i}];" for i in range(self.nqubits - 1): yield f"cx q[{i}],q[{self.nqubits - 1}];" for i in range(self.nqubits - 1): yield f"h q[{i}];" #for i in range(self.nqubits - 1): # yield f"measure m[{i}];" class HiddenShift(AbstractCircuit): """Applies the Hidden Shift algorithm. See `https://github.com/quantumlib/Cirq/blob/master/examples/hidden_shift_algorithm.py` for the Cirq code. If the shift (hidden bitstring) is not given then it is randomly generated using `np.random.randint`. """ def __init__(self, nqubits, shift=""): super().__init__(nqubits) if len(shift): if len(shift) != nqubits: raise ValueError("Shift bitstring of length {} was given for " "circuit of {} qubits." "".format(len(shift), nqubits)) self.shift = [int(x) for x in shift] else: self.shift = np.random.randint(0, 2, size=(self.nqubits,)) self.parameters = {"nqubits": nqubits, "shift": shift} def oracle(self): for i in range(self.nqubits // 2): yield f"cz q[{2 * i}],q[{2 * i + 1}];" def __iter__(self): for i in range(self.nqubits): yield f"h q[{i}];" for i, ish in enumerate(self.shift): if ish: yield f"x q[{i}];" for gate in self.oracle(): yield gate for i, ish in enumerate(self.shift): if ish: yield f"x q[{i}];" for i in range(self.nqubits): yield f"h q[{i}];" for gate in self.oracle(): yield gate for i in range(self.nqubits): yield f"h q[{i}];" #for i in range(self.nqubits): # yield f"measure m[{i}];" class QAOA(AbstractCircuit): """Example QAOA circuit for a MaxCut problem instance. See `https://github.com/quantumlib/Cirq/blob/master/examples/qaoa.py` for the Cirq code. If a JSON file containing the node link structure is given then the graph is loaded using `networkx.readwrite.json_graph.node_link_graph`, otherwise the graph is generated randomly using `networkx.random_regular_graph`. Note that different graphs may lead to different performance as the graph structure affects circuit depth. """ def __init__(self, nqubits, nparams="2", graph="", seed="123"): super().__init__(nqubits) import networkx self.nparams = int(nparams) self.seed = int(seed) if len(graph): import json with open(graph, "r") as file: data = json.load(file) self.graph = networkx.readwrite.json_graph.node_link_graph(data) else: self.graph = networkx.random_regular_graph( 3, self.nqubits, seed=self.seed ) self.parameters = {"nqubits": nqubits, "nparams": nparams, "graph": graph, "seed": seed} @staticmethod def RX(q, theta): return f"rx({theta}) q[{q}];" @staticmethod def RZZ(q0, q1, theta): return f"rzz({theta}) q[{q0}],q[{q1}];" def maxcut_unitary(self, betas, gammas): for beta, gamma in zip(betas, gammas): for i, j in self.graph.edges: yield self.RZZ(i, j, -0.5 * gamma) for i in range(self.nqubits): yield self.RX(i, 2 * beta) def dump(self, dir): """Saves graph data as JSON in given directory.""" import json import networkx data = networkx.readwrite.json_graph.node_link_data(self.graph) with open(dir, "w") as file: json.dump(data, file) def __iter__(self): np.random.seed(self.seed) betas = np.random.uniform(-np.pi, np.pi, size=self.nparams) gammas = np.random.uniform(-np.pi, np.pi, size=self.nparams) # Prepare uniform superposition for i in range(self.nqubits): yield f"h q[{i}];" # Apply QAOA unitary for gate in self.maxcut_unitary(betas, gammas): yield gate # Measure # yield gates.M(*range(self.nqubits)) class SupremacyCircuit(AbstractCircuit): """Random circuit by Boixo et al 2018 for demonstrating quantum supremacy. See `https://github.com/quantumlib/Cirq/blob/v0.11.0/cirq-core/cirq/experiments/google_v2_supremacy_circuit.py` for the Cirq code. This circuit is constructed using `cirq` by exporting to OpenQASM and importing back to Qibo. """ def __init__(self, nqubits, depth="2", seed="123"): super().__init__(nqubits) self.depth = int(depth) self.seed = int(seed) self.parameters = {"nqubits": nqubits, "depth": depth, "seed": seed} self.cirq_circuit = self.create_cirq_circuit() def create_cirq_circuit(self): import cirq from cirq.experiments import google_v2_supremacy_circuit as spc qubits = [cirq.GridQubit(i, 0) for i in range(self.nqubits)] return spc.generate_boixo_2018_supremacy_circuits_v2(qubits, self.depth, self.seed) def __iter__(self): qasm = self.cirq_circuit.to_qasm() for line in qasm.split("\n"): first_word = line.split(" ")[0] if first_word not in {"//", "OPENQASM", "include", "qreg"}: if first_word == "sx": yield line.replace("sx", "rx(pi*0.5)") # see issue #13 else: yield line class BasisChange(AbstractCircuit): """Basis change fermionic circuit. See `https://quantumai.google/openfermion/tutorials/circuits_1_basis_change` for OpenFermion/Cirq code. This circuit is constructed using `openfermion` and `cirq` by exporting to OpenQASM and importing back to Qibo. """ def __init__(self, nqubits, simulation_time="1", seed="123"): super().__init__(nqubits) self.simulation_time = float(simulation_time) self.seed = int(seed) self.parameters = {"nqubits": nqubits, "simulation_time": simulation_time, "seed": seed} self.openfermion_circuit = self.create_openfermion_circuit() def create_openfermion_circuit(self): import cirq import openfermion # Generate the random one-body operator. T = openfermion.random_hermitian_matrix(self.nqubits, seed=self.seed) # Diagonalize T and obtain basis transformation matrix (aka "u"). eigenvalues, eigenvectors = np.linalg.eigh(T) basis_transformation_matrix = eigenvectors.transpose() # Initialize the qubit register. qubits = cirq.LineQubit.range(self.nqubits) # Start circuit with the inverse basis rotation, print out this step. inverse_basis_rotation = cirq.inverse(openfermion.bogoliubov_transform(qubits, basis_transformation_matrix)) circuit = cirq.Circuit(inverse_basis_rotation) # Add diagonal phase rotations to circuit. for k, eigenvalue in enumerate(eigenvalues): phase = -eigenvalue * self.simulation_time circuit.append(cirq.rz(rads=phase).on(qubits[k])) # Finally, restore basis. basis_rotation = openfermion.bogoliubov_transform(qubits, basis_transformation_matrix) circuit.append(basis_rotation) return circuit def __iter__(self): qasm = self.openfermion_circuit.to_qasm() for line in qasm.split("\n"): first_word = line.split(" ")[0] if first_word not in {"//", "OPENQASM", "include", "qreg"}: yield line class QuantumVolume(AbstractCircuit): """Quantum Volume circuit from Qiskit. See `https://qiskit.org/documentation/stubs/qiskit.circuit.library.QuantumVolume.html` for the Qiskit model. This circuit is constructed using `qiskit` by exporting to OpenQASM and importing back to Qibo. """ def __init__(self, nqubits, depth="1", seed="123"): super().__init__(nqubits) self.depth = int(depth) self.seed = int(seed) self.parameters = {"nqubits": nqubits, "depth": depth, "seed": seed} self.qiskit_circuit = self.create_qiskit_circuit() self.expression_symbols = {"*", "/"} self.expression_symbols.update(str(x) for x in range(10)) def create_qiskit_circuit(self): from qiskit.circuit.library import QuantumVolume circuit = QuantumVolume(self.nqubits, self.depth, seed=self.seed) return circuit.decompose().decompose() def __iter__(self): raise NotImplementedError("Iteration is not available for " "`QuantumVolume` because it is prepared " "using Qiskit.") def evaluate_pi(self, qasm): left = qasm.find("pi") if left < 0: return qasm import sympy right = left + 2 left = left - 1 while qasm[left] in self.expression_symbols: left -= 1 while qasm[right] in self.expression_symbols: right += 1 expr = qasm[left + 1: right] evaluated = sympy.sympify(expr).evalf() return self.evaluate_pi(qasm.replace(expr, str(evaluated))) def __iter__(self): qasm = self.qiskit_circuit.qasm() for line in qasm.split("\n"): first_word = line.split(" ")[0] if first_word not in {"//", "OPENQASM", "include", "qreg"}: yield line.replace("1/(15*pi)", str(1.0 / (15.0 * np.pi))) def to_qasm(self, theta=None): qasm = super().to_qasm(theta) return self.evaluate_pi(qasm)