diff --git a/docs/source/conf.py b/docs/source/conf.py index 892317a6..ff32870e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -128,4 +128,4 @@ # app.connect(skip) # pygments_style = 'sphinx' -# suppress_warnings = ["myst_header"] \ No newline at end of file +# suppress_warnings = ["myst_header"] diff --git a/examples/03_qaoa_on_qpus.ipynb b/examples/03_qaoa_on_qpus.ipynb index 18f90ec3..95a05c1e 100644 --- a/examples/03_qaoa_on_qpus.ipynb +++ b/examples/03_qaoa_on_qpus.ipynb @@ -250,7 +250,7 @@ "outputs": [], "source": [ "# device. If qpu_crendetials is not specified,the default hub, group and provider is used.\n", - "qiskit_cloud = create_device(location='ibmq', name='ibm_perth', **qpu_credentials, as_emulator=True)\n", + "qiskit_cloud = create_device(location='ibmq', name='ibm_kyoto', **qpu_credentials, as_emulator=True)\n", "q_qiskit.set_device(qiskit_cloud)\n", "\n", "# circuit properties\n", diff --git a/examples/14_qaoa_benchmark.ipynb b/examples/14_qaoa_benchmark.ipynb index e81544b0..53a055f4 100644 --- a/examples/14_qaoa_benchmark.ipynb +++ b/examples/14_qaoa_benchmark.ipynb @@ -69,7 +69,7 @@ "q = QAOA()\n", "\n", "# set device and backend properties\n", - "qiskit_cloud = create_device(location='ibmq', name='ibm_perth', **qpu_credentials, as_emulator=True)\n", + "qiskit_cloud = create_device(location='ibmq', name='ibm_kyoto', **qpu_credentials, as_emulator=True)\n", "q.set_device(qiskit_cloud)\n", "q.set_backend_properties(n_shots=1000)\n", "\n", @@ -602,7 +602,7 @@ "q_ibm = QAOA()\n", "\n", "# set device and backend properties\n", - "qiskit_cloud = create_device(location='ibmq', name='ibm_perth', **qpu_credentials, as_emulator=True)\n", + "qiskit_cloud = create_device(location='ibmq', name='ibm_kyoto', **qpu_credentials, as_emulator=True)\n", "q_ibm.set_device(qiskit_cloud)\n", "q_ibm.set_backend_properties(n_shots=1000)\n", "\n", diff --git a/src/openqaoa-core/openqaoa/problems/bpsp.py b/src/openqaoa-core/openqaoa/problems/bpsp.py index 644a086f..87767bc6 100644 --- a/src/openqaoa-core/openqaoa/problems/bpsp.py +++ b/src/openqaoa-core/openqaoa/problems/bpsp.py @@ -35,28 +35,28 @@ class BPSP(Problem): """ Binary Paint Shop Problem (BPSP) - This class is dedicated to solving the Binary Paint Shop Problem (BPSP), a combinatorial - optimization challenge drawn from an automotive paint shop context. In this problem, there - exists a specific sequence of cars, with each car making two appearances in the sequence. - The primary objective is to find the best coloring sequence such that consecutive cars in the + This class is dedicated to solving the Binary Paint Shop Problem (BPSP), a combinatorial + optimization challenge drawn from an automotive paint shop context. In this problem, there + exists a specific sequence of cars, with each car making two appearances in the sequence. + The primary objective is to find the best coloring sequence such that consecutive cars in the sequence necessitate the fewest color switches. - When initializing, the solver accepts a list of car numbers indicative of a valid BPSP instance + When initializing, the solver accepts a list of car numbers indicative of a valid BPSP instance and subsequently establishes a Binary Paint Shop Problem instance based on this. To elaborate on the problem: - - Imagine we have 'n' distinct cars, labeled as c_1, c_2, ..., c_n. These cars are presented - in a sequence of length '2n', such that each car c_i makes two appearances. This sequence + + Imagine we have 'n' distinct cars, labeled as c_1, c_2, ..., c_n. These cars are presented + in a sequence of length '2n', such that each car c_i makes two appearances. This sequence is represented as w_1, w_2, ..., w_2n, with every w_i being one of the cars from the set. - The challenge further presents two paint colors, represented as 1 and 2. For every car - in the sequence, a decision must be made on which color to paint it first, resulting in a - color assignment sequence like f_1, f_2, ..., f_2n. It's crucial to ensure that if two + The challenge further presents two paint colors, represented as 1 and 2. For every car + in the sequence, a decision must be made on which color to paint it first, resulting in a + color assignment sequence like f_1, f_2, ..., f_2n. It's crucial to ensure that if two occurrences in the sequence pertain to the same car, their color designations differ. - The main goal is to choose a sequence of colors such that we minimize the instances where - consecutive cars require different paint colors. This is quantified by computing the difference + The main goal is to choose a sequence of colors such that we minimize the instances where + consecutive cars require different paint colors. This is quantified by computing the difference between color assignments for consecutive cars and aggregating this difference for the entire sequence. """ @@ -68,8 +68,8 @@ def __init__(self, car_sequence): Parameters: - car_sequence : list[int] - A list of integers representing car numbers, denoting the sequence - in which cars appear in the BPSP. Each car number appears exactly + A list of integers representing car numbers, denoting the sequence + in which cars appear in the BPSP. Each car number appears exactly twice, symbolizing the two distinct color coatings each car requires. Attributes Set: @@ -77,19 +77,19 @@ def __init__(self, car_sequence): Stores the sequence of car numbers. - self.car_positions : dict[int, tuple] - Maps each car number to a tuple of its two positions in the car_sequence. + Maps each car number to a tuple of its two positions in the car_sequence. Determined by the `car_pos` property. - self.bpsp_graph : networkx.Graph - Represents the BPSP as a graph where nodes are car positions and edges - indicate adjacent car positions in the sequence. Constructed by the + Represents the BPSP as a graph where nodes are car positions and edges + indicate adjacent car positions in the sequence. Constructed by the `bpsp_graph` method. Returns: ------- The initialized BPSP object. """ - + self.car_sequence = car_sequence self.car_positions = self.car_pos self.bpsp_graph = self.graph @@ -116,15 +116,14 @@ def convert_ndarrays(obj): else: return obj - @property def car_sequence(self): """ Getter for the 'car_sequence' property. - This method retrieves the current value of the `_car_sequence` attribute, which represents - the sequence of cars to be painted. Each car is identified by a unique integer ID, - and each ID must appear exactly twice in the sequence, indicating the two times the car + This method retrieves the current value of the `_car_sequence` attribute, which represents + the sequence of cars to be painted. Each car is identified by a unique integer ID, + and each ID must appear exactly twice in the sequence, indicating the two times the car is painted. Returns: @@ -139,7 +138,7 @@ def car_sequence(self, sequence): """ Setter for the 'car_sequence' property. - This method validates and sets a new value for the `_car_sequence` attribute. + This method validates and sets a new value for the `_car_sequence` attribute. The validation ensures: 1. Each car ID appears exactly twice in the sequence. 2. The range of car IDs is continuous (e.g., for IDs 0, 1, 2, both 0 and 2 cannot appear without 1). @@ -147,7 +146,7 @@ def car_sequence(self, sequence): Parameters: ---------- sequence : list[int] - A new sequence of car IDs to be assigned to `_car_sequence`. + A new sequence of car IDs to be assigned to `_car_sequence`. Raises: ------ @@ -171,19 +170,19 @@ def car_sequence(self, sequence): def random_instance(**kwargs): """ Generates a random instance of the BPSP problem, based on the specified number of cars. - + The function creates a list with two occurrences of each car ID, then applies the Fisher-Yates shuffle to randomize the order of the cars. The shuffled sequence of cars is then used to create a BPSP instance. - + Parameters ---------- **kwargs: num_cars: int - The number of distinct cars to be considered in the sequence. Each car will appear twice in the + The number of distinct cars to be considered in the sequence. Each car will appear twice in the final sequence. seed: int, optional The seed for the random number generator. If provided, the output will be deterministic based on this seed. - + Returns ------- BPSP @@ -214,28 +213,27 @@ def random_instance(**kwargs): # Return a BPSP instance using the shuffled car_sequence. return BPSP(car_sequence) - @property def car_pos(self): """ Retrieve the positions of each car ID in the car sequence. - + This method tracks the occurrences of each car ID in the car sequence. - Since each car ID is expected to appear twice in the sequence, the method - returns a dictionary where keys are car IDs and values are tuples with the + Since each car ID is expected to appear twice in the sequence, the method + returns a dictionary where keys are car IDs and values are tuples with the positions of the two occurrences. - + Returns ------- dict A dictionary where keys are car IDs and values are tuples with the positions - of the two occurrences. For example, for a sequence [0, 1, 0, 1], the + of the two occurrences. For example, for a sequence [0, 1, 0, 1], the output would be {0: (0, 2), 1: (1, 3)}. """ - + # Initialize an empty dictionary to store car positions. car_pos = {} - + # Enumerate through each car ID in the sequence to track its positions. for idx, car in enumerate(self.car_sequence): # Convert the car to int type (assuming it's of type np.int64 or similar) @@ -247,31 +245,30 @@ def car_pos(self): # If this is the first occurrence of the car ID, initialize a list with the position. else: car_pos[car_key] = [idx] - + # Convert the lists of positions to tuples for a consistent output format. for car_key, positions in car_pos.items(): car_pos[car_key] = tuple(positions) return car_pos - @property def graph(self): """ Construct a graph to represent the Binary Paint Shop Problem (BPSP) using the Ising model. This function takes a car sequence from the instance and translates it to an Ising graph. - In the graph, nodes represent cars, and edges represent the interaction between two consecutive + In the graph, nodes represent cars, and edges represent the interaction between two consecutive cars in the sequence. The weight on each edge indicates the interaction strength and sign. - + Returns ------- ising_graph : nx.Graph A graph representing the Ising model for the BPSP based on the car sequence of the instance. - + Notes ----- - The interaction strength is determined based on how many times the cars appeared + The interaction strength is determined based on how many times the cars appeared in the sequence before the current pair. """ @@ -287,21 +284,21 @@ def graph(self): # Helper function to add or update an edge in the graph. def add_edge(u, v, weight): # Sort the vertices to maintain a consistent edge representation. - edge = (u, v) #if u < v else (v, u) + edge = (u, v) # if u < v else (v, u) # Add the weight or update the existing weight of the edge. graph[edge] = graph.get(edge, 0) + weight # Process each pair of cars in the sequence. for i in range(len(self.car_sequence) - 1): # Get the current car pair. - car1, car2 = self.car_sequence[i], self.car_sequence[i+1] + car1, car2 = self.car_sequence[i], self.car_sequence[i + 1] # Get the occurrences of the cars in the sequence so far. occ_car1, occ_car2 = car_occurrences[car1], car_occurrences[car2] # Calculate the interaction weight based on car occurrences. - weight = (-1)**(occ_car1 + occ_car2 + 1) - + weight = (-1) ** (occ_car1 + occ_car2 + 1) + # Add or update the graph with the edge and its weight. add_edge(car1, car2, weight) @@ -320,7 +317,7 @@ def add_edge(u, v, weight): ising_graph.add_nodes_from(range(num_cars)) return ising_graph - + @property def docplex_bpsp_model(self): """ @@ -339,7 +336,7 @@ def docplex_bpsp_model(self): car_sequence : list A list representing the sequence of cars as they pass the paint shop. Each car is represented by its identifier (e.g., integer), and appears twice in the sequence. - + car_positions : dict A dictionary mapping each car to its two positions in the car_sequence. For example, if car 'a' appears in positions 2 and 5 in car_sequence, then @@ -350,26 +347,32 @@ def docplex_bpsp_model(self): Model A CPLEX model instance representing the BPSP, ready to be solved. """ - + mdl = Model("BPSP_Problem") sequence = self.car_sequence car_pos = self.car_positions # Dictionary for storing the paint value for car 'x' at position 'j'. - w_vars = {f"{w}_{i}": mdl.binary_var(name=f"w_{w}_{i}") for i, w in enumerate(sequence)} + w_vars = { + f"{w}_{i}": mdl.binary_var(name=f"w_{w}_{i}") + for i, w in enumerate(sequence) + } # This loop adds the constraint that a particular car cannot have the same paint - # at both occurrences in the paint sequence. If the first occurrence is 0, the other has to + # at both occurrences in the paint sequence. If the first occurrence is 0, the other has to # be 1 and vice-versa. for car, positions in car_pos.items(): w_key1, w_key2 = f"{car}_{positions[0]}", f"{car}_{positions[1]}" mdl.add_constraint(w_vars[w_key1] + w_vars[w_key2] == 1) - # Encode the objective function: sum_i^2n-1 |paint[i] - paint[i+1]|. Since docplex accepts abs operator, + # Encode the objective function: sum_i^2n-1 |paint[i] - paint[i+1]|. Since docplex accepts abs operator, # you can directly utilize it for the objective. This makes the model simpler than in Gurobi. w_keys = list(w_vars.keys()) - objective_function = mdl.sum(mdl.abs(w_vars[w_keys[i]] - w_vars[w_keys[i + 1]]) for i in range(len(w_keys) - 1)) - + objective_function = mdl.sum( + mdl.abs(w_vars[w_keys[i]] - w_vars[w_keys[i + 1]]) + for i in range(len(w_keys) - 1) + ) + # Set the objective to minimize. mdl.minimize(objective_function) @@ -381,16 +384,16 @@ def qubo(self): Returns the QUBO encoding of the Binary Paint Shop Problem. This function provides a QUBO representation of the BPSP, where the objective is to minimize the - total weight of violations. The violations here refer to two adjacent cars in the sequence receiving + total weight of violations. The violations here refer to two adjacent cars in the sequence receiving the same color. Returns ------- QUBO - The QUBO encoding of the BPSP. This is a representation that describes + The QUBO encoding of the BPSP. This is a representation that describes the quadratic objective function over binary variables. """ - + # Iterate over edges (with weight) and store accordingly terms = [] weights = [] @@ -399,40 +402,50 @@ def qubo(self): for u, v, edge_weight in self.bpsp_graph.edges(data="weight"): terms.append([u, v]) - weights.append(edge_weight) + weights.append(edge_weight) - return QUBO(self.bpsp_graph.number_of_nodes(), terms, weights, self.problem_instance,) + return QUBO( + self.bpsp_graph.number_of_nodes(), + terms, + weights, + self.problem_instance, + ) def solve_cplex(self): """ Solves the BPSP using the CPLEX solver and returns the solution and its objective value. - The Binary Paintshop Problem (BPSP) solution represents a sequence of paint choices + The Binary Paintshop Problem (BPSP) solution represents a sequence of paint choices where a value of 0 represents blue paint and a value of 1 represents red paint. Returns ------- tuple - A tuple where the first element is a list containing the paint choices + A tuple where the first element is a list containing the paint choices (0 for blue, 1 for red) and the second element is the objective value. """ - + # Retrieve the model for BPSP model = self.docplex_bpsp_model - + # Solve the BPSP using CPLEX model.solve() - + # Check the status of the solution status = model.solve_details.status if status != "integer optimal solution": print(status) - + # Extract the solution values representing paint choices - solution = [int(np.round(model.solution.get_value(var))) for var in model.iter_binary_vars()] - + solution = [ + int(np.round(model.solution.get_value(var))) + for var in model.iter_binary_vars() + ] + # Get the objective value of the solution - diff_sum = lambda lst: sum(abs(lst[i] - lst[i+1]) for i in range(len(lst)-1)) + diff_sum = lambda lst: sum( + abs(lst[i] - lst[i + 1]) for i in range(len(lst) - 1) + ) objective_value = diff_sum(solution) # Return the paint choices and their corresponding objective value return solution, objective_value @@ -440,47 +453,50 @@ def solve_cplex(self): def paintseq_from_bits(self, bitstring): """ Transforms a sequence of initial car colors to a paint sequence and computes the number of paint swaps. - - Given a bitstring sequence of car colors ('0' or '1'), this function transforms it into - a paint sequence. Each car's colors are determined by two consecutive positions in - the sequence. The function also computes the number of times the paint changes (swaps) + + Given a bitstring sequence of car colors ('0' or '1'), this function transforms it into + a paint sequence. Each car's colors are determined by two consecutive positions in + the sequence. The function also computes the number of times the paint changes (swaps) between consecutive positions in the paint sequence. - + Parameters ---------- colors : str A string of car colors where each character is either '0' or '1'. - + Returns ------- tuple - A tuple where the first element is a list representing the paint sequence + A tuple where the first element is a list representing the paint sequence (0 for blue, 1 for red) and the second element is the number of paint swaps. """ - + # Convert the input string colors to a list of integers colors = [int(color) for color in bitstring] - + # Initialize the paint sequence with zeros paint_sequence = [0 for _ in range(2 * len(colors))] - + # Fill the paint sequence based on the input colors and car positions for car, color in enumerate(colors): pos1, pos2 = self.car_positions[car] - + paint_sequence[pos1] = color paint_sequence[pos2] = 1 - color # The opposite color - + # Compute the number of paint swaps in the sequence - color_swaps = sum(abs(paint_sequence[i] - paint_sequence[i + 1]) for i in range(len(paint_sequence) - 1)) - + color_swaps = sum( + abs(paint_sequence[i] - paint_sequence[i + 1]) + for i in range(len(paint_sequence) - 1) + ) + return paint_sequence, color_swaps def solve_redfirst(self): """ - The `red_first_solution` method applies a heuristic to generate a paint sequence for cars. - Specifically, it colors the first occurrence of each car as Red (1) and the second - occurrence as Blue (0). On average, this heuristic may not be as efficient as the greedy + The `red_first_solution` method applies a heuristic to generate a paint sequence for cars. + Specifically, it colors the first occurrence of each car as Red (1) and the second + occurrence as Blue (0). On average, this heuristic may not be as efficient as the greedy algorithm. Attributes: @@ -495,10 +511,10 @@ def solve_redfirst(self): 1. A list representing the paint sequence with '1' indicating Red and '0' indicating Blue. 2. An integer representing the total number of paint swaps in the sequence. """ - + # Dictionary to keep track of whether a car has been painted or not cars_painted = defaultdict(bool) - + # Create the paint sequence paint_sequence = [] for car in self.car_sequence: @@ -509,15 +525,18 @@ def solve_redfirst(self): paint_sequence.append(0) # Compute the number of color swaps in the sequence - color_swaps = sum(abs(paint_sequence[i] - paint_sequence[i + 1]) for i in range(len(self.car_sequence) - 1)) - + color_swaps = sum( + abs(paint_sequence[i] - paint_sequence[i + 1]) + for i in range(len(self.car_sequence) - 1) + ) + return paint_sequence, color_swaps def solve_greedy(self): """ - The `greedy_solution` method determines a feasible paint sequence for cars using a - greedy approach. It processes the car sequence from left to right, coloring the - first occurrence of each car based on its predecessor and the second occurrence + The `greedy_solution` method determines a feasible paint sequence for cars using a + greedy approach. It processes the car sequence from left to right, coloring the + first occurrence of each car based on its predecessor and the second occurrence with the opposite color. Attributes: @@ -535,16 +554,16 @@ def solve_greedy(self): 1. A list representing the paint sequence with '1' indicating Red and '0' indicating Blue. 2. An integer representing the total number of paint swaps in the sequence. """ - + # Dictionary to keep track of whether a car has been painted or not cars_painted = defaultdict(bool) - + # List to store the paint sequence paint_sequence = [] - + # Variable to keep track of the last used color last_color = 0 - + # Create the paint sequence for car in self.car_sequence: if not cars_painted[car]: @@ -556,8 +575,11 @@ def solve_greedy(self): paint_sequence.append(1 - paint_sequence[self.car_positions[car][0]]) # Compute the number of color swaps in the sequence - color_swaps = sum(abs(paint_sequence[i] - paint_sequence[i + 1]) for i in range(len(self.car_sequence) - 1)) - + color_swaps = sum( + abs(paint_sequence[i] - paint_sequence[i + 1]) + for i in range(len(self.car_sequence) - 1) + ) + return paint_sequence, color_swaps def plot_paint_sequence(self, paint_sequence, ax=None): @@ -568,52 +590,64 @@ def plot_paint_sequence(self, paint_sequence, ax=None): ---------- self.car_sequence : numpy.ndarray[int] Numpy array containing the order of cars to be painted. - + paint_sequence : list[int] or numpy.ndarray[int] - List or numpy array containing 0 or 1 for each car in `self.car_sequence`. + List or numpy array containing 0 or 1 for each car in `self.car_sequence`. A 0 indicates the car is painted Blue, while a 1 indicates it's painted Red. ax : matplotlib.axes.Axes, optional Axes object to plot on. If not provided, a new figure is created. - + Returns: ------- None Note: ----- - This function uses a blue-red color mapping and plots bars without any gaps to visually - represent the paint sequence of cars. The plot's size is dynamically adjusted based on + This function uses a blue-red color mapping and plots bars without any gaps to visually + represent the paint sequence of cars. The plot's size is dynamically adjusted based on the number of cars. """ - + # Define color mapping for 0s and 1s in the paint_sequence to 'blue' and 'red' respectively - color_map = {0: '#48cae4', 1: '#f25c54'} # shades of blue and red + color_map = {0: "#48cae4", 1: "#f25c54"} # shades of blue and red plot_colors = [color_map[color] for color in paint_sequence] # Dynamically determine the width of the figure based on the number of cars in self.car_sequence - fig_width = self.car_sequence.size * 0.8 # This provides each bar approximately 0.8 inch width + fig_width = ( + self.car_sequence.size * 0.8 + ) # This provides each bar approximately 0.8 inch width # If no ax (subplot) is provided, create a new figure with the determined width if ax is None: fig, ax = plt.subplots(figsize=(fig_width, 2)) - + # Plot bars for each car, colored based on the paint_sequence - ax.bar(range(len(self.car_sequence)), np.ones_like(self.car_sequence), color=plot_colors, width=1, align='center') - ax.set_xlim(-0.5, len(self.car_sequence) - 0.5) # Set x limits to tightly fit bars - ax.set_ylim(0, 1) # Set y limits from 0 to 1 as the bars have a fixed height of 1 + ax.bar( + range(len(self.car_sequence)), + np.ones_like(self.car_sequence), + color=plot_colors, + width=1, + align="center", + ) + ax.set_xlim( + -0.5, len(self.car_sequence) - 0.5 + ) # Set x limits to tightly fit bars + ax.set_ylim( + 0, 1 + ) # Set y limits from 0 to 1 as the bars have a fixed height of 1 ax.yaxis.set_visible(False) # Hide the y-axis as it's not relevant - + # Set x-ticks to indicate car numbers and label them as "Car 3", "Car 2", "Car 2", etc. ax.set_xticks(range(len(self.car_sequence))) ax.set_xticklabels([f"Car {int(car)}" for car in self.car_sequence]) # Hide the top, right, and left spines for cleaner visuals - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['left'].set_visible(False) - + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["left"].set_visible(False) + # If no ax is provided, show the plot directly if ax is None: plt.tight_layout() - plt.show() \ No newline at end of file + plt.show() diff --git a/src/openqaoa-core/tests/test_bpsp.py b/src/openqaoa-core/tests/test_bpsp.py index a8b08346..cf244677 100644 --- a/src/openqaoa-core/tests/test_bpsp.py +++ b/src/openqaoa-core/tests/test_bpsp.py @@ -19,11 +19,12 @@ def terms_list_equality(terms_list1, terms_list2): return bool + def fisher_yates_shuffle(arr, rng): """ Perform an in-place shuffle of a list using the Fisher-Yates shuffle algorithm. - This algorithm runs in O(n) time and ensures that every permutation of the array is equally likely. + This algorithm runs in O(n) time and ensures that every permutation of the array is equally likely. The shuffle is performed in-place, meaning that the input array `arr` is modified directly. Parameters: @@ -59,6 +60,7 @@ def fisher_yates_shuffle(arr, rng): # Return the shuffled array return arr + class TestBPSP(unittest.TestCase): """ Test suite for the BPSP problem. @@ -71,12 +73,14 @@ def test_bpsp_qubo(self): """ Test the correct QUBO formation from a provided graph. - This method validates that BPSP creates the expected QUBO by comparing + This method validates that BPSP creates the expected QUBO by comparing its terms and weights with predefined expected values. """ car_sequence = [3, 1, 0, 0, 2, 2, 4, 4, 3, 1] G = nx.Graph() - G.add_weighted_edges_from([[3, 1, -2], [3, 4, -1], [1, 0, -1], [0, 2, 1], [2, 4, 1]]) + G.add_weighted_edges_from( + [[3, 1, -2], [3, 4, -1], [1, 0, -1], [0, 2, 1], [2, 4, 1]] + ) gr_edges = [[3, 1], [3, 4], [1, 0], [0, 2], [2, 4]] gr_weights = [-2, -1, -1, 1, 1] @@ -103,12 +107,12 @@ def test_bpsp_random_instance(self): """ Test the random instance generation of the Binary Paint Shop Problem (BPSP). - This test verifies whether the `random_instance` method of the BPSP class + This test verifies whether the `random_instance` method of the BPSP class correctly creates a randomized car sequence based on a given number of cars and seed. The car sequence should be a list containing two occurrences of each car ID, randomly shuffled using the Fisher-Yates shuffle algorithm. - The function asserts that the sequence generated by the BPSP class's method + The function asserts that the sequence generated by the BPSP class's method is identical to the sequence produced by an explicitly defined Fisher-Yates shuffle process, ensuring both consistency and correctness in the shuffling method based on a fixed seed. """ @@ -120,7 +124,8 @@ def test_bpsp_random_instance(self): # Lambda function to create and shuffle car sequence using Fisher-Yates algorithm # The lambda function first duplicates each car ID using np.tile, then applies the shuffle. create_car_seq = lambda num_cars, seed: fisher_yates_shuffle( - np.tile(np.arange(num_cars), 2), np.random.default_rng(seed)) + np.tile(np.arange(num_cars), 2), np.random.default_rng(seed) + ) # Generating a random BPSP instance using the class method bpsp = BPSP.random_instance(num_cars=num_cars, seed=seed) @@ -128,8 +133,10 @@ def test_bpsp_random_instance(self): # Asserting if the generated car sequence in BPSP instance is identical # to the sequence produced by our lambda function. # This confirms the random instance generation works as expected. - self.assertTrue(all(bpsp.car_sequence == create_car_seq(num_cars, seed)), - "The generated car sequence does not match the expected shuffled sequence.") + self.assertTrue( + all(bpsp.car_sequence == create_car_seq(num_cars, seed)), + "The generated car sequence does not match the expected shuffled sequence.", + ) def test_bpsp_car_pos(self): """Test the retrieval of car positions.""" @@ -140,22 +147,26 @@ def test_bpsp_graph(self): """Test the generation of a graph representation of the BPSP instance.""" car_sequence = [3, 1, 0, 0, 2, 2, 4, 4, 3, 1] G = nx.Graph() - G.add_weighted_edges_from([[3, 1, -2], [3, 4, -1], [1, 0, -1], [0, 2, 1], [2, 4, 1]]) + G.add_weighted_edges_from( + [[3, 1, -2], [3, 4, -1], [1, 0, -1], [0, 2, 1], [2, 4, 1]] + ) bpsp_graph = BPSP(car_sequence).graph - assert nx.is_isomorphic(G, bpsp_graph), "The created graph and BPSP graph are not identical," + assert nx.is_isomorphic( + G, bpsp_graph + ), "The created graph and BPSP graph are not identical," def test_bpsp_docplex_bpsp_model(self): """Test if the docplex model representation of BPSP is generated.""" bpsp = BPSP.random_instance(num_cars=2, seed=1234) - + # Obtain the model mdl = bpsp.docplex_bpsp_model # Access as attribute due to @property decorator - + # Verify that the function returns a valid model instance self.assertIsInstance(mdl, Model, "Model is not an instance of DOcplex Model") - + # Manually retrieve binary variables based on their expected names sequence = bpsp.car_sequence expected_var_names = [f"w_{w}_{i}" for i, w in enumerate(sequence)] @@ -168,13 +179,19 @@ def test_bpsp_docplex_bpsp_model(self): self.assertTrue(var.is_binary(), f"Variable {var} is not binary") # Check number of constraints - self.assertEqual(mdl.number_of_constraints, 11, "Unexpected number of constraints") - + self.assertEqual( + mdl.number_of_constraints, 11, "Unexpected number of constraints" + ) + # Check if objective function exists self.assertIsNotNone(mdl.objective_expr, "Objective function is missing") - + # Check if the objective is to minimize - self.assertEqual(mdl.objective_sense, ObjectiveSense.Minimize, "Objective should be of type 'minimize'") + self.assertEqual( + mdl.objective_sense, + ObjectiveSense.Minimize, + "Objective should be of type 'minimize'", + ) def test_bpsp_solve_cplex(self): """ @@ -191,7 +208,7 @@ def test_bpsp_solve_cplex(self): def test_bpsp_paintseq_from_bits(self): """Test the solution of the BPSP problem using QAOA.""" bpsp = BPSP(np.array([0, 1, 0, 1])) - sequence, color_swaps = bpsp.paintseq_from_bits('10') + sequence, color_swaps = bpsp.paintseq_from_bits("10") self.assertEqual(sequence, [1, 0, 0, 1]) self.assertEqual(color_swaps, 2) @@ -209,5 +226,6 @@ def test_bpsp_solve_greedy(self): self.assertEqual(sequence, [0, 0, 1, 1]) self.assertEqual(color_swaps, 1) + if __name__ == "__main__": unittest.main() diff --git a/src/openqaoa-core/tests/test_qubo.py b/src/openqaoa-core/tests/test_qubo.py index b1c14c59..4dcc5166 100644 --- a/src/openqaoa-core/tests/test_qubo.py +++ b/src/openqaoa-core/tests/test_qubo.py @@ -159,7 +159,12 @@ def test_problem_instance(self): ], "sherrington_kirkpatrick": ["problem_type", "G"], "k_color": ["problem_type", "G", "k", "penalty"], - "binary_paint_shop_problem": ["problem_type", "car_sequence", "car_positions", "bpsp_graph"], + "binary_paint_shop_problem": [ + "problem_type", + "car_sequence", + "car_positions", + "bpsp_graph", + ], "generic_qubo": ["problem_type"], } diff --git a/src/openqaoa-qiskit/requirements.txt b/src/openqaoa-qiskit/requirements.txt index 8ed43dfe..469ae1b6 100644 --- a/src/openqaoa-qiskit/requirements.txt +++ b/src/openqaoa-qiskit/requirements.txt @@ -1,3 +1,3 @@ -qiskit>=0.36.1 +qiskit>=0.36.1,<1.0 qiskit-ibm-provider qiskit-aer \ No newline at end of file diff --git a/src/openqaoa-qiskit/tests/test_circuit_routing_qiskit.py b/src/openqaoa-qiskit/tests/test_circuit_routing_qiskit.py index 37fa1529..cd73d03d 100644 --- a/src/openqaoa-qiskit/tests/test_circuit_routing_qiskit.py +++ b/src/openqaoa-qiskit/tests/test_circuit_routing_qiskit.py @@ -59,11 +59,12 @@ def values_return(self): class TestingQubitRouting(unittest.TestCase): @pytest.mark.qpu def setUp(self): - # case qubits device > qubits problem (IBM NAIROBI) - self.IBM_NAIROBI_KNAPSACK = ExpectedRouting( + # case qubits device > qubits problem (IBM KYOTO) + self.IBM_KYOTO_KNAPSACK = ExpectedRouting( qubo=Knapsack.random_instance(n_items=3, seed=20).qubo, + # qubo = NumberPartition(list(range(1,4))).qubo, device_location="ibmq", - device_name="ibm_nairobi", + device_name="ibm_kyoto", qpu_credentials={ "hub": "ibm-q", "group": "open", @@ -82,30 +83,29 @@ def setUp(self): (1, 3), (1, 4), ], + # problem_to_solve = [[0,1],[1,2],[2,3]], initial_mapping=None, gate_indices_list=[ - [2, 4], + [0, 1], + [2, 3], + [3, 4], [1, 2], - [3, 5], - [0, 5], - [4, 5], - [4, 5], - [2, 4], - [0, 5], - [2, 4], + [2, 3], + [3, 4], [1, 2], - [4, 5], - [0, 5], + [0, 1], [1, 2], - [2, 4], - [2, 4], - [0, 5], - [4, 5], + [1, 2], + [2, 3], + [3, 4], + [2, 3], + [0, 1], + [1, 2], + [2, 3], ], swap_mask=[ False, False, - True, False, False, True, @@ -121,40 +121,40 @@ def setUp(self): True, False, ], - initial_physical_to_logical_mapping={6: 0, 2: 1, 1: 2, 4: 3, 3: 4, 5: 5}, - final_logical_qubit_order=[5, 4, 0, 1, 2, 3], + initial_physical_to_logical_mapping={0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, + final_logical_qubit_order=[1, 2, 4, 0, 3], ) - # case qubits problem == 2 (IBM perth) - self.IBM_perth_QUBO2 = ExpectedRouting( + # case qubits problem == 2 (IBM kyoto) + self.IBM_KYOTO_QUBO2 = ExpectedRouting( qubo=QUBO.from_dict( { - "terms": [[0, 1], [1]], - "weights": [9.800730090617392, 26.220558065741773], - "n": 2, + "terms": [[0, 1], [0, 2], [1]], + "weights": [1, 9.800730090617392, 26.220558065741773], + "n": 3, } ), device_location="ibmq", - device_name="ibm_perth", + device_name="ibm_kyoto", qpu_credentials={ "hub": "ibm-q", "group": "open", "project": "main", "as_emulator": True, }, - problem_to_solve=[(0, 1)], + problem_to_solve=[(0, 1), (0, 2)], initial_mapping=None, - gate_indices_list=[[0, 2], [1, 2]], - swap_mask=[True, False], - initial_physical_to_logical_mapping={2: 0, 3: 1, 1: 2}, - final_logical_qubit_order=[2, 1, 0], + gate_indices_list=[[0, 1], [1, 2], [0, 1]], + swap_mask=[False, True, False], + initial_physical_to_logical_mapping={0: 0, 1: 1, 2: 2}, + final_logical_qubit_order=[0, 2, 1], ) # case qubits device == qubits problem (IBM perth) - self.IBM_perth_NPARTITION = ExpectedRouting( + self.IBM_KYOTO_NPARTITION = ExpectedRouting( qubo=NumberPartition.random_instance(n_numbers=7, seed=2).qubo, device_location="ibmq", - device_name="ibm_perth", + device_name="ibm_kyoto", qpu_credentials={ "hub": "ibm-q", "group": "open", @@ -186,37 +186,42 @@ def setUp(self): ], initial_mapping=None, gate_indices_list=[ - [0, 5], - [1, 5], - [3, 4], [4, 5], + [1, 2], + [0, 1], [2, 3], - [3, 6], - [4, 5], - [0, 5], - [1, 5], [3, 4], + [5, 6], + [1, 2], + [0, 1], + [2, 3], [3, 4], [2, 3], - [3, 6], [4, 5], - [0, 5], - [1, 5], [2, 3], - [3, 6], + [1, 2], + [4, 5], + [5, 6], [3, 4], + [0, 1], + [1, 2], [3, 4], - [3, 6], - [0, 5], - [1, 5], + [5, 6], [4, 5], + [2, 3], + [1, 2], [4, 5], - [1, 5], - [3, 6], + [3, 4], + [1, 2], + [0, 1], + [2, 3], [3, 4], [3, 4], - [1, 5], [4, 5], + [5, 6], + [2, 3], + [0, 1], + [1, 2], ], swap_mask=[ False, @@ -228,24 +233,29 @@ def setUp(self): True, False, False, - False, True, False, False, True, False, + True, + False, False, True, False, + True, + True, False, True, False, True, False, + True, False, True, False, True, + True, False, True, True, @@ -253,14 +263,14 @@ def setUp(self): ], initial_physical_to_logical_mapping={ 0: 0, - 2: 1, - 4: 2, - 5: 3, - 3: 4, - 1: 5, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, 6: 6, }, - final_logical_qubit_order=[3, 5, 1, 0, 6, 2, 4], + final_logical_qubit_order=[5, 4, 1, 6, 3, 0, 2], ) # create a list of all the cases diff --git a/src/openqaoa-qiskit/tests/test_gate_applicators_qiskit.py b/src/openqaoa-qiskit/tests/test_gate_applicators_qiskit.py index c26d50a2..59310b75 100644 --- a/src/openqaoa-qiskit/tests/test_gate_applicators_qiskit.py +++ b/src/openqaoa-qiskit/tests/test_gate_applicators_qiskit.py @@ -3,6 +3,8 @@ from qiskit.circuit import gate as qsk_gate from qiskit.circuit import controlledgate as qsk_c_gate from qiskit.circuit.library import standard_gates as qsk_s_gate + +# from qiskit.circuit import singleton as qsk_sgt from qiskit import QuantumCircuit import openqaoa @@ -22,6 +24,18 @@ def setUp(self): for each_subclass_gate in qsk_c_gate.ControlledGate.__subclasses__() ] ) + # available_gates.extend( + # [ + # each_subclass_gate + # for each_subclass_gate in qsk_sgt.SingletonGate.__subclasses__() + # ] + # ) + # available_gates.extend( + # [ + # each_subclass_gate + # for each_subclass_gate in qsk_sgt.SingletonControlledGate.__subclasses__() + # ] + # ) available_qiskit_gates_name = [ each_gate.__name__.lower() for each_gate in available_gates diff --git a/src/openqaoa-qiskit/tests/test_qiskit_backend_wrapper.py b/src/openqaoa-qiskit/tests/test_qiskit_backend_wrapper.py index b9c309e3..d1b3ade7 100644 --- a/src/openqaoa-qiskit/tests/test_qiskit_backend_wrapper.py +++ b/src/openqaoa-qiskit/tests/test_qiskit_backend_wrapper.py @@ -96,7 +96,7 @@ def test_wrap_any_backend(self): create_device(location="local", name="qiskit.qasm_simulator") ) device_list.append( - create_device(location="ibmq", name="ibm_perth"), + create_device(location="ibmq", name="ibm_kyoto"), ) for device in device_list: diff --git a/src/openqaoa-qiskit/tests/test_qpu_qiskit.py b/src/openqaoa-qiskit/tests/test_qpu_qiskit.py index 0e875404..78707b72 100644 --- a/src/openqaoa-qiskit/tests/test_qpu_qiskit.py +++ b/src/openqaoa-qiskit/tests/test_qpu_qiskit.py @@ -459,7 +459,7 @@ def test_remote_qubit_overflow(self): # Check the creation of the varitional parms _ = create_qaoa_variational_params(qaoa_descriptor, "standard", "rand") - qiskit_device = DeviceQiskit("ibm_perth", self.HUB, self.GROUP, self.PROJECT) + qiskit_device = DeviceQiskit("ibm_kyoto", self.HUB, self.GROUP, self.PROJECT) try: QAOAQiskitQPUBackend( @@ -494,7 +494,7 @@ def test_integration_on_emulator(self): qaoa_descriptor = QAOADescriptor(cost_hamil, mixer_hamil, p=p) variate_params = QAOAVariationalStandardParams(qaoa_descriptor, betas, gammas) qiskit_device = DeviceQiskit( - "ibm_perth", + "ibm_kyoto", self.HUB, self.GROUP, self.PROJECT, @@ -530,7 +530,7 @@ def test_remote_integration_qpu_run(self): mixer_hamil = X_mixer_hamiltonian(n_qubits=nqubits) qaoa_descriptor = QAOADescriptor(cost_hamil, mixer_hamil, p=p) variate_params = QAOAVariationalStandardParams(qaoa_descriptor, betas, gammas) - qiskit_device = DeviceQiskit("ibm_perth", self.HUB, self.GROUP, self.PROJECT) + qiskit_device = DeviceQiskit("ibm_kyoto", self.HUB, self.GROUP, self.PROJECT) qiskit_backend = QAOAQiskitQPUBackend( qaoa_descriptor, qiskit_device, shots, None, None, False diff --git a/tests/test_imports.py b/tests/test_imports.py index 2fdaf5de..a745087a 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -17,7 +17,9 @@ def test_all_module_import(self): """ folder_names = [ - each_file for each_file in os.listdir("src") if ("openqaoa-" in each_file and not "openqaoa-pyquil") + each_file + for each_file in os.listdir("src") + if ("openqaoa-" in each_file and not "openqaoa-pyquil") ] packages_import = []