├── EQC ├── Optimizer.py ├── QAOA.py ├── QuantumCircuitOptimizer.py ├── TrainableCircuit.py ├── VQE.py ├── main.py └── qcirc_actor.py ├── LICENSE ├── NOTICE └── README.md /EQC/Optimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Adam(): 5 | def __init__(self, learning_rate=0.01, beta1=0.9, beta2=0.999, epsilon=1e-8): 6 | self.m_dw, self.v_dw = 0, 0 7 | self.beta1 = beta1 8 | self.beta2 = beta2 9 | self.epsilon = epsilon 10 | self.eta = learning_rate 11 | 12 | def update(self, t, w, dw): 13 | # ======================================================================================== 14 | # Update rule for ADAM Optimizer 15 | # ======================================================================================== 16 | t = t+1 17 | self.m_dw = self.beta1*self.m_dw + (1-self.beta1)*dw 18 | self.v_dw = self.beta2*self.v_dw + (1-self.beta2)*(dw**2) 19 | m_dw_corr = self.m_dw/(1-self.beta1**t) 20 | v_dw_corr = self.v_dw/(1-self.beta2**t) 21 | w = w - self.eta*(m_dw_corr/(np.sqrt(v_dw_corr)+self.epsilon)) 22 | return w 23 | 24 | 25 | class SGD(): 26 | def __init__(self,learning_rate=1): 27 | self.learning_rate = learning_rate 28 | 29 | def update(self,epoch,w,dw,z): 30 | # ======================================================================================== 31 | # Update rule for SGD Optimizer 32 | # ======================================================================================== 33 | print(f"Updating with gradient {dw} with Z: {z}") 34 | new_value = w + z*self.learning_rate*dw 35 | return new_value[0] 36 | -------------------------------------------------------------------------------- /EQC/QAOA.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import qiskit 3 | import sys 4 | import random 5 | import matplotlib.pyplot as plt 6 | import os 7 | import json 8 | from TrainableCircuit import TrainableCircuit 9 | from Optimizer import Adam,SGD 10 | 11 | class QAOA(TrainableCircuit): 12 | def __init__(self,k,entanglement='full',seed=45): 13 | super().__init__(k,entanglement,seed) 14 | self.gradient_circuits = None 15 | np.random.seed(seed) 16 | self.seed = seed 17 | self.backend = None 18 | self.parameters = None 19 | self.CIRCUITS_TO_INDUCE = None 20 | self.qubits = qiskit.circuit.QuantumRegister(k) 21 | self.circuit = qiskit.circuit.QuantumCircuit(self.qubits) 22 | self.entanglement_mode = entanglement 23 | self.hamiltonian = np.zeros((2**self.k,2**self.k)) 24 | self.initialize_hamiltonian() 25 | 26 | 27 | def initialize_hamiltonian(self): 28 | self.connections = [(0,1),(0,3),(1,2),(2,3)] 29 | Z = self.PAULI_MATRICES["Z"] 30 | I = self.PAULI_MATRICES["I"] 31 | for pair in self.connections: 32 | h = 1 33 | for i in range(len(self.qubits)): 34 | if i in pair: 35 | h = np.kron(h,Z) 36 | else: 37 | h = np.kron(h,I) 38 | self.hamiltonian += 0.5*(np.identity(h.shape[0])-h) 39 | self.hamiltonian = -1*self.hamiltonian 40 | print(self.hamiltonian) 41 | 42 | def z_or_i(self): 43 | p=0.5 44 | if random.random() > p: 45 | return "Z" 46 | else: 47 | return "I" 48 | 49 | def initialize(self): 50 | if self.parameters is None: 51 | self.parameters = [] 52 | for qub in self.qubits: 53 | self.circuit.h(qub) 54 | # Because there are grouped parameters ... we have to do a lazy method 55 | for _ in range(1): 56 | self.new_param() 57 | for pair in self.connections: 58 | self.circuit.rzz(2 * self.get_last_param(),list(self.qubits)[pair[0]],list(self.qubits)[pair[1]]) 59 | self.new_param() 60 | for qub in list(self.qubits): 61 | self.circuit.rx(2 * self.get_last_param(), qub) 62 | # set up parameter for p2 63 | self.circuit.measure_all() 64 | self.initialize_params() 65 | print(self.circuit) 66 | else: 67 | print("Model already initialized - Parameters exist.") 68 | 69 | def set_backend(self,backend): 70 | self.backend = backend 71 | 72 | def cost_function(self,statevector): 73 | print(np.matmul(np.matmul(statevector.T,self.hamiltonian),statevector)) 74 | return np.matmul(np.matmul(statevector.T,self.hamiltonian),statevector) 75 | 76 | -------------------------------------------------------------------------------- /EQC/QuantumCircuitOptimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import qiskit 3 | import sys 4 | import random 5 | import matplotlib.pyplot as plt 6 | import os 7 | import json 8 | from Optimizer import Adam 9 | from qcirc_actor import RemoteWorker 10 | import ray 11 | import time 12 | import pathlib 13 | from Optimizer import Adam,SGD 14 | 15 | class QuantumCircuitOptimizer: 16 | def __init__(self,circuit,parameters,parameter_values,cost_function,learning_rate,backends,epochs=100): 17 | self.circuit = circuit 18 | self.optimizer = SGD(learning_rate=0.1) 19 | self.parameters = parameters 20 | self.parameter_values = parameter_values 21 | self.epochs = epochs 22 | self.cost_function = cost_function 23 | self.learning_rate = learning_rate 24 | self.backends = backends 25 | print(self.backends) 26 | 27 | def assign_cost_function(self,cost_function): 28 | self.cost_function = cost_function 29 | 30 | def update_params(self,gradient,epoch,index,learning_rate=0.01,descent=True): 31 | if descent is True: 32 | z = -1 33 | else: 34 | z = 1 35 | new_values = [] 36 | print(f"Old value: {self.parameter_values[index]}\nGradient:{gradient}\n") 37 | self.parameter_values[index] = self.optimizer.update(epoch,self.parameter_values[index],gradient,z) 38 | print(f"New Value: {self.parameter_values[index]}") 39 | 40 | def complete_results(self,r_dict:dict): 41 | # QAPP might produce only n values, yet the feature space is 2^n 42 | # To tackle this, generate and append all binary maps that are not featured. 43 | n_bits = len(list(r_dict.keys())[0]) 44 | b_str = ''.join(['1' for _ in range(n_bits)]) 45 | for i in range(int(b_str,2)+1): 46 | key_b = bin(i).lstrip('0b').zfill(n_bits) 47 | if key_b not in r_dict: 48 | r_dict[key_b] = 0 49 | return r_dict 50 | 51 | def get_cost(self): 52 | circuit = self.circuit.assign_parameters(self.parameter_values) 53 | sim = qiskit.Aer.get_backend('aer_simulator') 54 | circ = qiskit.transpile(circuit,sim) 55 | result = sim.run(circ).result().get_counts() 56 | result = self.complete_results(result) 57 | keys = list(result.keys()) 58 | keys.sort() 59 | results = np.array([result[key] for key in keys]) 60 | shots = sum(results) 61 | wavefunc = results/shots 62 | return self.cost_function(wavefunc) 63 | 64 | def save_info(self,file_path,file): 65 | # ======================================================================================== 66 | # Function to save information to a results folder. Must exist prior. 67 | # This can be changed to pickle.dump(), however pickle.dump() does not play well 68 | # with some Backend objects from Qiskit. Hence using a simple string write. 69 | # ======================================================================================== 70 | path = str(pathlib.Path(__file__).parent.resolve()) 71 | path += "/results" 72 | print(path) 73 | with open(path+"FILE_PATH",'w') as f: 74 | f.write(str(file)) 75 | 76 | def train(self,epochs=250,learning_rate=0.,validate=True): 77 | curr_epoch = 0 78 | # ======================================================================================== 79 | # These variables take care of tracking system performance. This is used in the generation 80 | # of analytics for performance 81 | # ======================================================================================== 82 | self.parameter_history = [] 83 | self.workers = [] 84 | self.loss = [] 85 | index = 0 86 | n_complete = 0 87 | ep_pr_hr = [] 88 | # ======================================================================================== 89 | # Cold start max and min values set, updated after 1st returned value from any machine. 90 | # ======================================================================================== 91 | max_p_correct = 0.7 92 | min_p_correct = 0.5 93 | start_epoch = time.time() 94 | for backend in self.backends: 95 | wrkr = RemoteWorker.remote(backend, self.circuit, self.parameter_values, index, 96 | self.cost_function) 97 | self.workers.append(wrkr) 98 | index +=1 99 | if index >= len(self.parameter_values): 100 | index = 0 101 | curr_epoch +=1 102 | futures = [c.run_circuit.remote() for c in self.workers] 103 | while True: 104 | if time.time()-start_epoch > 15*60: 105 | ep_pr_hr.append(n_complete*4) 106 | start_epoch = time.time() 107 | n_complete = 0 108 | ready, not_ready = ray.wait(futures) 109 | print(f"Ready: {ready}") 110 | for obj in ready: 111 | diff_index,grad,p_correct = ray.get(obj) 112 | # ======================================================================================== 113 | # For the weighting system, we set the bound here. This wil be used in the normalisation 114 | # calculation later. If this is not desired, simply set min and max to 0 and 1 respectively. 115 | # If we find a machine with a better or worse performance than observed, we update the new 116 | # max or min p_correct to that. 117 | # ======================================================================================== 118 | if p_correct > max_p_correct: 119 | max_p_correct = p_correct 120 | print(f"Updated max_p_c to {p_correct}") 121 | if p_correct < min_p_correct: 122 | min_p_correct = min_p_correct 123 | # ======================================================================================== 124 | # Use the below equation, adapted accordingly, if you want to use a weighted gradient descent 125 | # approach. However, if you want to use unweighted, comment out the below line and leave it as 126 | # grad = grad. NOTE: Modify the below equation accordingly to scale your weights according 127 | # to the paper 128 | # ======================================================================================== 129 | grad = grad * (0.25*((p_correct - min_p_correct)/(max_p_correct - min_p_correct))+0.75) 130 | #grad = grad 131 | # ======================================================================================== 132 | # This print section prints out information of the returned values from each cilent node 133 | # when it is returned. Comment out if not desired. 134 | # ======================================================================================== 135 | print("Returned Values:") 136 | print(diff_index) 137 | print(grad) 138 | print(p_correct) 139 | print('-'*20) 140 | 141 | # ======================================================================================== 142 | # Logic behind distributing the optimization accross multiple nodes below. 143 | # Mimic of Algorithm 1 in EQC paper 144 | # ======================================================================================== 145 | self.update_params(grad,curr_epoch,diff_index,learning_rate=learning_rate,descent=True) 146 | for indx, future in enumerate(futures): 147 | if future == obj: 148 | r_worker = indx 149 | index += 1 150 | old_epoch = curr_epoch 151 | if index >= len(self.parameter_values): 152 | index = 0 153 | curr_epoch += 1 154 | print(f"Updating Working {r_worker} with Index {index}") 155 | self.workers[r_worker].update_circuit.remote(self.circuit, self.parameter_values, index) 156 | futures[r_worker] = self.workers[r_worker].run_circuit.remote() 157 | if curr_epoch != old_epoch: 158 | n_complete += 1 # For evaluating epochs/hr 159 | cost = self.get_cost() 160 | self.loss.append(cost) 161 | self.parameter_history.append(list(self.parameter_values)) 162 | t_params = self.parameter_history.copy() 163 | t_params = list(t_params) 164 | # ======================================================================================== 165 | # Enable this section of code if you want to save performance, costs, backend list, and parameters 166 | # history to the save_info function mentioned above. 167 | # ======================================================================================== 168 | # self.save_info(None,{"costs": self.loss, 169 | # "performance":ep_pr_hr,"backends":self.backends, 170 | # "parameters":t_params}) 171 | print(f" Current Cost: {cost}") 172 | print(self.parameter_values) 173 | 174 | print("Running a new circuit") 175 | print(f"Not Ready: {not_ready}") 176 | time.sleep(0.01) 177 | if curr_epoch == epochs: 178 | break 179 | -------------------------------------------------------------------------------- /EQC/TrainableCircuit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import qiskit 3 | import sys 4 | import random 5 | import matplotlib.pyplot as plt 6 | import os 7 | import json 8 | from qiskit.circuit import Parameter 9 | from QuantumCircuitOptimizer import QuantumCircuitOptimizer 10 | 11 | 12 | class TrainableCircuit(): 13 | def __init__(self,k,entanglement='full',seed=42): 14 | self.PAULI_MATRICES = {'Z':np.array([[1, 0], [0, -1]]), 15 | 'X':np.array([[0,1],[1,0]]), 16 | 'Y':np.array([[0,0-1j],[0+1j,0]]), 17 | 'I': np.array([[1,0],[0,1]])} 18 | self.parameters = [] 19 | self.k = k 20 | self.parameter_values = None 21 | self.circuit = None 22 | self.backend = None 23 | 24 | def initialize_params(self): 25 | if len(self.parameters) == 0: 26 | print("Your circuit is empty with no trainable parameters") 27 | return 28 | else: 29 | print(f"Initializing {len(self.parameters)} parameter values...") 30 | np.random.seed(self.seed) 31 | self.parameter_values = np.random.rand(len(self.parameters))*np.pi 32 | 33 | def new_param(self): 34 | n_p = Parameter(f"theta_{len(self.parameters)}") 35 | self.parameters.append(n_p) 36 | return n_p 37 | 38 | def get_last_param(self): 39 | return self.parameters[-1] 40 | 41 | def train(self,epochs=100,learning_rate=0.01, 42 | generate_evaluation_metrics = False): 43 | if self.parameters is None or len(self.parameters) == 0: 44 | print("Can not train - Model not initialized - please call model.initialize") 45 | return 46 | else: 47 | optimizer = QuantumCircuitOptimizer(self.circuit,self.parameters,self.parameter_values, 48 | self.cost_function,learning_rate, 49 | self.backend,epochs) 50 | optimizer.assign_cost_function(self.cost_function) 51 | optimizer.train() 52 | -------------------------------------------------------------------------------- /EQC/VQE.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import qiskit 3 | import sys 4 | import random 5 | import matplotlib.pyplot as plt 6 | import os 7 | import json 8 | from TrainableCircuit import TrainableCircuit 9 | from Optimizer import Adam,SGD 10 | 11 | class VQE(TrainableCircuit): 12 | def __init__(self,k,entanglement='full',seed=43): 13 | super().__init__(k,entanglement,seed) 14 | self.gradient_circuits = None 15 | np.random.seed(seed) 16 | self.seed = seed 17 | self.backend = None 18 | self.parameters = None 19 | self.CIRCUITS_TO_INDUCE = None 20 | self.qubits = qiskit.circuit.QuantumRegister(k) 21 | self.circuit = qiskit.circuit.QuantumCircuit(self.qubits) 22 | self.entanglement_mode = entanglement 23 | self.hamiltonian = np.zeros((2**self.k,2**self.k),dtype="complex128") 24 | self.four_qubit_heisenberg() 25 | 26 | def four_qubit_heisenberg(self): 27 | connections = [(0, 1), (0, 3), (1, 2), (2, 3)] 28 | Z = self.PAULI_MATRICES["Z"] 29 | X = self.PAULI_MATRICES["X"] 30 | Y = self.PAULI_MATRICES["Y"] 31 | I = self.PAULI_MATRICES["I"] 32 | for pair in connections: 33 | z = 1 34 | x = 1 35 | y = 1 36 | for i in range(len(self.qubits)): 37 | if i in pair: 38 | z = np.kron(z, Z) 39 | x = np.kron(x, X) 40 | y = np.kron(y, Y) 41 | else: 42 | z = np.kron(z, I) 43 | x = np.kron(x, I) 44 | y = np.kron(y, I) 45 | z = z.astype('complex128') 46 | x = x.astype('complex128') 47 | y = y.astype('complex128') 48 | self.hamiltonian += z + x + y 49 | for qub in range(len(self.qubits)): 50 | h = 1 51 | for i in range(len(self.qubits)): 52 | if i == qub: 53 | h = np.kron(h, Z) 54 | else: 55 | h = np.kron(h, I) 56 | h = h.astype('complex128') 57 | self.hamiltonian += h 58 | print(self.hamiltonian) 59 | 60 | 61 | 62 | def initialize_random_hamiltonian_matrix(self,pauli_model=True): 63 | if pauli_model is True: 64 | weights = np.random.random(10) 65 | for weight in weights: 66 | new_matrix = 1 67 | for i in range(self.k): 68 | new_matrix = np.kron(new_matrix, self.PAULI_MATRICES[self.z_or_i()]) 69 | self.hamiltonian += new_matrix*weight*(1/len(weights)) 70 | else: 71 | self.hamiltonian = np.random.rand(2**self.k,2**self.k) 72 | self.hamiltonian *= 10 73 | 74 | def z_or_i(self): 75 | p=0.5 76 | if random.random() > p: 77 | return "Z" 78 | else: 79 | return "I" 80 | 81 | def initialize(self): 82 | if self.parameters is None: 83 | self.parameters = [] 84 | for qub in self.qubits: 85 | self.circuit.ry(self.new_param(),qub) 86 | self.circuit.rz(self.new_param(),qub) 87 | for q1,q2 in zip(list(self.qubits)[:-1],list(self.qubits)[1:]): 88 | self.circuit.cx(q1,q2) 89 | self.circuit.cx(self.qubits[-1],self.qubits[0]) 90 | for qub in self.qubits: 91 | self.circuit.ry(self.new_param(),qub) 92 | self.circuit.rz(self.new_param(),qub) 93 | self.circuit.measure_all() 94 | self.initialize_params() 95 | else: 96 | print("Model already initialized - Parameters exist.") 97 | 98 | def set_backend(self,backend): 99 | self.backend = backend 100 | 101 | def cost_function(self,statevector): 102 | return np.matmul(np.matmul(statevector.T,self.hamiltonian),statevector) 103 | 104 | -------------------------------------------------------------------------------- /EQC/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import qiskit 4 | import numpy as np 5 | import ray 6 | import time 7 | from qcirc_actor import RemoteWorker 8 | from VQE import VQE 9 | import pathlib 10 | from QAOA import QAOA 11 | 12 | ray.init() 13 | path =str(pathlib.Path(__file__).parent.absolute()) 14 | 15 | if __name__ == "__main__": 16 | print("-"*20+"\nMain.py starting...\n"+"-"*20) 17 | k=4 18 | # ======================================================================================== 19 | # Generate a list of backends using the qiskit provider.backends call 20 | # This can be a combination of providers, so long as they are independantly addressable 21 | # ======================================================================================== 22 | backends = [None] 23 | # ======================================================================================== 24 | # VQE and QAOA are two classes described in this package. They are examples of how to build 25 | # a variational quantum algorithm object, and then implement it onto EQC. 26 | # They inherit from the TrainableCircuit class. 27 | # ======================================================================================== 28 | vqe = VQE(k) 29 | vqe.initialize() 30 | vqe.set_backend(backends) 31 | vqe.train(learning_rate=.1) 32 | 33 | -------------------------------------------------------------------------------- /EQC/qcirc_actor.py: -------------------------------------------------------------------------------- 1 | import ray 2 | import qiskit 3 | import numpy as np 4 | import time 5 | from qiskit.providers.ibmq.managed import IBMQJobManager 6 | import datetime 7 | 8 | 9 | # ======================================================================================== 10 | # This is the client code. Each client is initiated with this class, and holds all of its methods 11 | # This is how the asynchronous updating and requesting of circuits is done. 12 | # ======================================================================================== 13 | @ray.remote 14 | class RemoteWorker: 15 | def __init__(self, backend, circuit, parameters, 16 | index, cost_function): 17 | qiskit.IBMQ.load_account() 18 | self.bck, self.fwd = None, None 19 | try: 20 | # ======================================================================================== 21 | # Change this to your provider 22 | # ======================================================================================== 23 | provider = None 24 | self.backend = provider.get_backend(backend) 25 | except: 26 | try: 27 | provider = qiskit.IBMQ.get_provider(hub='ibm-q') 28 | self.backend = provider.get_backend(backend) 29 | except: 30 | self.backend = backend 31 | self.index = index 32 | self.cost_function = cost_function 33 | self.parameter_values = parameters 34 | self.circuit = circuit 35 | self.k = self.circuit.num_qubits 36 | self.generate_gradient_circuits() 37 | self.circuits = qiskit.transpile(self.circuits, backend=self.backend, optimization_level=3, 38 | coupling_map=self.backend.configuration().coupling_map) 39 | 40 | def update_circuit(self,circuit,parameters,index): 41 | self.index = index 42 | self.parameter_values = parameters 43 | self.circuit = circuit 44 | self.generate_gradient_circuits() 45 | self.circuits = qiskit.transpile(self.circuits, backend=self.backend, optimization_level=3, 46 | coupling_map=self.backend.configuration().coupling_map) 47 | 48 | def generate_gradient_circuits(self): 49 | self.differentiate_parameters() 50 | fwd = self.circuit.assign_parameters(self.fwd) 51 | bck = self.circuit.assign_parameters(self.bck) 52 | self.circuits = [fwd, bck] 53 | 54 | def differentiate_parameters(self): 55 | self.fwd = self.parameter_values.copy() 56 | # ======================================================================================== 57 | # You can fine tune the coefficient to the differentiator. Things such as QAOA have extremely 58 | # sensitive cost landscapes, so reducing your finite difference methods bounds can improve performance. 59 | # Changing the 0.5 to a 0.05 for example, can greatly increase your convergence in QAOA examples. 60 | # ======================================================================================== 61 | self.fwd[self.index] += 0.5*np.pi 62 | self.bck = self.parameter_values.copy() 63 | self.bck[self.index] -= 0.5*np.pi 64 | 65 | 66 | def complete_results(self,r_dict:dict): 67 | # ======================================================================================== 68 | # QAPP might produce only n values, yet the feature space is 2^n 69 | # To tackle this, generate and append all binary maps that are not featured. 70 | # ======================================================================================== 71 | n_bits = len(list(r_dict.keys())[0]) 72 | b_str = ''.join(['1' for _ in range(n_bits)]) 73 | for i in range(int(b_str,2)+1): 74 | key_b = bin(i).lstrip('0b').zfill(n_bits) 75 | if key_b not in r_dict: 76 | r_dict[key_b] = 0 77 | return r_dict 78 | 79 | def to_p_dist(self, r_dict: dict): 80 | # ======================================================================================== 81 | # Convert a count-based dictionary to a probability-based dictionary 82 | # ======================================================================================== 83 | r_dict = self.complete_results(r_dict) 84 | keys = list(r_dict.keys()) 85 | keys.sort() 86 | results = np.array([r_dict[key] for key in keys]) 87 | shots = sum(results) 88 | return results / shots 89 | 90 | def calculate_gate_gradients(self,fwd_state,bck_state): 91 | gradients = [] 92 | for f_gate,b_gate in zip(fwd_state,bck_state): 93 | try: 94 | f_cost = self.cost_function(f_gate) 95 | b_cost = self.cost_function(b_gate) 96 | gradient = (f_cost-b_cost)/2 97 | except: 98 | print(f_gate) 99 | print(b_gate) 100 | gradients.append(gradient) 101 | gradients = np.array(gradients) 102 | return gradients 103 | 104 | 105 | def run_circuit(self): 106 | # ======================================================================================== 107 | # If backend set to sim, this will not work , hence try except 108 | # ======================================================================================== 109 | try: 110 | job_manager = IBMQJobManager() 111 | job_set_foo = job_manager.run(self.circuits, backend=self.backend,shots=8192) 112 | start = time.time() 113 | while True: 114 | status = job_set_foo.statuses()[0] == job_set_foo.statuses()[0].DONE 115 | if status: 116 | break 117 | else: 118 | run_time = time.time() - start 119 | # 1 hor is the limit 120 | if run_time > 10400: 121 | status = False 122 | job_set_foo.cancel() 123 | print("Run time exceeded 1 hour - breaking") 124 | break 125 | if status: 126 | # GET RESULTS 127 | print("Succesful") 128 | for i in range(2): 129 | print(f"RESULTS {i}: {job_set_foo.results().get_counts(i)}") 130 | else: 131 | print(f"Failed to run on {self.backend}") 132 | # No gradient, No index 133 | return self.index, np.array([0.0]), 0.0 134 | results = [job_set_foo.results().get_counts(i) for i in range(len(self.circuits))] 135 | except: 136 | job = self.backend.run(self.circuits,shots=8192) 137 | results = job.result().get_counts() 138 | dist_list = [] 139 | for index,circuit in enumerate(self.circuits): 140 | result = results[index] 141 | result = self.complete_results(result) 142 | dist_list.append(self.to_p_dist(result)) 143 | dist_list = np.array(dist_list) 144 | fwd_vector = dist_list[:int(len(dist_list)/2)] 145 | bck_vector = dist_list[int(len(dist_list)/2):] 146 | grad = self.calculate_gate_gradients(fwd_vector,bck_vector) 147 | try: 148 | p_error = self.calculate_p_err(self.circuit,self.backend,self.circuit.num_qubits) 149 | except: 150 | p_error = 1 151 | print(p_error) 152 | return self.index, grad, p_error 153 | 154 | def get_circuit_cost(self): 155 | results = self.backend.run(self.circuit,shots=8000) 156 | result = results.result().get_counts() 157 | result = self.complete_results(result) 158 | return self.cost_function(self.to_p_dist(result)) 159 | 160 | def calculate_p_err(self,circuit, backend, k): 161 | # ======================================================================================== 162 | # P error calculation model from paper. This is assuming the implementation of 163 | # backend.properties().to_dict() has been sufficiently implemented. 164 | # ======================================================================================== 165 | 166 | sq_names = ['ID', 'RZ', 'SX', 'X'] 167 | cx_names = ['CX'] 168 | RESULTS = {} 169 | t_circs = qiskit.transpile(circuit, backend=backend) 170 | t_sq = 0 171 | t_cx = 0 172 | for key in t_circs.count_ops().keys(): 173 | if key.upper() in sq_names: 174 | t_sq += t_circs.count_ops()[key] 175 | elif key.upper() in cx_names: 176 | t_cx += t_circs.count_ops()[key] 177 | else: 178 | pass 179 | data = backend.properties().to_dict() 180 | now = datetime.datetime.now() 181 | t_calibration = datetime.datetime.timestamp(now) - datetime.datetime.timestamp(data['last_update_date']) 182 | # Collects list of CNOT gate time and SQG gate time 183 | cx_times = [] 184 | sq_times = [] 185 | cx_error = [] 186 | sq_error = [] 187 | for gate_info in data['gates']: 188 | if len(gate_info["parameters"]) < 2: 189 | continue 190 | if gate_info["gate"] == "cx": 191 | cx_times.append(gate_info["parameters"][1]["value"]) 192 | cx_error.append(gate_info["parameters"][0]["value"]) 193 | else: 194 | time = gate_info["parameters"][1]["value"] 195 | sq_times.append(time) 196 | sq_error.append(gate_info["parameters"][0]["value"]) 197 | t1_times = [] 198 | m_error = [] 199 | t2_times = [] 200 | for qubit in data['qubits']: 201 | t1_times.append(qubit[0]['value']) 202 | t2_times.append(qubit[1]['value']) 203 | m_error.append(qubit[4]['value']) 204 | t1_times = np.array(t1_times) 205 | t2_times = np.array(t2_times) 206 | sq_times = np.array(sq_times) 207 | cx_times = np.array(cx_times) 208 | m_error = np.array(m_error) 209 | cx_error = np.array(cx_error) 210 | sq_error = np.array(sq_error) 211 | t1 = np.mean(t1_times) * 1000 # Unit conversion 212 | t2 = np.mean(t2_times) * 1000 # Unit conversion 213 | sq = np.mean(sq_error) 214 | cx = np.mean(cx_error) 215 | m = np.mean(m_error) 216 | transpiled_circ = qiskit.transpile(circuit, backend=backend) 217 | depth = transpiled_circ.depth() 218 | t_mu = (cx_times.mean() + sq_times.mean()) / 2 219 | return self.get_p(t1, t2, t_mu, depth, sq, t_sq, cx, t_cx, m, k) 220 | 221 | def get_p(self,t1,t2,t_mu,cd,g1_error,g1,g2_error,g2,m_error,m): 222 | return np.exp((-1*t_mu*cd)/(t1*t2))*(1-g1_error)**g1 * (1-g2_error)**g2 * (1-m_error)**m -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | FreeBSD License 2 | 3 | Copyright © 2020, Battelle Memorial Institute 4 | 5 | 1.Battelle Memorial Institute (hereinafter Battelle) hereby grants permission 6 | to any person or entity lawfully obtaining a copy of this software and associated 7 | documentation files (hereinafter “the Software”) to redistribute and use the 8 | Software in source and binary forms, with or without modification. Such person 9 | or entity may use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and may permit others to do so, subject to the 11 | following conditions: 12 | 13 | - Redistributions of source code must retain the above copyright notice, this list 14 | of conditions and the following disclaimers. 15 | 16 | - Redistributions in binary form must reproduce the above copyright notice, this list 17 | of conditions and the following disclaimer in the documentation and/or other materials 18 | provided with the distribution. 19 | 20 | - Other than as used herein, neither the name Battelle Memorial Institute or Battelle 21 | may be used in any form whatsoever without the express written consent of Battelle. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 24 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 25 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 26 | SHALL BATTELLE OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 28 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | NOTICE 2 | 3 | This material was prepared as an account of work sponsored by an agency of the United States Government. Neither the United States Government nor the United States Department of Energy, nor the Contractor, nor any or their employees, nor any jurisdiction or organization that has cooperated in the development of these materials, makes any warranty, express or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness or any information, apparatus, product, software, or process disclosed, or represents that its use would not infringe privately owned rights. 4 | 5 | Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or any agency thereof, or Battelle Memorial Institute. The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or any agency thereof. 6 | 7 | 8 | PACIFIC NORTHWEST NATIONAL LABORATORY 9 | operated by 10 | BATTELLE 11 | for the 12 | UNITED STATES DEPARTMENT OF ENERGY 13 | under Contract DE-AC05-76RL01830 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # EQC - Ensembled Quantum Computing for Variational Quantum Algorithms (ISCA'22) 3 | 4 | The following repository contains the software used to implement EQC, a distributed variational quantum algorithm optimizer. Please look at our ISCA-22 paper (DOI: 10.1145/3470496.3527434) see below for details. 5 | 6 | EQC is a single-master node multi-client multi-quantum-processor system that utilized asynchronous stochastic gradient descent to optimize variational quantum algorithms. 7 | Key benefits of EQC include substantial reduction in training time, mitigation of device-specific bias and time-dependant drift, and the ability to homogenise multiple quantum providers to one virtualized quantum backend. 8 | 9 | # Prerequisites 10 | 11 | The core requirements for EQC include using Python 3.7.0+, and Ray.io. The dependancies 12 | of your system will also depend on the quantum provider you are connecting to. For 13 | usage with IBM Quantum Experience, using Qiskit would be recommended. However, 14 | providers such as IonQ, Rigetti, etc, will require installation and usage of their 15 | required packages. 16 | 17 | # Utilizing EQC 18 | 19 | To use EQC, generating a similar-styled object that inherits from the TrainableCircuit 20 | class. This requires the declaration 21 | 22 | ```python 23 | class YourVariationalAlgorithm(TrainableCircuit): 24 | ``` 25 | Once your class is implemented, it should contain sufficient information similar to how 26 | QAOA or VQE are implemnted in this repot. These classes include methods such as 27 | cost function, which defines the objects optimization function. 28 | 29 | Having implented your variational algorithm, the train function is called. 30 | This creates the QuantumCircuitOptimizer object, which is the master node of the system. 31 | This object keeps track of global parameters, which iteration it is on, which parameter is next etc. 32 | The train function is based off of the Algorithm 1 presented in the ISCA paper. 33 | 34 | The QuantumCircuitOptimizer initializes a set of client nodes, based on Algorithm 2. 35 | The RemoteWorker object represents a client node, and handles one-on-one communication with 36 | the assigned quantum processor. 37 | 38 | ## Citation format 39 | 40 | If you find this repository useful, please cite our ISCA-22 paper: 41 | - Samuel Stein, Nathan Wiebe, Yufei Ding, Bo Peng, Karol Kowalski, Nathan Baker, James Ang, and Ang Li. "EQC: Ensembled Quantum Computing for Variational Quantum Algorithms" In Proceedings of the International Symposium on Computer Architecture, ACM, 2022. 42 | 43 | Bibtex: 44 | ```text 45 | @inproceedings{li2020density, 46 | title={EQC: Ensembled Quantum Computing for Variational Quantum Algorithms}, 47 | author={Stein, Samuel and Wiebe, Nathan and Ding, Yufei and Peng, Bo and Kowalski, Karol and Baker, Nathan and Ang, James and Li, Ang}, 48 | booktitle={Proceedings of the International Symposium on Computer Architecture}, 49 | year={2022} 50 | } 51 | ``` 52 | 53 | 54 | 55 | 56 | ## License 57 | 58 | This project is licensed under the BSD License, see [LICENSE](LICENSE) file for details. 59 | 60 | 61 | 62 | 63 | # Acknowledgements 64 | **PNNL-IPID: 32474-E** 65 | 66 | This material is based upon work supported by the U.S. Department of Energy, Office of Science, National Quantum Information Science Research Centers, Co-design Center for Quantum Advantage (C2QA) under contract number DE-SC0012704. The Pacific Northwest National Laboratory is operated by Battelle for the U.S. Department of Energy under contract DE-AC05-76RL01830. 67 | --------------------------------------------------------------------------------