def solve_maxcut_by_exhaustive_search(graph): """Brute-force solver for MAXCUT instances using exhaustive search. Args: graph (networkx.Graph): undirected weighted graph describing the MAXCUT instance. Returns: tuple: tuple whose first elements is the number of cuts, and second is a list of bit strings that correspond to the solution(s). """ solution_set = [] num_nodes = len(graph.nodes) # find one MAXCUT solution maxcut = -1 one_maxcut_solution = None for i in range(0, 2**num_nodes): trial_solution = dec2bin(i, num_nodes) current_cut = get_solution_cut_size(trial_solution, graph) if current_cut > maxcut: one_maxcut_solution = trial_solution maxcut = current_cut solution_set.append(one_maxcut_solution) # search again to pick up any degeneracies for i in range(0, 2**num_nodes): trial_solution = dec2bin(i, num_nodes) current_cut = get_solution_cut_size(trial_solution, graph) if current_cut == maxcut and trial_solution != one_maxcut_solution: solution_set.append(trial_solution) return maxcut, solution_set
def get_thermal_sampled_distribution( n_samples: int, n_spins: int, temperature: float, hamiltonian_parameters: typing.Tuple[np.ndarray, np.ndarray], ) -> BitstringDistribution: """Generates thermal states sample distribution Args: n_samples: the number of samples from the original distribution n_spins: number of spins in the Ising system temperature: temperature factor in the Boltzmann distribution Returns: histogram_samples: keys are binary string representations and values are corresponding probabilities. """ distribution = get_thermal_target_bitstring_distribution( n_spins, temperature, hamiltonian_parameters ).distribution_dict sample_distribution_dict = sample_from_probability_distribution( distribution, n_samples ) histogram_samples = np.zeros(2 ** n_spins) for samples, counts in sample_distribution_dict.items(): integer_list: typing.List[int] = [] for elem in samples: integer_list.append(int(elem)) idx = convert_ising_bitstring_to_integer(integer_list) histogram_samples[idx] += counts / n_samples for spin in range(int(2 ** n_spins)): binary_bitstring = convert_tuples_to_bitstrings([dec2bin(spin, n_spins)])[0] sample_distribution_dict[binary_bitstring] = histogram_samples[spin] return BitstringDistribution(sample_distribution_dict)
def _solve_bitstring_problem_by_exhaustive_search( cost_function: Callable[[Tuple[int]], float], num_nodes: int, ) -> Tuple[float, List[Tuple[int]]]: """ Solves given cost function of a graph problem using exhaustive search. It searches for degeneracy and returns all the best solutions if more than one exists. Args: cost_function: function which calculates the cost of solution of a given problem. Callable that takes a bitstring solution and outputs cost value. num_nodes: number of nodes of the graph for which we want to solve the problem Returns: float: value of the best solution List[Tuple[int]]: list of solutions which correspond to the best value, each solution is a tuple of ints. """ solutions_list = [] best_value = np.inf for i in range(2**num_nodes): trial_solution: Tuple[int] = tuple(dec2bin(i, num_nodes)) current_value = cost_function(trial_solution) if current_value == best_value: solutions_list.append(trial_solution) if current_value < best_value: best_value = current_value solutions_list = [trial_solution] return best_value, solutions_list
def test_thermal_sampled_distribution(self): # Given n_samples = 5000 n_spins = 2 temperature = 0.85 np.random.seed(SEED) # needed by our samplers random.seed(SEED) # needed to make lea reproducible external_fields = np.random.rand(n_spins) two_body_couplings = np.random.rand(n_spins, n_spins) hamiltonian_parameters = [external_fields, two_body_couplings] expected_bitstrings = convert_tuples_to_bitstrings( [dec2bin(number, n_spins) for number in range(int(2 ** n_spins))] ) expected_keys = expected_bitstrings[len(expected_bitstrings) :: -1] # When sample_distribution = get_thermal_sampled_distribution( n_samples, n_spins, temperature, hamiltonian_parameters ) # Then self.assertListEqual( sorted(list(sample_distribution.distribution_dict.keys())), sorted(expected_keys), ) self.assertAlmostEqual( sum(list(sample_distribution.distribution_dict.values())), 1 )
def test_get_thermal_target_distribution_dict(self): # Given n_spins = 5 temperature = 1.0 expected_bitstrings = convert_tuples_to_bitstrings( [dec2bin(number, n_spins) for number in range(int(2 ** n_spins))] ) expected_keys = expected_bitstrings[len(expected_bitstrings) :: -1] np.random.seed(SEED) external_fields = np.random.rand(n_spins) two_body_couplings = np.random.rand(n_spins, n_spins) hamiltonian_parameters = [external_fields, two_body_couplings] # When target_distribution = get_thermal_target_bitstring_distribution( n_spins, temperature, hamiltonian_parameters ) # Then self.assertListEqual( sorted(list(target_distribution.distribution_dict.keys())), sorted(expected_keys), ) self.assertAlmostEqual( sum(list(target_distribution.distribution_dict.values())), 1.0, )
def _calculate_expectation_value_for_wavefunction(wavefunction: Wavefunction, operator: IsingOperator, alpha: float) -> float: expectation_values_per_bitstring = {} probability_per_bitstring = {} n_qubits = wavefunction.amplitudes.shape[0].bit_length() - 1 for decimal_bitstring in range(2**n_qubits): # `decimal_bitstring` is the bitstring converted to decimal. # Convert decimal bitstring into bitstring bitstring = "".join( [str(int) for int in dec2bin(decimal_bitstring, n_qubits)]) # Calculate expectation values for each bitstring. expected_value = _calculate_expectation_value_of_bitstring( bitstring, operator) expectation_values_per_bitstring[bitstring] = expected_value # Compute the probability p(x) for each n-bitstring x from the wavefunction, # p(x) = |amplitude of x| ^ 2. probability = np.abs(wavefunction.amplitudes[decimal_bitstring])**2 probability_per_bitstring[bitstring] = float(probability) return _sum_expectation_values(expectation_values_per_bitstring, probability_per_bitstring, alpha)
def f(j): # Function which computes the index of the nonzero # element in P for a given column j j_str = dec2bin(j, n) for index in range(0, n): if label_vec[index] in [1, 2]: # flip if X or Y j_str[index] = int(not j_str[index]) return bin2dec(j_str)
def convert_integer_to_ising_bitstring(number: int, length: int) -> typing.List[int]: """Converts an integer into a +/-1s bitstring (also called Ising bitstring). Args: number: positive number to be converted into its corresponding Ising bitstring representation length: length of the Ising bitstring (i.e. positive number of spins in the Ising system) Returns: Ising bitstring representation (1D array of +/-1).""" if length < 0: raise ValueError("Length cannot be negative.") binary_bitstring = dec2bin(number, length) ising_bitstring = [bit * 2 - 1 for bit in binary_bitstring] return ising_bitstring
def nz(j): # Function which computes the value of the nonzero # element in P on the column j val_nz = 1.0 j_str = dec2bin(j, n) for index in range(0, n): if label_vec[index] == 2: if j_str[index] == 0: val_nz = val_nz * (1j) if j_str[index] == 1: val_nz = val_nz * (-1j) if label_vec[index] == 3: if j_str[index] == 1: val_nz = val_nz * (-1) return val_nz
def get_thermal_target_bitstring_distribution( n_spins: int, temperature: float, hamiltonian_parameters: typing.Tuple[np.ndarray, np.ndarray], ) -> BitstringDistribution: """Generates thermal states target distribution, saved in a dict where keys are bitstrings and values are corresponding probabilities according to the Boltzmann Distribution formula. Args: n_spins: positive number of spins in the Ising system temperature: temperature factor in the boltzman distribution hamiltonian_parameters: values of hamiltonian parameters, namely external fields and two body couplings. Returns: Thermal target distribution. Number of positive spins in the spin state. """ partition_function = 0 external_fields, two_body_couplings = hamiltonian_parameters beta = 1.0 / temperature distribution = {} for spin in range(int(2 ** n_spins)): ising_bitstring = convert_integer_to_ising_bitstring(spin, n_spins) energy = 0 for i in range(n_spins): energy -= ising_bitstring[i] * external_fields[i] if i != n_spins - 1: energy -= ( ising_bitstring[i] * ising_bitstring[i + 1] * two_body_couplings[i, i + 1] ) boltzmann_factor = np.exp(energy * beta) partition_function += boltzmann_factor binary_bitstring = convert_tuples_to_bitstrings([dec2bin(spin, n_spins)])[0] distribution[binary_bitstring] = boltzmann_factor normalized_distribution = { key: value / partition_function for key, value in distribution.items() } return BitstringDistribution(normalized_distribution)
def get_qubitop_from_matrix(operator: List[List]) -> QubitOperator: r"""Expands a 2^n by 2^n matrix into n-qubit Pauli basis. The runtime of this function is O(2^2n). Args: operator: a list of lists (rows) representing a 2^n by 2^n matrix. Returns: A QubitOperator instance corresponding to the expansion of the input operator as a sum of Pauli strings: O = 2^-n \sum_P tr(O*P) P """ nrows = len(operator) ncols = len(operator[0]) # Check if the input operator is square if nrows != ncols: raise Exception('The input operator is not square') # Check if the dimensions are powers of 2 if not (((nrows & (nrows - 1)) == 0) and nrows > 0): raise Exception('The number of rows is not a power of 2') if not (((ncols & (ncols - 1)) == 0) and ncols > 0): raise Exception('The number of cols is not a power of 2') n = int(np.log2(nrows)) # number of qubits def decode(bit_string): # Helper function for converting any 2n-bit # string to a label vector representing a Pauli # string of length n if len(bit_string) != 2 * n: raise Exception('LH_expand:decode: input bit string length not 2n') output_label = list(np.zeros(n)) for i in range(0, n): output_label[i] = bin2dec(bit_string[2 * i:2 * i + 2]) return output_label def trace_product(label_vec): # Helper function for computing tr(OP) # where O is the input operator and P is a # Pauli string operator def f(j): # Function which computes the index of the nonzero # element in P for a given column j j_str = dec2bin(j, n) for index in range(0, n): if label_vec[index] in [1, 2]: # flip if X or Y j_str[index] = int(not j_str[index]) return bin2dec(j_str) def nz(j): # Function which computes the value of the nonzero # element in P on the column j val_nz = 1.0 j_str = dec2bin(j, n) for index in range(0, n): if label_vec[index] == 2: if j_str[index] == 0: val_nz = val_nz * (1j) if j_str[index] == 1: val_nz = val_nz * (-1j) if label_vec[index] == 3: if j_str[index] == 1: val_nz = val_nz * (-1) return val_nz # Compute the trace tr = 0.0 for j in range(0, 2**n): # loop over the columns tr = tr + operator[j][f(j)] * nz(j) return tr / 2**n # Expand the operator in Pauli basis coeffs = list(np.zeros(4**n)) labels = list(np.zeros(4**n)) for i in range(0, 4**n): # loop over all 2n-bit strings current_string = dec2bin(i, 2 * n) # see util.py current_label = decode(current_string) coeffs[i] = trace_product(current_label) labels[i] = current_label return get_qubitop_from_coeffs_and_labels(coeffs, labels)
def test_dec_bin_conversion(self): integer = random.randint(1, 10 ** 9) integer2 = bin2dec(dec2bin(integer, 30)) assert integer == integer2
def test_ordered_bitstring(num_qubits): bitstrings = get_ordered_list_of_bitstrings(num_qubits) expected_bitstrings = convert_tuples_to_bitstrings( [dec2bin(integer, num_qubits) for integer in range(2**num_qubits)]) assert np.all(expected_bitstrings == bitstrings) assert np.all([len(bitstring) == num_qubits for bitstring in bitstrings])