import numpy as np
import math
import copy
import graycode
import csv
from itertools import count
from qiskit.circuit import Parameter
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime.fake_provider import FakeAuckland
from qiskit_aer.primitives import SamplerV2
import random
import json
import torch
from typing import Callable # Import Callable for type hinting
from pathlib import Path
from modules.config import (NETWORK_DIR,
DATA_SOURCES,
PRINT_FREQUENCY,
)
from classes.LRUCacheUnhashable import LRUCacheUnhashable
[docs]
def load_dict_from_json(filename: str) -> dict:
"""Loads a dictionary from a JSON file"""
with open(filename, 'r') as f:
return json.load(f)
[docs]
def read_index(filename: str, encoding: str) -> dict:
"""Reads CSV file and returns a dictionary
Parameters
----------
filename : str
The filename of the CSV file.
encoding : str
The expected coding. If this is missed
get odd charactors at start of the file
Returns
-------
dict : dict
A dictionary with the contents on the CSV file
"""
dict = {}
index = count()
with open( filename, 'r', encoding=encoding) as csv_file:
csv_reader = csv.DictReader(csv_file)
for row in csv_reader:
dict[next(index)] = row
return(dict)
[docs]
def read_file_name(locations: int,
data_sources: dict,
file_type:str='file'
) -> str:
"""Find the filename for a certain number of locations
Parameters
----------
locations : int
Number of locations, or vertices
data_sources : dict
Dictionary listing the filename for each problem size
source : str
Source of data - only sim has
Returns
----------
filename : string
The filename for that problem size
"""
if file_type == 'file':
filename = data_sources[locations]['file']
print(f'Reading distance data')
elif file_type == 'points':
filename = data_sources[locations]['points']
print(f'Reading co-ordinate data')
else:
raise Exception(f'File type {file_type} is not coded for')
return(filename)
[docs]
def validate_distance_array(array :np.ndarray, locs: int):
"""Validates the distance array and raises an Exception if the
array is not valid. Checks the array is the correct shape, and
is symmetric
Parameters
----------
array : array
Numpy symmetric array with distances
locs : int
Number of locations or vertices
"""
if len(np.shape(array)) !=2:
raise Exception('The distance array is not two dimensional')
for index in [0,1]:
if np.shape(array)[index] != locs:
raise Exception(f'The shape of the array does not match {locs} locations')
#check symmetry
for i in range(locs):
for j in range(locs):
if array[i,j] != array[j,i]:
raise Exception('The array is not symmetrical')
[docs]
def find_distance(loc1: int,
loc2: int,
distance_array: np.ndarray,
verbose: bool=False,
) -> float:
"""Finds the distance between locations using the distance matrix
Parameters
----------
loc1 : int
First location
loc2 : int
Second location
distance_array : np.ndarray
An array containing the distances between locations
verbose : bool
If True then more information is printed
Returns
----------
distance : Float
The distance between two locations
"""
distance = distance_array[loc1][loc2]
if verbose:
print(f'The distance from location {loc1} to location {loc2} is {distance}')
return(distance)
[docs]
def find_bin_length(i: int) -> int:
"""find the length of a binary string to represent integer i"""
if i <= 0:
raise ValueError("n must be a positive integer")
bin_len = math.ceil((math.log2(i)))
return(bin_len)
[docs]
def find_problem_size(sdl) -> int:
"""Finds the number of binary variables needed
Parameters
----------
sdl: sub data logger containing fields
sdl.locations : int
Number of locations
sdl.formulation:
'original' => method from Goldsmith D, Day-Evans J.
'new' => method from Schnaus M, Palackal L, Poggel B, Runge X, Ehm H, Lorenz JM, et al.
Returns
----------
pb_dim : int
Length of the bit string needed to store the problem
"""
if sdl.formulation == 'original':
pb_dim = 0
for i in range(1, sdl.locations):
bin_len = find_bin_length(i)
pb_dim += bin_len
elif sdl.formulation == 'new':
f = math.factorial(sdl.locations)
pb_dim = find_bin_length(f)
else:
raise Exception(f'Unknown method {sdl.formulation}')
return(pb_dim)
[docs]
def convert_binary_list_to_integer(binary_list: list, gray:bool=False)->int:
"""Converts list of binary numbers to an integer
Parameters
----------
binary_list : list
List of binary numbers
gray : bool
Determines whether gray code is used.
Returns
----------
result : int
The integer represented by the concatenated binary string
"""
string = ''
for item in binary_list:
string += str(item)
result= int(string, base=2)
if gray:
result = graycode.gray_code_to_tc(result)
return(result)
[docs]
def convert_integer_to_binary_list(integer: int, length: int, gray:bool=False)->list:
"""Converts an integer to a list of binary numbers
Parameters
----------
integer : int
The integer to be converted
length : int
The length of the binary string
gray : bool
If True Gray codes are used
Returns
----------
result : list
A list of binary numbers
"""
if gray:
integer = graycode.tc_to_gray_code(integer)
binary_string = bin(integer)
binary_string = binary_string_format(binary_string, length)
result = [int(i) for i in binary_string]
return(result)
[docs]
def check_loc_list(loc_list:list, locs:int) -> bool:
"""Checks that the location list is a valid cycle with no repetition of nodes.
Parameters
----------
loc_list : list
A list of locations with a length one less than the number of locations in the problem
locs : int
The total number of locations in the problem
Returns
----------
valid : Boolean
Whether the loc_list is a valid cycle
"""
valid = True
for i in range(len(loc_list)):
if loc_list[i] > (locs - 1):
valid = False
for i in range(0, locs-1):
for j in range(0, locs-1):
if i != j:
if loc_list[i] == loc_list[j]:
valid = False
return(valid)
[docs]
def augment_loc_list(loc_list:list, locs:int)-> list:
"""Completes the cycle by adding the missing location to the end of the cycle.
Parameters
----------
loc_list : list
A list of locations with a length one less than the number of locations in the problem
locs : int
The total number of locations in the problem
Returns
----------
valid : loc_list
The original location list with the missing node list added to the end of the cycle
"""
full_list = [i for i in range(0,locs)]
for item in full_list:
if item not in loc_list:
add_item = item
loc_list.append(add_item)
return(loc_list)
[docs]
def find_total_distance(int_list: list,
locs: int,
distance_array :np.ndarray
)-> float:
"""Finds the total distance for a valid formatted bit string representing a cycle.
Parameters
----------
int_list : list of integers
A list representing a valid cycle
locs: int
The number of locations
distance_array : array
Numpy symmetric array with distances between locations
Returns
-------
total_distance : float
The total distance for the cycle represented by that integer list
"""
if len(int_list) != locs:
raise Exception(f'The list supplied has {len(int_list)} entries and {locs} are expected')
total_distance = 0
for i in range(0, locs):
if i < locs-1:
j = i + 1
elif i == locs - 1:
#complete cycle
j = 0
else:
raise Exception('Unexpected values of i in loop')
distance = find_distance(int_list[i], int_list[j], distance_array)
total_distance += distance
return total_distance
[docs]
def cost_fn_fact(sdl, distance_array: np.ndarray, ) -> Callable[[list], int]:
""" Returns a cost function inside a decorator,
Parameters
----------
sdl: SubDataLogger object containing key parameters:
sdl.locations: int
The number of locations in the problem
sdl.gray: bool
If True Gray codes are used
sdl.formulation: str
'original' => method from Goldsmith D, Day-Evans J.
'new' => method from Schnaus M, Palackal L, Poggel B, Runge X, Ehm H, Lorenz JM, et al.
distance_array: array
Numpy symmetric array with distances between locations
Returns
-------
cost_fn: cost function
A function of a bit string evaluating a distance for that bit string
"""
@LRUCacheUnhashable
def cost_fn(bit_string_input: list) -> float:
"""Returns the value of the objective function for a bit_string"""
if isinstance(bit_string_input, list):
bit_string = bit_string_input
full_list_of_locs = convert_bit_string_to_cycle(bit_string,
sdl.locations,
sdl.gray,
sdl.formulation
)
total_distance = find_total_distance(full_list_of_locs,
sdl.locations,
distance_array
)
valid = check_loc_list(full_list_of_locs,
sdl.locations
)
if not valid:
raise Exception('Algorithm returned incorrect cycle')
return total_distance
else:
raise Exception(f'bit_string {bit_string_input} is not a list or a tensor')
return cost_fn
[docs]
def cost_fn_tensor(input: torch.tensor,
cost_fn: Callable)-> torch.Tensor:
""" Find the distance for each bit string input using cost_fn
Parameters
----------
input : torch.tensor
Torch array with n bit strings for analysis
cost_fn : function
maps a bit_list to a distance
Returns
-------
distance_tensor : torch.tensor
a Torch array with one distance entry for each input
"""
if isinstance(input, torch.Tensor):
if input.dim() != 2:
raise Exception(f'input= {input} is a Torch tensor but does not have dimension 2')
rows = input.size(0)
distance_tensor = torch.zeros(rows)
for i in range(rows):
row = input[i]
bit_string = row.int().tolist()
distance = cost_fn(bit_string)
distance_tensor[i] = distance
return distance_tensor
else:
raise Exception(f'bit_string {input} is not a tensor')
[docs]
def convert_bit_string_to_cycle(bit_string: list,
locs: int,
gray: bool=False,
method: str='original') -> list:
"""Converts a bit string to a cycle.
Parameters
----------
bit_string : list
A list of zeros and ones produced by the quantum computer
locs: int
The number of locations
gray: bool
If True Gray codes are used
method: str
'original' => method from Goldsmith D, Day-Evans J.
'new' => method from Schnaus M, Palackal L, Poggel B, Runge X, Ehm H, Lorenz JM, et al.
Returns
----------
end_cycle_list : list
A list of integers showing a cycle. The bit string is processed from left to right
"""
end_cycle_list = []
start_cycle_list = [i for i in range(locs)]
if method == 'original':
#need to avoid changing the original bit_string
bit_string_copy = copy.deepcopy(bit_string)
end_cycle_list.append(start_cycle_list.pop(0)) #end point of cycle is always 0
for i in range(locs-1, 1, -1):
bin_len = find_bin_length(i)
bin_string = []
for count in range(bin_len):
bin_string.append(bit_string_copy.pop(0)) #pop the most left hand item
position = convert_binary_list_to_integer(bin_string, gray=gray)
index = position % i
end_cycle_list.append(start_cycle_list.pop(index))
end_cycle_list.append(start_cycle_list.pop(0))
if start_cycle_list != []:
raise Exception('Cycle returned may not be complete')
if bit_string_copy != []:
raise Exception(f'bit_string not consumed {bit_string_copy} left')
return(end_cycle_list)
elif method == 'new':
f = math.factorial(locs)
bit_string_length = math.ceil(math.log2(f))
if len(bit_string) != bit_string_length:
raise Exception(f'bit_string length {len(bit_string)} does not match {bit_string_length}')
x = convert_binary_list_to_integer(bit_string, gray=gray)
y = x % f
i = 0
while i < locs:
f = int(f / (locs - i))
#correcting mistype in paper
k = math.floor(y / f)
end_cycle_list.append(start_cycle_list[k])
start_cycle_list.remove(start_cycle_list[k])
#correcting mistype in paper
y -= k * f
i += 1
return end_cycle_list
else:
raise Exception(f'Unknown method {method}')
[docs]
def find_stats(cost_fn: Callable,
counts: dict,
shots: int,
average_slice: float=1,
)-> tuple[float, float, list[int]]:
"""Finds the average energy of the relevant counts, and the lowest energy
Parameters
----------
cost_fn: function
A function of a bit string evaluating an energy(distance) for that bit string
counts : dict
Dictionary holding the binary string and the counts observed
shots: integer
number of shots
average_slice: float
average over this slice of the energy. eg
If average_slice = 1 then average over all energies.
If average_slice = 0.2 then average over the bottom 20% of energies
Returns
----------
average : float
The average energy
lowest_dist : float
The lowest energy
lowest_energy_bit_string: list
A list of the bits for the lowest energy bit string
"""
if average_slice > 1:
raise Exception(f'The average_slice must be less or equal to 1, not {average_slice}')
elif average_slice <=0:
raise Exception(f'The average_slice must be greater than zero, not {average_slice}')
elif average_slice == 1:
slicing = False
else:
slicing = True
total_counts, total_energy = 0, 0
first = True
if slicing:
energy_dict = {}
for key, count in counts.items():
bit_list = [int(bits) for bits in key]
energy = cost_fn(bit_list)
if slicing:
#if already in dictionary increment
if energy in energy_dict.keys():
energy_dict[energy] +=count
else:
energy_dict[energy] =count
if first == True:
lowest_energy = energy
first = False
lowest_energy_bit_string = bit_list
else:
if energy < lowest_energy:
lowest_energy = energy
lowest_energy_bit_string = bit_list
total_counts += count
total_energy += energy * count
if shots != total_counts:
raise Exception(f'The total_counts {total_counts=} does not agree to the {shots=}')
if slicing:
accum, total_energy, total_counts = 0, 0, 0
stop = shots * average_slice
sorted_energy_dict = dict(sorted(energy_dict.items()))
for energy, count in sorted_energy_dict.items():
if accum < stop:
if accum + count < stop:
total_energy += energy * count
total_counts += count
else:
total_counts += stop - accum
total_energy += energy * (stop - accum)
accum += count
average_energy = total_energy / total_counts
return(average_energy, lowest_energy, lowest_energy_bit_string)
[docs]
def hot_start(sdl, distance_array: np.ndarray) -> list:
"""Finds a route from a distance array where the distance to the next point is the shortest available
Parameters
----------
sdl : SubDataLogger object containing key parameters:
sdl.locations: int
The number of locations in the problem
distance_array: array
Numpy symmetric array with distances between locations
Returns
-------
end_cycle_list: list
A list of integers showing the an estimate of the lowest cycle
"""
validate_distance_array(distance_array, sdl.locations)
remaining_cycle_list = [i for i in range(sdl.locations)]
end_cycle_list = []
end_cycle_list.append(remaining_cycle_list.pop(0)) #start point of cycle is always 0
next_row = 0
for i in range(sdl.locations-1):
for j, column in enumerate(remaining_cycle_list):
distance = distance_array[next_row][column]
if j == 0:
arg_min = j
lowest_distance = distance
else:
if distance < lowest_distance:
arg_min = j
lowest_distance = distance
next_row = remaining_cycle_list.pop(arg_min)
end_cycle_list.append(next_row)
return(end_cycle_list)
[docs]
def hot_start_list_to_string(sdl, hot_start_list: list) -> list:
"""Invert the hot start integer list into a string
Parameters:
-----------
sdl : SubDataLogger object containing key parameters:
sdl.locations: int
The number of location in the problem
sdl.gray: bool
If True Gray codes are used
sdl.formulation: str
'original' => method from Goldsmith D, Day-Evans J.
'new' => method from Schnaus M, Palackal L, Poggel B, Runge X, Ehm H, Lorenz JM, et al.
hot_start_list: list
A list of integers showing an estimate of the lowest cycle
Returns
-------
result_list: list
A list of bits that represents the bit string for the lowest cycle
"""
if sdl.formulation == 'original':
if len(hot_start_list) != sdl.locations:
raise Exception(f'The hot start list should be length {sdl.locations}')
first_item = hot_start_list.pop(0)
#remove the first item for the list which should be zero
if first_item != 0:
raise Exception(f'The first item of the list must be zero')
initial_list = [i for i in range(1, sdl.locations)]
total_binary_string = ''
result_list = []
for i, integer in enumerate(hot_start_list):
bin_len = find_bin_length(len(initial_list))
if bin_len > 0:
#find the index of integer in hot start list
index = initial_list.index(integer)
if sdl.gray:
binary_string = bin(graycode.tc_to_gray_code(index))
else:
binary_string = bin(index)
binary_string = binary_string_format(binary_string, bin_len)
total_binary_string += binary_string
initial_list.pop(index)
for i in range(len(total_binary_string)):
result_list.append(int(total_binary_string[i]))
return(result_list)
elif sdl.formulation == 'new':
dim = find_problem_size(sdl)
f = math.factorial(sdl.locations)
y = 0
i = 0
start_cycle_list = [i for i in range(sdl.locations)]
while i < sdl.locations:
f = int(f / (sdl.locations - i))
m = hot_start_list[i]
j = start_cycle_list.index(m)
start_cycle_list.remove(m)
y += j * f
i += 1
result_list = convert_integer_to_binary_list(y, dim, gray=sdl.gray)
return result_list
else:
raise Exception(f'Unknown method {sdl.formulation}')
[docs]
def validate_gradient_type(gradient_type):
"""Check that the gradient type is valid"""
allowed_types = ['parameter_shift', 'SPSA']
if gradient_type not in allowed_types:
raise Exception (f'Gradient type {gradient_type} is not coded for')
[docs]
def update_parameters_using_gradient(sdl,
params: list,
rots: np.ndarray,
cost_fn: Callable,
qc: QuantumCircuit,
print_results: str=False,
):
"""Updates parameters using SPSA or parameter shift gradients"""
cost_list, lowest_list, index_list, gradient_list = [], [], [], []
parameter_list, average_list = [], []
validate_gradient_type(sdl.gradient_type)
if sdl.gradient_type == 'SPSA':
#define_parameters
# A is <= 10% of the number of iterations normally, but here the number of iterations is lower.
# order of magnitude of first gradients
abs_gradient = np.abs(my_gradient(sdl,
cost_fn,
qc,
params,
rots,
average_slice=sdl.slice,
)
)
magnitude_g0 = abs_gradient.mean()
if magnitude_g0 == 0:
# stop div by zero error
a = 999
else:
a = sdl.eta*((sdl.big_a+1)**sdl.alpha)/magnitude_g0
for i in range(0, sdl.iterations):
if detect_quantum_GPU_support:
bc = qc.assign_parameters({params: rot for params, rot in zip(params, rots)})
else:
bc = bind_weights(params, rots, qc)
cost, lowest, lowest_energy_bit_string = cost_func_evaluate(sdl,
cost_fn,
bc,
average_slice= sdl.slice,
)
#cost is the top-sliced energy
average, _ , _ = cost_func_evaluate(sdl,
cost_fn,
bc,
average_slice=1,
)
#average is the average energy with no top slicing
if i == 0:
lowest_string_to_date = lowest_energy_bit_string
lowest_to_date = lowest
else:
if lowest < lowest_to_date:
lowest_to_date = lowest
lowest_string_to_date = lowest_energy_bit_string
route_list = convert_bit_string_to_cycle(lowest_string_to_date,
sdl.locations,
sdl.gray,
sdl.formulation,
)
index_list.append(i)
cost_list.append(cost)
lowest_list.append(lowest_to_date)
average_list.append(average)
parameter_list.append(rots)
if sdl.gradient_type == 'parameter_shift':
gradient = my_gradient(sdl,
cost_fn,
qc,
params,
rots,
average_slice=sdl.slice,
)
rots = rots - sdl.eta * gradient
elif sdl.gradient_type == 'SPSA':
ak = a/((i+1+sdl.big_a)**(sdl.alpha))
ck = sdl.c/((i+1)**(sdl.gamma))
gradient = my_gradient(sdl,
cost_fn,
qc,
params,
rots,
average_slice=sdl.slice,
ck=ck,
)
rots = rots - ak * gradient
else:
raise Exception(f'Error found when calculating gradient. {sdl.gradient_type} is not an allowed gradient type')
gradient_list.append(gradient.tolist())
if print_results:
if i % PRINT_FREQUENCY == 0:
print(f'For iteration {i} using the best {sdl.average_slice*100} percent of the results')
print(f'The average cost from the sample is {average:.3f} and the top-sliced average of the best results is {cost:.3f}')
print(f'The lowest cost from the sample is {lowest:.3f}')
print(f'The lowest cost to date is {lowest_to_date:.3f} corresponding to bit string {lowest_string_to_date} ')
print(f'and route {route_list}')
return index_list, cost_list, lowest_list, gradient_list, average_list, parameter_list
[docs]
def cost_func_evaluate(sdl,
cost_fn: Callable,
model,
average_slice: float=1,
) -> tuple[float, float, list[int]]:
"""Evaluate cost function on a quantum computer
Parameters
----------
sdl: SubDataLogger object containing key parameters:
sdl.noise: bool
If True a noisy quantum computer is used
sdl.quantum: bool
If True a quantum computer is used. If False a classical model is used
sdl.shots: int
The number of shots for which the quantum circuit is to be run
cost_fn: function
A function of a bit string evaluating a distance for that bit string
model : a model to evalulate an output bit string given weights eg
QuantumCircuit
A quantum circuit with bound weights for which the energy is to be found
Classical Model
A classical model with bound weights for which the energy is to be found
average_slice: float
average over this slice of the energy. For example:
If average_slice = 1 then average over all energies.
If average_slice = 0.2 then average over the bottom 20% of energies
Returns
-------
cost: float
The average cost evaluated
lowest: float
The lowest cost found
lowest_energy_bit_string: string
A list of the bits for the lowest energy bit string
"""
if sdl.quantum:
if sdl.noise:
backend = FakeAuckland()
job = backend.run(model, shots=sdl.shots)
counts = job.result().get_counts()
elif sdl.mps:
simulator = AerSimulator(method='matrix_product_state')
results = simulator.run(model).result()
counts = results.get_counts(model)
else:
if detect_quantum_GPU_support():
simulator = AerSimulator(method='statevector', device='GPU')
results = simulator.run(model).result()
counts = results.get_counts(model)
else:
sampler = SamplerV2()
job = sampler.run([model], shots=sdl.shots)
results = job.result()
counts = results[0].data.meas.get_counts()
else:
raise Exception('Classical model not yet coded for')
cost, lowest, lowest_energy_bit_string = find_stats(cost_fn, counts, sdl.shots, average_slice, )
return(cost, lowest, lowest_energy_bit_string)
[docs]
def my_gradient(sdl,
cost_fn,
qc:QuantumCircuit,
params:list,
rots:np.ndarray,
average_slice:float,
ck:float=1e-2,
) -> np.ndarray:
"""Calculate gradient for a quantum circuit with parameters and rotations
Parameters
----------
sdl: SubDataLogger object containing key parameters, for example:
sdl.noise: bool
If True a noisy quantum computer is used
sdl.shots: int
The number of shots for which the quantum circuit is to be run
sdl.s: float
Parameter shift parameter
sdl.gradient_type: str
controls the optimiser to be used.
if 'parameter shift' uses analytical expression
if 'SPSA' uses a stochastical method
sdl.ck: float
SPSA parameter, small number controlling perturbations
cost_fn: function
A function of a bit string evaluating an energy (distance) for that bit string
qc: QuantumCircuit
A quantum circuit for which the gradient is to be found, without the weights being bound
params: list
A list of parameters (the texts)
rots: list
The exact values for the parameters, which are rotations of quantum gates
average_slice: float
Controls the amount of data to be included in the average.
For example, 0.2 means that the lowest 20% of distances found is included in the average.
Returns
-------
gradient:array
The gradient for each parameter
"""
new_rots = copy.deepcopy(rots)
if sdl.gradient_type == 'parameter_shift':
gradient_list = []
for i, theta in enumerate(rots):
new_rots[i] = theta + np.pi/(4*sdl.s)
bc = bind_weights(params, new_rots, qc)
cost_plus, _, _ = cost_func_evaluate(sdl,
cost_fn,
bc,
average_slice,
)
new_rots[i] = theta - np.pi/(4*sdl.s)
bc = bind_weights(params, new_rots, qc)
cost_minus, _, _ = cost_func_evaluate(sdl,
cost_fn,
bc,
average_slice,
)
delta = sdl.s * (cost_plus - cost_minus)
gradient_list.append(delta)
gradient_array = np.array(gradient_list)
elif sdl.gradient_type == 'SPSA':
# number of parameters
length = len(rots)
# bernoulli-like distribution
deltak = np.random.choice([-1, 1], size=length)
# simultaneous perturbations
ck_deltak = ck * deltak
new_rots = rots + ck_deltak
# gradient approximation]
bc = bind_weights(params, new_rots, qc)
cost_plus, _, _ = cost_func_evaluate(sdl,
cost_fn,
bc,
average_slice,
)
new_rots = rots - ck_deltak
bc = bind_weights(params, new_rots, qc)
cost_minus, _, _ = cost_func_evaluate(sdl,
cost_fn,
bc,
average_slice,
)
delta = cost_plus - cost_minus
gradient_array = delta / (2 * ck_deltak)
#need to return an array to match parameter shift
else:
raise Exception(f'Gradient type {sdl.gradient_type} is not an allowed choice')
return gradient_array
[docs]
def define_parameters(sdl) -> list:
"""Set up parameters and initialise text
Parameters
----------
sdl: MySubDataLogger
A sub data logger holding the parameters for the run with key fields:
sdl.qubits: int - The number of qubits in the circuit
sdl.mode: int - Controls setting the circuit up in different modes
Returns
-------
params: list
A list of parameters (the texts)
"""
params = []
if sdl.mode in [1, 2, 3, 4, 6,]:
for i in range(sdl.num_params):
text = "param " + str(i)
params.append(Parameter(text))
return params
else:
raise Exception(f'Mode {sdl.mode} has not been coded for')
[docs]
def vqc_circuit(sdl, params: list) -> QuantumCircuit:
"""Set up a variational quantum circuit
Parameters
----------
sdl: MySubDataLogger
A sub data logger holding the parameters for the run with key fields:
sdl.qubits: int - The number of qubits in the circuit
sdl.mode: int - Controls setting the circuit up in different modes
sdl.noise: bool- Controls if noise is included in the circuit
params: list
A list of parameters (the texts)
Returns
-------
qc: Quantum Circuit
A quantum circuit without bound weights
"""
qc = QuantumCircuit(sdl.qubits)
if sdl.mode == 1:
for layer in range(sdl.layers):
offset = layer * sdl.qubits * 2
for i in range(sdl.qubits):
qc.h(i)
qc.ry(params[i+offset], i)
qc.rx(params[sdl.qubits+i+offset], i)
for i in range(sdl.qubits):
if i < sdl.qubits-1:
qc.cx(i,i+1)
else:
qc.cx(i,0)
elif sdl.mode == 2:
for layer in range(sdl.layers):
offset = layer * sdl.qubits * 2
for i in range(sdl.qubits):
qc.rx(params[i+offset], i)
for i in range(sdl.qubits):
if i < sdl.qubits-1:
qc.rxx(params[sdl.qubits+i+offset], i, i+1,)
else:
qc.rxx(params[sdl.qubits+i+offset], i, 0,)
elif sdl.mode == 3:
for layer in range(sdl.layers):
offset = layer * sdl.qubits * 2
for i in range(sdl.qubits):
qc.h(i)
if i < sdl.qubits-1:
qc.rzz(params[sdl.qubits+i+offset], i, i+1,)
else:
qc.rzz(params[sdl.qubits+i+offset], i, 0,)
for i in range(sdl.qubits):
qc.rz(params[i+offset], i)
qc.h(i)
elif sdl.mode == 4:
for layer in range(sdl.layers):
offset = layer * sdl.qubits
for i in range(sdl.qubits):
qc.rx(params[i+offset], i)
elif sdl.mode == 5:
#test mode
if sdl.qubits != 5:
raise Exception(f'test mode {sdl.mode} is only to be used with 5 qubits. {sdl.qubits} qubits are specified')
qc.x(1)
qc.x(3)
qc.x(4)
elif sdl.mode == 6:
for layer in range(sdl.layers):
offset = layer * sdl.qubits * 2
for i in range(sdl.qubits):
qc.h(i)
qc.ry(params[i+offset], i)
qc.rx(params[sdl.qubits+i+offset], i)
else:
raise Exception(f'Mode {sdl.mode} has not been coded for')
qc.measure_all()
if sdl.noise:
backend = FakeAuckland()
qc= transpile(qc, backend)
return qc
[docs]
def create_initial_rotations(sdl, bin_hot_start_list: list=False,)-> np.ndarray:
"""Initialise parameters with random weights
Parameters
----------
sdl : Subdata logger with fields:
sdl.qubits : int
The number of qubits in the circuit
sdl.mode : int
Controls setting the circuit up in different modes
sdl.hot_start : bool
If true hot start values are used
bin_hot_start_list : list
Binary list containing the hot start values
Returns
-------
init_rots: array
initial rotations
"""
if sdl.mode in [1, 2, 3, 6,]:
param_num = 2 * sdl.qubits * sdl.layers
elif sdl.mode == 4:
param_num = sdl.qubits * sdl.layers
else:
raise Exception(f'Mode {sdl.mode} is not yet coded')
if sdl.hot_start:
if sdl.layers in [1]:
raise Exception('Cannot use a hot start for mode {mode}')
init_rots = [0 for i in range(param_num)]
for i, item in enumerate(bin_hot_start_list):
if item == 1:
init_rots[sdl.qubits-i-1] = np.pi
#need to reverse order because of qiskit convention
else:
init_rots= [random.random() * 2 * math.pi for i in range(param_num)]
init_rots_array = np.array(init_rots)
return(init_rots_array)
from typing import Callable
[docs]
def bind_weights(params:list, rots:list, qc:QuantumCircuit) -> QuantumCircuit:
"""Bind parameters to rotations and return a bound quantum circuit
Parameters
----------
params: list
A list of parameters (the texts)
rots: list
The exact values for the parameters, which are rotations of quantum gates
qc: Quantum Circuit
A quantum circuit without bound weights
Returns
-------
bc: Quantum Circuit
A quantum circuit with including bound weights, ready to run an evaluation
"""
binding_dict = {}
for i, rot in enumerate(rots):
binding_dict[str(params[i])] = rot
bc = qc.assign_parameters(binding_dict)
return(bc)
[docs]
def find_run_stats(lowest_list:list)-> tuple[float, int]:
"""Finds the lowest energy and the iteration at which it was found
Parameters
----------
lowest_list: list
A list of floats showing the lowest energy found at that iteration
Returns
-------
lowest_energy: float
The lowest energy found
iteration: int
The iteration at which the lowest energy was found
"""
previous_lowest = max(lowest_list)
lowest_energy = previous_lowest
iteration = 0
for i, value in enumerate(lowest_list):
if value < previous_lowest:
lowest_energy = value
previous_lowest = value
iteration = i
return lowest_energy, iteration
[docs]
def find_distances_array(locations:int,
print_comments:bool=False)-> tuple[np.ndarray, float]:
"""Finds the array of distances between locations and the best distance"""
sources_filename = Path(NETWORK_DIR).joinpath(DATA_SOURCES)
data_source_dict = load_dict_from_json(sources_filename)
filename = read_file_name(str(locations), data_source_dict)
filepath = Path(NETWORK_DIR).joinpath(filename)
best_dist = data_source_dict[str(locations)]['best']
if print_comments:
print(f'Data will be read from filename {filepath}.')
print(f'It is known that the shortest distance is {best_dist}')
distance_array = np.genfromtxt(filepath)
validate_distance_array(distance_array, locations)
return distance_array, best_dist
[docs]
def detect_quantum_GPU_support()-> bool:
"""Detect if a GPU is available for quantum simulations"""
devices = AerSimulator().available_devices()
if 'GPU' in devices:
return True
else:
return False
[docs]
def calculate_hot_start_data(sdl,
distance_array: np.ndarray,
cost_fn: Callable,
print_results:bool=False,
)-> tuple[list, float]:
"""Calculate hot start data from a distance array
Parameters
----------
sdl : SubDataLogger object
Containing key parameters
distance_array: array
Numpy symmetric array with distances between locations
cost_fn : function
A function that returns a distance from a binary string
print_results: bool
If true prints out the hot start data
Returns
-------
hot_start_list: list
A list of integers showing an estimate of the lowest cycle
hot_start_distance: float
The distance of the hot start cycle
"""
hot_start_list = hot_start(sdl, distance_array, )
bin_hot_start_list = hot_start_list_to_string(sdl, hot_start_list)
hot_start_distance = cost_fn(bin_hot_start_list)
if print_results:
print(f'The hot start location list is {hot_start_list}')
print(f'This is equivalent to a binary list: {bin_hot_start_list}')
print(f'The hot start distance is {hot_start_distance}, compared to a best distance of {sdl.best_dist}.')
return bin_hot_start_list, hot_start_distance