From f54cb6a038e6389233840367811a0015bac08a99 Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 30 Sep 2024 20:30:17 +0200 Subject: [PATCH 01/40] Refactor code for PEP8 compliance and improved readability --- .settings/requirements_full.txt | 4 +- src/demo/instruction_demo.py | 35 +- .../applications/optimization/ACL/ACL.py | 1113 +++++++++-------- .../applications/optimization/ACL/__init__.py | 7 +- .../optimization/ACL/mappings/ISING.py | 108 +- .../optimization/ACL/mappings/QUBO.py | 128 +- .../optimization/ACL/mappings/__init__.py | 7 +- .../applications/optimization/MIS/MIS.py | 92 +- .../applications/optimization/MIS/__init__.py | 7 +- .../optimization/MIS/data/__init__.py | 6 +- .../optimization/MIS/data/graph_layouts.py | 94 +- .../optimization/MIS/mappings/NeutralAtom.py | 49 +- .../optimization/MIS/mappings/__init__.py | 6 +- .../applications/optimization/PVC/PVC.py | 130 +- .../applications/optimization/PVC/__init__.py | 6 +- .../PVC/data/createReferenceGraph.py | 38 +- .../optimization/PVC/mappings/ISING.py | 88 +- .../optimization/PVC/mappings/QUBO.py | 144 +-- .../optimization/PVC/mappings/__init__.py | 6 +- 19 files changed, 1008 insertions(+), 1060 deletions(-) diff --git a/.settings/requirements_full.txt b/.settings/requirements_full.txt index 30c31969..a6025883 100644 --- a/.settings/requirements_full.txt +++ b/.settings/requirements_full.txt @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72809f65fb0919ae9ff2dcdc19151cc52a41adf9ee7f18900ce7ddc365197e69 -size 754 +oid sha256:38b4f8ba527ee7c43b6f5cb7b1bfbbc82e7f5763067d28545b63d53b731fdc9f +size 818 diff --git a/src/demo/instruction_demo.py b/src/demo/instruction_demo.py index 612c0bf9..6f53a968 100644 --- a/src/demo/instruction_demo.py +++ b/src/demo/instruction_demo.py @@ -9,40 +9,59 @@ class InstructionDemo(Application): """ A simple QUARK Application implementation showing the usage of instructions. """ + def __init__(self, application_name: str = None): super().__init__(application_name) self.submodule_options = ["Dummy"] - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple: + """ + Preprocess input data with given configuration and instructions. + """ logging.info("%s", kwargs.keys()) logging.info("previous_job_info: %s", kwargs.get("previous_job_info")) + rep_count = kwargs["rep_count"] instruction_name = config.get("instruction", Instruction.PROCEED.name) instruction = Instruction.PROCEED + if instruction_name == Instruction.PROCEED.name: instruction = Instruction.PROCEED elif instruction_name == Instruction.INTERRUPT.name: instruction = Instruction.INTERRUPT if instruction_name == "mixed": instruction = Instruction.PROCEED - if rep_count%2 == 1: + if rep_count % 2 == 1: instruction = Instruction.INTERRUPT elif instruction_name == "exception": raise Exception("demo exception") - logging.info("InstructionDemo iteration %s returns instruction %s", rep_count, instruction.name) + logging.info( + "InstructionDemo iteration %s returns instruction %s", + rep_count, instruction.name + ) return instruction, "", 0. def get_parameter_options(self) -> dict: + """ + Returns parameter options for the preprocess methios. + """ return { - "instruction": {"values": [Instruction.PROCEED.name, - Instruction.INTERRUPT.name, - "exception", - "mixed"], - "description": "How should preprocess behave?"} + "instruction": { + "values": [ + Instruction.PROCEED.name, + Instruction.INTERRUPT.name, + "exception", + "mixed" + ], + "description": "How should preprocess behave?" + } } def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + """ return Dummy() def save(self, path: str, iter_count: int) -> None: diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index d20971c8..d7352f9a 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -17,23 +17,24 @@ # Modifications Copyright (c) 2007- Stuart Anthony Mitchell # # Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the Software +# is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. -from typing import TypedDict +import os +import logging +from typing import TypedDict, List, Dict, Any, Tuple import pandas as pd import numpy as np import pulp -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement @@ -41,15 +42,15 @@ class ACL(Optimization): """ The distribution of passenger vehicles is a complex task and a high cost factor for automotive original -equipment manufacturers (OEMs). On the way from the production plant to the customer, vehicles travel -long distances on different carriers such as ships, trains, and trucks. To save costs, OEMs and logistics service -providers aim to maximize their loading capacities. Modern auto carriers are extremely flexible. Individual -platforms can be rotated, extended, or combined to accommodate vehicles of different shapes and weights -and to nest them in a way that makes the best use of the available space. In practice, finding feasible -combinations is done with the help of simple heuristics or based on personal experience. In research, most -papers that deal with auto carrier loading focus on route or cost optimization. Only a rough approximation -of the loading sub-problem is considered. We formulate the problem as a mixed integer quadratically constrained -assignment problem. + equipment manufacturers (OEMs). Vehicles travel long distance on different carriers, such as ships, + trains, and trucks, from the production plant to the customer. + + To save costs, OEMs and logistics service providers aim to maximize their loading capacities. + Modern auto carriers are flexible, allowing individual platforms to be rotated, extended, or combined + to accommodate vehicles of different shapes and weights in a space-efficient manner. + + In practice, finding feasible combinations is often based on heuristics or personal experience. + We formulate the problem as a mixed integer quadratically constrained assignment problem. """ def __init__(self): @@ -61,44 +62,45 @@ def __init__(self): self.application = None @staticmethod - def get_requirements() -> list: - return [ - { - "name": "pulp", - "version": "2.9.0" - }, - { - "name": "pandas", - "version": "2.2.2" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "openpyxl", - "version": "3.1.5" - } + def get_requirements() -> list[dict]: + """ + Returns the list of module requirements. + :return: List of dictionaries containing module requirements + """ + return [ + {"name": "pulp", "version": "2.9.0"}, + {"name": "pandas", "version": "2.2.2"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "openpyxl", "version": "3.1.5"}, ] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The submodule option to retrieve + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented + """ if option == "MIPsolverACL": from modules.solvers.MIPsolverACL import MIPaclp # pylint: disable=C0415 return MIPaclp() - # elif option == "Ising": - # from modules.applications.optimization.ACL.mappings.ISING import Ising # pylint: disable=C0415 - # return Ising() elif option == "QUBO": from modules.applications.optimization.ACL.mappings.QUBO import Qubo # pylint: disable=C0415 return Qubo() else: raise NotImplementedError(f"Submodule Option {option} not implemented") - def get_parameter_options(self): + def get_parameter_options(self) -> dict: + """ + Returns parameter options for selecting different models. + + :return: Dictionary containing model selection options + """ return { "model_select": { - "values": list(["Full", "Small", "Tiny"]), + "values": ["Full", "Small", "Tiny"], "description": "Do you want the full model or a simplified (QUBO friendly) one?" } } @@ -107,25 +109,37 @@ class Config(TypedDict): model_select: str @staticmethod - def intersectset(p1, p2): + def intersectset(p1: List, p2: List) -> List: + """ + Computes the intersection of two lists. + + :param p1: First list + :param p2: Second list + :return: List containing elements common to both p1 and p2 + """ return np.intersect1d(p1, p2).tolist() @staticmethod - def diffset(p1, p2): + def diffset(p1: List, p2: List) -> List: + """ + Computes the difference between two lists. + + :param p1: First list + :param p2: Second list + :return: List containing elements in p1 that are not in p2 + """ return np.setdiff1d(p1, p2).tolist() - def generate_problem(self, config: Config) -> dict: # pylint: disable=R0915 + def generate_problem(self, config: Config) -> Dict: # pylint: disable=R0915 """ This function includes three models: Full, small and tiny. Full refers to the original model with all of its - constraints. Small refers to the simplified model which is more suitable for solving it with QC methods. + constraints. Small refers to the simplified model suitable for solving it with QC methods. The simplified model does not consider the possibility that vehicles can be angled or that they can be oriented forwards or backwards in relation to the auto carrier. For the tiny version we do not consider split platforms and consider only weight constraints. :param config: Config containing the selected scenario - :type config: Config :return: Dictionary with scenario-dependent model formulated as linear problem - :rtype: dict """ # Enter vehicles to load (BMW model codes) vehicles = ["G20", "G20", "G20", "G20", "G07", "G20"] @@ -133,544 +147,539 @@ def generate_problem(self, config: Config) -> dict: # pylint: disable=R0915 df = pd.read_excel(os.path.join(os.path.dirname(__file__), "Vehicle_data_QUARK.xlsx")) model_select = config['model_select'] - # All the parameters are given in decimeters -> 4m == 400 cm == 40 dm or decitons -> 2 tons -> 20 dt + # All the parameters are in decimeters and decitons (4m == 400 cm == 40 dm , 2 tons -> 20 dt # Below are the model specific parameters, constraints and objectives for the tiny, small and the full model if model_select == "Tiny": - # Weight parameters - # max. total weight on truck / trailer - wt = [100] - # wt = [10] - # max. weight on the four levels - wl = [50, 60] - # wl = [5, 6] - # max. weights on platforms p, if not angled - wp = [23, 23, 23, 26, 17] - # wp = [2, 2, 2, 2, 1] - - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model - weight_list = [0] * (len(vehicles)) - - for i in set(range(len(vehicles))): - df_new = df.loc[df['Type'] == vehicles[i]] - # df_new = (df.loc[df['Type'] == vehicles[i]]) - weight_list[i] = int(df_new["Weight"].iloc[0]) - # weight_list[i] = int(int(df_new["Weight"].iloc[0])/10) - - # Construct sets - # Set of available cars - vecs = set(range(len(vehicles))) - # Set of available platforms - platforms_array = np.array([0, 1, 2, 3, 4]) - plats = set(range(len(platforms_array))) - - # Set of platforms that have a limitation on allowed weight - platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) - plats_l = set(range(len(platforms_level_array))) - - # Set of platforms that form trailer and truck - platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) - plats_t = set(range(len(platforms_truck_trailer_array))) - - # Create decision variables - # Vehicle v assigned to p - x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') - - # Create the 'prob' variable to contain the problem data - prob = pulp.LpProblem("ACL", pulp.LpMaximize) - - # Objective function - # Maximize number of vehicles on the truck - prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) - - # Constraints - # Assignment constraints - # (1) Every vehicle can only be assigned to a single platform - for p in plats: - prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 - - # (2) Every platform can only hold a single vehicle - for v in vecs: - prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + self._generate_tiny_model(df, vehicles) + elif model_select == "Small": + self._generate_small_model(df, vehicles) + else: + self._generate_full_model(df, vehicles) - # (3) Weight limit for every platform - for p in plats: - for v in vecs: - prob += weight_list[v] * x[p, v] <= wp[p] + problem_instance = self.application.to_dict() + self.application = problem_instance + return self.application - # (4) Weight constraint for every level - for p_l in plats_l: - prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ - wl[p_l] + def _generate_tiny_model(self, df, vehicles): + """ + Generate the problem model for the Tiny configurations. + """ + # Weight parameters + # max. total weight on truck / trailer + wt = [100] + # max. weight on the four levels + wl = [50, 60] + # max. weights on platforms p, if not angled + wp = [23, 23, 23, 26, 17] + + # Create empty lists for different vehicle parameters. This is required for proper indexing in the model + weight_list = [0] * (len(vehicles)) + + for i in range(len(vehicles)): + df_new = df.loc[df['Type'] == vehicles[i]] + weight_list[i] = int(df_new["Weight"].iloc[0]) + + # Set of available cars + vecs = set(range(len(vehicles))) + # Set of available platforms + platforms_array = np.array([0, 1, 2, 3, 4]) + plats = set(range(len(platforms_array))) + + # Set of platforms that have a limitation on allowed weight + platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) + plats_l = set(range(len(platforms_level_array))) + + # Set of platforms that form trailer and truck + platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) + plats_t = set(range(len(platforms_truck_trailer_array))) + + # Create decision variables + x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') + + # Create the problem instance + prob = pulp.LpProblem("ACL", pulp.LpMaximize) + + # Objective function + # Maximize number of vehicles on the truck + prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) + + # Constraints + # (1) Every vehicle can only be assigned to a single platform + for p in plats: + prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 + + # (2) Every platform can only hold a single vehicle + for v in vecs: + prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + + # (3) Weight limit for every platform + for p in plats: + for v in vecs: + prob += weight_list[v] * x[p, v] <= wp[p] - # (5) Weight constraint for truck and trailer - for t in plats_t: - prob += pulp.lpSum( - weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[t] for v in vecs) <= wt[t] + # (4) Weight constraint for every level + for p_l in plats_l: + prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ + wl[p_l] - elif model_select == "Small": + # (5) Weight constraint for truck and trailer + for t in plats_t: + prob += pulp.lpSum( + weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[t] for v in vecs) <= wt[t] + + self.application = prob - # For the small model, we only consider two levels with 3 and 2 platforms each - - # Length parameters - # Level 1 (Truck up), 2 (Truck down) - lmax_l = [97, 79] - - # Height parameters - # Considers base truck height and height distance between vehicles (~10cm) - hmax_truck = [34, 34, 33, 36, 32, 36] - # [0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4] - - # Weight parameters - # max. total weight on truck / trailer - wt = [100] - # max. weight on the two levels - wl = [65, 50] - # max. weights on platforms p, if not angled - wp = [23, 23, 23, 26, 17] - # max. weight on p, if sp is used - wsp = [28, 28, 28] - - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model - class_list = [0] * (len(vehicles)) - length_list = [0] * (len(vehicles)) - height_list = [0] * (len(vehicles)) - weight_list = [0] * (len(vehicles)) - - for i in set(range(len(vehicles))): - df_new = df.loc[df['Type'] == vehicles[i]] - class_list[i] = int(df_new["Class"].iloc[0]) - length_list[i] = int(df_new["Length"].iloc[0]) - height_list[i] = int(df_new["Height"].iloc[0]) - weight_list[i] = int(df_new["Weight"].iloc[0]) - - # Construct sets - # Set of available cars - vecs = set(range(len(vehicles))) - # Set of available platforms - platforms_array = np.array([0, 1, 2, 3, 4]) - plats = set(range(len(platforms_array))) - - # Set of possible split platforms - split_platforms_array = np.array([[0, 1], [1, 2], [3, 4]], dtype=object) - plats_sp = set(range(len(split_platforms_array))) - - # Set of platforms that have a limitation on allowed length and weight because they are on the same level - platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) - plats_l = set(range(len(platforms_level_array))) - - # Set of platforms that form trailer and truck - platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) - plats_t = set(range(len(platforms_truck_trailer_array))) - - # Set of platforms that have a limitation on allowed height - platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], - dtype=object) - plats_h1 = set(range(len(platforms_height_array_truck))) - - # Create decision variables - # Vehicle v assigned to p - x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') - # Usage of split platform - sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') - # Auxiliary variable for linearization of quadratic constraints - gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') - - # Create the 'prob' variable to contain the problem data - prob = pulp.LpProblem("ACL", pulp.LpMaximize) - - # Objective function - # Maximize number of vehicles on the truck - prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) - - # Constraints - # Assignment constraints - # (1) Every vehicle can only be assigned to a single platform - for p in plats: - prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 - - # (2) Every platform can only hold a single vehicle + def _generate_small_model(self, df, vehicles): + """ + Generate the problem model for the Small configuration + """ + # Parameters for the small model (2 levels with 3 and 2 platforms each) + + # Length parameters + # Level 1 (Truck up), 2 (Truck down) + lmax_l = [97, 79] + # Height parameters + # Considers base truck height and height distance between vehicles (~10cm) + hmax_truck = [34, 34, 33, 36, 32, 36] + # Weight parameters + # max. total weight on truck / trailer + wt = [100] + # max. weight on the two levels + wl = [65, 50] + # max. weights on platforms p, if not angled + wp = [23, 23, 23, 26, 17] + # max. weight on p, if sp is used + wsp = [28, 28, 28] + + class_list, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) + + # Set of available cars + vecs = set(range(len(vehicles))) + # Set of available platforms + platforms_array = np.array([0, 1, 2, 3, 4]) + plats = set(range(len(platforms_array))) + + # Set of possible split platforms + split_platforms_array = np.array([[0, 1], [1, 2], [3, 4]], dtype=object) + plats_sp = set(range(len(split_platforms_array))) + + # Set of platforms that have a limitation on allowed length and weight because they are on the same level + platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) + plats_l = set(range(len(platforms_level_array))) + + # Set of platforms that form trailer and truck + platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) + plats_t = set(range(len(platforms_truck_trailer_array))) + + # Set of platforms that have a limitation on allowed height + platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], dtype=object) + plats_h1 = set(range(len(platforms_height_array_truck))) + + # Create decision variables + x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') + # Usage of split platform + sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') + # Auxiliary variable for linearization of quadratic constraints + gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') + + # Create the 'prob' variable to contain the problem data + prob = pulp.LpProblem("ACL", pulp.LpMaximize) + # Maximize number of vehicles on the truck + prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) + + # Constraints + # (1) Every vehicle can only be assigned to a single platform + for p in plats: + prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 + + # (2) Every platform can only hold a single vehicle + for v in vecs: + prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + + # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used + for q in plats_sp: + prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ + <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] + + # (4) It is always only possible to use a single split-platform for any given p + for q in plats_sp: + for p in plats_sp: + if p != q: + z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) + if z is True: + prob += sp[q] + sp[p] <= 1 + + # (5) Length constraint + # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length + for L in plats_l: + prob += (pulp.lpSum(x[p, v] * length_list[v] for p in platforms_level_array[L] for v in vecs) + <= lmax_l[L]) + + # (6) Height constraints for truck and trailer, analogue to length constraints + # Truck + for h in plats_h1: + prob += pulp.lpSum(x[p, v] * height_list[v] for p in platforms_height_array_truck[h] for v in vecs) \ + <= hmax_truck[h] + + # (7) Linearization constraint -> gamma == 1, if split platform is used + for q in plats_sp: + prob += pulp.lpSum( + sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) + for v in vecs) >= 2 * gamma[q] + + # (8) Weight limit for every platform + for p in plats: for v in vecs: - prob += pulp.lpSum(x[p, v] for p in plats) <= 1 - - # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used - for q in plats_sp: - prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ - <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] - - # (4) It is always only possible to use a single split-platform for any given p - for q in plats_sp: - for p in plats_sp: - if p != q: - z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) - if z is True: - prob += sp[q] + sp[p] <= 1 - - # (5) Length constraint - # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length - for L in plats_l: - prob += (pulp.lpSum(x[p, v] * length_list[v] for p in platforms_level_array[L] for v in vecs) - <= lmax_l[L]) - - # (6) Height constraints for truck and trailer, analogue to length constraints - # Truck - for h in plats_h1: - prob += pulp.lpSum(x[p, v] * height_list[v] for p in platforms_height_array_truck[h] for v in vecs) \ - <= hmax_truck[h] - - # (7) Linearization constraint -> gamma == 1, if split platform is used - for q in plats_sp: - prob += pulp.lpSum( - sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) - for v in vecs) >= 2 * gamma[q] - - # (8) Weight limit for every platform - for p in plats: - for v in vecs: - prob += weight_list[v] * x[p, v] <= wp[p] - - # (9) If a split platform is used, weight limit == wsp, if not, then weight limit == wp - for q in plats_sp: - for p in split_platforms_array[q]: - prob += pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] \ - + (1 - gamma[q]) * wp[p] - - # (10) Weight constraint for every level - for p_l in plats_l: - prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ - wl[p_l] - - # (11) Weight constraint for truck and trailer - for p_t in plats_t: - prob += pulp.lpSum( - weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) <= wt[p_t] - else: - # Horizontal Coefficients: Length reduction - # 1 = forward, 0 = backward - # [0:1, 1:1, 0:0, 1:0] - v_coef = np.array([[0.20, 0.15, 0.14, 0.19], - [0.22, 0.22, 0.22, 0.22], - [0.22, 0.13, 0.12, 0.17]]) - - # Vertical Coefficients: Height increase - # [0:1, 1:1, 0:0, 1:0] - h_coef = np.array([[0.40, 1, 1, 1], - [0.17, 0.22, 0.21, 0.22], - [0.17, 0.38, 0.32, 0.32]]) - - # Length parameters - # Level 1 (Truck up), 2 (Truck down), 3 (Trailer up), 4 (Trailer down) - lmax_l = [97, 79, 97, 97] - - # Height parameters - # Considers base truck height and height distance between vehicles (~10cm) - hmax_truck = [34, 34, 33, 36, 32, 36] - # [0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4] - hmax_trailer = [36, 32, 32, 34] - # [5, 7], [5, 8], [6, 8], [6, 9] - - # Weight parameters - # max. total weight - wmax = 180 - # max. total weight on truck / trailer - wt = [100, 100] - # max. weight on the four levels - wl = [50, 60, 50, 90] - # max. weights on platforms p, if not angled - wp = [23, 23, 23, 26, 17, 26, 26, 26, 23, 26] - # max. weights on p, angled (if possible: 1, 2, 4, 7, 8, 9): - wpa = [20, 22, 17, 18, 19, 22] - # max. weight on p, if sp is used - wsp = [28, 28, 28, 28, 28, 28] - - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model - class_list = [0] * (len(vehicles)) - length_list = [0] * (len(vehicles)) - height_list = [0] * (len(vehicles)) - weight_list = [0] * (len(vehicles)) - - for i in set(range(len(vehicles))): - df_new = df.loc[df['Type'] == vehicles[i]] - class_list[i] = int(df_new["Class"].iloc[0]) - length_list[i] = int(df_new["Length"].iloc[0]) - height_list[i] = int(df_new["Height"].iloc[0]) - weight_list[i] = int(df_new["Weight"].iloc[0]) - - # Construct sets - # Set of available cars - vecs = set(range(len(vehicles))) - # Set of available platforms - platforms_array = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - plats = set(range(len(platforms_array))) - - # Set of platforms that can be angled - platforms_angled_array = [1, 2, 4, 7, 8] - vp = [0, 1, 3, 8, 9] # Platforms "under" a_p - plats_a = set(range(len(platforms_angled_array))) - - # Set of possible split platforms - split_platforms_array = np.array([[0, 1], [1, 2], [3, 4], [5, 6], [7, 8], [8, 9]], dtype=object) - plats_sp = set(range(len(split_platforms_array))) - - # Set of platforms that have a limitation on allowed length and weight because they are on the same level - platforms_level_array = np.array([[0, 1, 2], [3, 4], [5, 6], [7, 8, 9]], dtype=object) - plats_l = set(range(len(platforms_level_array))) - - # Set of platforms that form trailer and truck - platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], dtype=object) - plats_t = set(range(len(platforms_truck_trailer_array))) - - # Set of platforms that have a limitation on allowed height - platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], - dtype=object) - platforms_height_array_trailer = np.array([[5, 7], [5, 8], [6, 8], [6, 9]], dtype=object) - - plats_h1 = set(range(len(platforms_height_array_truck))) - plats_h2 = set(range(len(platforms_height_array_trailer))) - - # Create the 'prob' variable to contain the problem data - prob = pulp.LpProblem("ACL", pulp.LpMaximize) - - # Create decision variables - # Vehicle v assigned to p - x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') - # Usage of split platform - sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') - # Direction of vehicle on p - d = pulp.LpVariable.dicts('d', (p for p in plats), cat='Binary') - # State of platform p in PA - angled == 1, not angled == 0 - a_p = pulp.LpVariable.dicts('a_p', (p for p in plats_a), cat='Binary') - - # Create auxiliary variables for linearization of quadratic constraints - y1 = pulp.LpVariable.dicts('y1', (p for p in plats_a), cat='Binary') - y2 = pulp.LpVariable.dicts('y2', (p for p in plats_a), cat='Binary') - y3 = pulp.LpVariable.dicts('y3', (p for p in plats_a), cat='Binary') - y4 = pulp.LpVariable.dicts('y4', (p for p in plats_a), cat='Binary') - ay1 = pulp.LpVariable.dicts('ay1', (p for p in plats_a), cat='Binary') - ay2 = pulp.LpVariable.dicts('ay2', (p for p in plats_a), cat='Binary') - ay3 = pulp.LpVariable.dicts('ay3', (p for p in plats_a), cat='Binary') - ay4 = pulp.LpVariable.dicts('ay4', (p for p in plats_a), cat='Binary') - # Weight for split-platforms - gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') - - # Here the model starts, including objective and constraints - - # Objective function - # Maximize number of vehicles on the truck - prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) - - # Constraints - # Assignment constraints - # (1) Every vehicle can only be assigned to a single platform - for p in plats: - prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 - - # (2) Every platform can only hold a single vehicle + prob += weight_list[v] * x[p, v] <= wp[p] + + # (9) If a split platform is used, weight limit == wsp, if not, then weight limit == wp + for q in plats_sp: + for p in split_platforms_array[q]: + prob += pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] \ + + (1 - gamma[q]) * wp[p] + + # (10) Weight constraint for every level + for p_l in plats_l: + prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ + wl[p_l] + + # (11) Weight constraint for truck and trailer + for p_t in plats_t: + prob += pulp.lpSum( + weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) <= wt[p_t] + + self.application = prob + + def _generate_full_model(self, df, vehicles): + """ + Generate the problem model for the Full configuration. + """ + # Horizontal Coefficients: Length reduction + # 1 = forward, 0 = backward + v_coef = np.array([[0.20, 0.15, 0.14, 0.19], + [0.22, 0.22, 0.22, 0.22], + [0.22, 0.13, 0.12, 0.17]]) + + # Vertical Coefficients: Height increase + h_coef = np.array([[0.40, 1, 1, 1], + [0.17, 0.22, 0.21, 0.22], + [0.17, 0.38, 0.32, 0.32]]) + + # Length parameters + # Level 1 (Truck up), 2 (Truck down), 3 (Trailer up), 4 (Trailer down) + lmax_l = [97, 79, 97, 97] + + # Height parameters + # Considers base truck height and height distance between vehicles (~10cm) + hmax_truck = [34, 34, 33, 36, 32, 36] + hmax_trailer = [36, 32, 32, 34] + + # Weight parameters + # max. total weight + wmax = 180 + # max. total weight on truck / trailer + wt = [100, 100] + # max. weight on the four levels + wl = [50, 60, 50, 90] + # max. weights on platforms p, if not angled + wp = [23, 23, 23, 26, 17, 26, 26, 26, 23, 26] + # max. weights on p, angled (if possible: 1, 2, 4, 7, 8, 9): + wpa = [20, 22, 17, 18, 19, 22] + # max. weight on p, if sp is used + wsp = [28, 28, 28, 28, 28, 28] + + class_list, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) + + # Set of available cars + vecs = set(range(len(vehicles))) + # Set of available platforms + platforms_array = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + plats = set(range(len(platforms_array))) + + # Set of platforms that can be angled + platforms_angled_array = [1, 2, 4, 7, 8] + vp = [0, 1, 3, 8, 9] + plats_a = set(range(len(platforms_angled_array))) + + # Set of possible split platforms + split_platforms_array = np.array([[0, 1], [1, 2], [3, 4], [5, 6], [7, 8], [8, 9]], dtype=object) + plats_sp = set(range(len(split_platforms_array))) + + # Set of platforms that have a limitation on allowed length and weight because they are on the same level + platforms_level_array = np.array([[0, 1, 2], [3, 4], [5, 6], [7, 8, 9]], dtype=object) + plats_l = set(range(len(platforms_level_array))) + + # Set of platforms that form trailer and truck + platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], dtype=object) + plats_t = set(range(len(platforms_truck_trailer_array))) + + # Set of platforms that have a limitation on allowed height + platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], dtype=object) + platforms_height_array_trailer = np.array([[5, 7], [5, 8], [6, 8], [6, 9]], dtype=object) + + plats_h1 = set(range(len(platforms_height_array_truck))) + plats_h2 = set(range(len(platforms_height_array_trailer))) + + # Create decision variables + # Vehicle v assigned to p + x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') + # Usage of split platform + sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') + # Direction of vehicle on p + d = pulp.LpVariable.dicts('d', (p for p in plats), cat='Binary') + # State of platform p in PA - angled == 1, not angled == 0 + a_p = pulp.LpVariable.dicts('a_p', (p for p in plats_a), cat='Binary') + + # Auxiliary variables for linearization of quadratic constraints + y1 = pulp.LpVariable.dicts('y1', (p for p in plats_a), cat='Binary') + y2 = pulp.LpVariable.dicts('y2', (p for p in plats_a), cat='Binary') + y3 = pulp.LpVariable.dicts('y3', (p for p in plats_a), cat='Binary') + y4 = pulp.LpVariable.dicts('y4', (p for p in plats_a), cat='Binary') + ay1 = pulp.LpVariable.dicts('ay1', (p for p in plats_a), cat='Binary') + ay2 = pulp.LpVariable.dicts('ay2', (p for p in plats_a), cat='Binary') + ay3 = pulp.LpVariable.dicts('ay3', (p for p in plats_a), cat='Binary') + ay4 = pulp.LpVariable.dicts('ay4', (p for p in plats_a), cat='Binary') + # Weight for split-platforms + gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') + + # Create the 'prob' variable to contain the problem data + prob = pulp.LpProblem("ACL", pulp.LpMaximize) + # Maximize number of vehicles on the truck + prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) + + # Assignment constraints + # (1) Every vehicle can only be assigned to a single platform + for p in plats: + prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 + # (2) Every platform can only hold a single vehicle + for v in vecs: + prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + + # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used + for q in plats_sp: + prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs)\ + <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] + + # (3.1) It is always only possible to use a single split-platform for any given p + for q in plats_sp: + for p in plats_sp: + if p != q: + z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) + if z is True: + prob += sp[q] + sp[p] <= 1 + + # (3.2) It is not allowed to angle platforms next to empty platforms + for i, p in enumerate(platforms_angled_array): + prob += pulp.lpSum(x[p, v] + x[vp[i], v] for v in vecs) >= 2 * a_p[i] + + # Linearization constraints + # Linearization of d_p and d_v(p) -> orientations of two neighboring cars + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + v_p = vp[z] + prob += (1 - d[p]) + d[v_p] >= 2 * y1[z] + prob += d[p] + d[v_p] >= 2 * y2[z] + prob += (1 - d[p]) + (1 - d[v_p]) >= 2 * y3[z] + prob += d[p] + (1 - d[v_p]) >= 2 * y4[z] + + # Linearization of a_p with y1 - y4 -> linear combination of angle and orientations + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + prob += a_p[z] + y1[z] >= 2 * ay1[z] + prob += a_p[z] + y2[z] >= 2 * ay2[z] + prob += a_p[z] + y3[z] >= 2 * ay3[z] + prob += a_p[z] + y4[z] >= 2 * ay4[z] + + # Linearization of x * ay -> linear combination of assignment and orientation/angle + xay1 = pulp.LpVariable.dicts('xay1', ((p, v) for p in plats_a for v in vecs), cat='Binary') + xay2 = pulp.LpVariable.dicts('xay2', ((p, v) for p in plats_a for v in vecs), cat='Binary') + xay3 = pulp.LpVariable.dicts('xay3', ((p, v) for p in plats_a for v in vecs), cat='Binary') + xay4 = pulp.LpVariable.dicts('xay4', ((p, v) for p in plats_a for v in vecs), cat='Binary') + for p in platforms_angled_array: + z = platforms_angled_array.index(p) for v in vecs: - prob += pulp.lpSum(x[p, v] for p in plats) <= 1 - - # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used - for q in plats_sp: - prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs)\ - <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] - - # (3.1) It is always only possible to use a single split-platform for any given p - for q in plats_sp: - for p in plats_sp: - if p != q: - z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) - if z is True: - prob += sp[q] + sp[p] <= 1 - - # (3.2) It is not allowed to angle platforms next to empty platforms - for i, p in enumerate(platforms_angled_array): - prob += pulp.lpSum(x[p, v] + x[vp[i], v] for v in vecs) >= 2 * a_p[i] - - # Linearization constraints - # Linearization of d_p and d_v(p) -> orientations of two neighboring cars - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - v_p = vp[z] - prob += (1 - d[p]) + d[v_p] >= 2 * y1[z] - prob += d[p] + d[v_p] >= 2 * y2[z] - prob += (1 - d[p]) + (1 - d[v_p]) >= 2 * y3[z] - prob += d[p] + (1 - d[v_p]) >= 2 * y4[z] - - # Linearization of a_p with y1 - y4 -> linear combination of angle and orientations - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - prob += a_p[z] + y1[z] >= 2 * ay1[z] - prob += a_p[z] + y2[z] >= 2 * ay2[z] - prob += a_p[z] + y3[z] >= 2 * ay3[z] - prob += a_p[z] + y4[z] >= 2 * ay4[z] - - # Linearization of x * ay -> linear combination of assignment and orientation/angle - xay1 = pulp.LpVariable.dicts('xay1', ((p, v) for p in plats_a for v in vecs), cat='Binary') - xay2 = pulp.LpVariable.dicts('xay2', ((p, v) for p in plats_a for v in vecs), cat='Binary') - xay3 = pulp.LpVariable.dicts('xay3', ((p, v) for p in plats_a for v in vecs), cat='Binary') - xay4 = pulp.LpVariable.dicts('xay4', ((p, v) for p in plats_a for v in vecs), cat='Binary') - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - for v in vecs: - prob += ay1[z] + x[z, v] >= 2 * xay1[z, v] - prob += ay2[z] + x[z, v] >= 2 * xay2[z, v] - prob += ay3[z] + x[z, v] >= 2 * xay3[z, v] - prob += ay4[z] + x[z, v] >= 2 * xay4[z, v] - - # Making sure always only 1 case applies - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - prob += ay1[z] + ay2[z] + ay3[z] + ay4[z] <= 1 - prob += y1[z] + y2[z] + y3[z] + y4[z] <= 1 - - # (4) Length constraint - # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length - # The length of the vehicles depends on whether they are angled or not and which vehicle is standing on - # platform v(p) - for L in plats_l: - prob += pulp.lpSum(x[p, v] * length_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][0]*length_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][1]*length_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][2]*length_list[v]) - - xay4[platforms_angled_array.index(p), v] - * int(v_coef[class_list[v]][3]*length_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) - for v in vecs)\ - + pulp.lpSum(x[p, v] * length_list[v] - for p in self.diffset(platforms_level_array[L], platforms_angled_array) - for v in vecs) \ - <= lmax_l[L] - - # (5) Platforms can not be angled, if they are part of a split platform - for q in plats_sp: - prob += pulp.lpSum(a_p[platforms_angled_array.index(p)] - for p in self.intersectset(platforms_angled_array, split_platforms_array[q]))\ - <= len(split_platforms_array[q]) * (1 - sp[q]) - - # (6) Weight constraint if split platform is used, gamma == 1 - for q in plats_sp: - prob += pulp.lpSum(sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) - for v in vecs) >= 2 * gamma[q] - - # If split platform is used, weight limit == wsp, if not, then weight limit == wp - for q in plats_sp: - for p in split_platforms_array[q]: - prob += (pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] + (1 - gamma[q]) * - wp[p]) - - # (7) If a platform that can be angled is angled, weight limit == wpa - # Need another linearization for that: - apx = pulp.LpVariable.dicts('apx', ((p, v) for p in plats_a for v in vecs), cat='Binary') - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - for v in vecs: - prob += a_p[z] + x[z, v] >= 2 * apx[z, v] - - for p in platforms_angled_array: - prob += pulp.lpSum(weight_list[v] * apx[platforms_angled_array.index(p), v] for v in vecs) \ - <= wpa[platforms_angled_array.index(p)] - - # (8) Weight constraint for every level - for p_l in plats_l: - prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= - wl[p_l]) - - # (9) Weight constraint for truck and trailer - for p_t in plats_t: - prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) - <= wt[p_t]) - - # (10) Weight constraint for entire auto carrier - prob += pulp.lpSum(weight_list[v] * x[p, v] for p in plats for v in vecs) <= wmax - - # (11) Height constraints for truck and trailer, analogue to length constraints - # Truck - for h in plats_h1: - prob += pulp.lpSum(x[p, v] * height_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][0]*height_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][1]*height_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][2]*height_list[v]) - - xay4[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][3]*height_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) - for v in vecs)\ - + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) - for v in vecs) \ - <= hmax_truck[h] - # Trailer - for h in plats_h2: - prob += pulp.lpSum(x[p, v] * height_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][0]*height_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][1]*height_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][2]*height_list[v]) - - xay4[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][3]*height_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) - for v in vecs)\ - + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) - for v in vecs) \ - <= hmax_trailer[h] - - # Set the problem sense and name - problem_instance = prob.to_dict() - self.application = problem_instance - return self.application + prob += ay1[z] + x[z, v] >= 2 * xay1[z, v] + prob += ay2[z] + x[z, v] >= 2 * xay2[z, v] + prob += ay3[z] + x[z, v] >= 2 * xay3[z, v] + prob += ay4[z] + x[z, v] >= 2 * xay4[z, v] + + # Making sure always only 1 case applies + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + prob += ay1[z] + ay2[z] + ay3[z] + ay4[z] <= 1 + prob += y1[z] + y2[z] + y3[z] + y4[z] <= 1 + + # (4) Length constraint + # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length + # The length of the vehicles depends on whether they are angled or not and which vehicle is standing on + # platform v(p) + for L in plats_l: + prob += pulp.lpSum(x[p, v] * length_list[v] + - xay1[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][0]*length_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][1]*length_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][2]*length_list[v]) + - xay4[platforms_angled_array.index(p), v] + * int(v_coef[class_list[v]][3]*length_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) + for v in vecs)\ + + pulp.lpSum(x[p, v] * length_list[v] + for p in self.diffset(platforms_level_array[L], platforms_angled_array) + for v in vecs) \ + <= lmax_l[L] + + # (5) Platforms can not be angled, if they are part of a split platform + for q in plats_sp: + prob += pulp.lpSum(a_p[platforms_angled_array.index(p)] + for p in self.intersectset(platforms_angled_array, split_platforms_array[q]))\ + <= len(split_platforms_array[q]) * (1 - sp[q]) + + # (6) Weight constraint if split platform is used, gamma == 1 + for q in plats_sp: + prob += pulp.lpSum(sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) + for v in vecs) >= 2 * gamma[q] + + # If split platform is used, weight limit == wsp, if not, then weight limit == wp + for q in plats_sp: + for p in split_platforms_array[q]: + prob += (pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] + (1 - gamma[q]) * + wp[p]) + + # (7) If a platform that can be angled is angled, weight limit == wpa + # Need another linearization for that: + apx = pulp.LpVariable.dicts('apx', ((p, v) for p in plats_a for v in vecs), cat='Binary') + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + for v in vecs: + prob += a_p[z] + x[z, v] >= 2 * apx[z, v] + + for p in platforms_angled_array: + prob += pulp.lpSum(weight_list[v] * apx[platforms_angled_array.index(p), v] for v in vecs) \ + <= wpa[platforms_angled_array.index(p)] + + # (8) Weight constraint for every level + for p_l in plats_l: + prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= + wl[p_l]) + + # (9) Weight constraint for truck and trailer + for p_t in plats_t: + prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) + <= wt[p_t]) + + # (10) Weight constraint for entire auto carrier + prob += pulp.lpSum(weight_list[v] * x[p, v] for p in plats for v in vecs) <= wmax + + # (11) Height constraints for truck and trailer, analogue to length constraints + # Truck + for h in plats_h1: + prob += pulp.lpSum(x[p, v] * height_list[v] + - xay1[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][0]*height_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][1]*height_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][2]*height_list[v]) + - xay4[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][3]*height_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) + for v in vecs)\ + + pulp.lpSum(x[p, v] * height_list[v] + for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) + for v in vecs) \ + <= hmax_truck[h] + # Trailer + for h in plats_h2: + prob += pulp.lpSum(x[p, v] * height_list[v] + - xay1[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][0]*height_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][1]*height_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][2]*height_list[v]) + - xay4[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][3]*height_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) + for v in vecs)\ + + pulp.lpSum(x[p, v] * height_list[v] + for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) + for v in vecs) \ + <= hmax_trailer[h] + + + self.application = prob + + def _get_vehicle_params(self, df, vehicles): + """ + Extract vehicle parameters for the problem formulation - def validate(self, solution:any) -> (bool, float): + :param df : Dataframe containing vehicle data + :param vehicles: List of vehicle types to consider + :return: Lists containing class, length, height, and weight of vehicles + """ + class_list = [0] * (len(vehicles)) + length_list = [0] * (len(vehicles)) + height_list = [0] * (len(vehicles)) + weight_list = [0] * (len(vehicles)) + + for i in set(range(len(vehicles))): + df_new = df.loc[df['Type'] == vehicles[i]] + class_list[i] = int(df_new["Class"].iloc[0]) + length_list[i] = int(df_new["Length"].iloc[0]) + height_list[i] = int(df_new["Height"].iloc[0]) + weight_list[i] = int(df_new["Weight"].iloc[0]) + + return class_list, length_list, height_list, weight_list + + def validate(self, solution: Any) -> Tuple[bool, float]: """ Checks if the solution is a valid solution : :param solution: Proposed solution - :type solution: any - :return: bool value if solution is valid and the time it took to validate the solution - :rtype: tuple(bool, float) + :return: Tuple containing a boolean indicating if the solution is valid + and the time it took to validate the solution """ - start = start_time_measurement() - status = solution["status"] - if status == 'Optimal': - return True, end_time_measurement(start) - else: - return False, end_time_measurement(start) + status = solution.get("status") + is_valid = status == "Optimal" + return is_valid, end_time_measurement(start) def get_solution_quality_unit(self) -> str: + """ + Provides the unit of measure for solution quality. + + :return: The unit of measure fro solution quality + """ return "Number of loaded vehicles" - def evaluate(self, solution: any) -> (float, float): + def evaluate(self, solution: Any) -> Tuple[float, float]: """ - Checks how good the solution is + Checks how good the solution is. :param solution: Provided solution - :type solution: any - :return: Evaluation and the time it took to create it - :rtype: tuple(any, float) - + :return: Tuple containing the objective value and the time it took to evaluate the solution """ start = start_time_measurement() - objective_value = solution["obj_value"] + objective_value = solution.get("obj_value", 0) logging.info("Loading successful!") logging.info(f"{objective_value} cars will fit on the auto carrier.") - variables = solution["variables"] - assignments = [] - # Check which decision variables are equal to 1 - for key in variables: - if variables[key] > 0: - assignments.append(key) + + variables = solution.get("variables", {}) + assignments = [key for key in variables if variables[key] > 0] + logging.info(f"vehicle-to-platform assignments (platform, vehicle): {assignments}") return objective_value, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the problem instance to a JSON file. + + :param path: Directory path where the instance should be saved + :param iter_count: Iteration count (unused) + """ # Convert our problem instance from Dict to an LP problem and then to json _, problem_instance = pulp.LpProblem.from_dict(self.application) # Save problem instance to json diff --git a/src/modules/applications/optimization/ACL/__init__.py b/src/modules/applications/optimization/ACL/__init__.py index b9f77189..2b03c28f 100644 --- a/src/modules/applications/optimization/ACL/__init__.py +++ b/src/modules/applications/optimization/ACL/__init__.py @@ -12,4 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing the ACL""" +""" +Module containing the ACL + +This module initializes the ACL application, which is responsible for formulating +and solving the ACL problem using various mappings and solvers. +""" diff --git a/src/modules/applications/optimization/ACL/mappings/ISING.py b/src/modules/applications/optimization/ACL/mappings/ISING.py index d8f5bbee..91dc449e 100644 --- a/src/modules/applications/optimization/ACL/mappings/ISING.py +++ b/src/modules/applications/optimization/ACL/mappings/ISING.py @@ -11,27 +11,27 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import logging -from typing import TypedDict -from more_itertools import locate +from typing import TypedDict, Any, Tuple import numpy as np +from more_itertools import locate from qiskit_optimization import QuadraticProgram from qiskit_optimization.converters import QuadraticProgramToQubo -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping from utils import start_time_measurement, end_time_measurement class Ising(Mapping): """ Ising formulation of the auto-carrier loading (ACL) problem. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "QiskitQAOA"] @@ -45,32 +45,20 @@ def get_requirements() -> list[dict]: Return requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "more-itertools", - "version": "10.5.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, + {"name": "numpy", "version": "1.26.4"}, + {"name": "more-itertools", "version": "10.5.0"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, ] - def get_parameter_options(self): + def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. :return: empty dict - :rtype: dict """ - return { - } + return {} class Config(TypedDict): """ @@ -78,18 +66,17 @@ class Config(TypedDict): """ pass - def map_pulp_to_qiskit(self, problem: any): + def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: """ Maps the problem dict to a quadratic program. :param problem: Problem formulation in dict form - :type problem: dict :return: quadratic program in qiskit-optimization format - :rtype: QuadraticProgram """ # Details at: # https://coin-or.github.io/pulp/guides/how_to_export_models.html # https://qiskit.org/documentation/stable/0.26/tutorials/optimization/2_converters_for_quadratic_programs.html + qp = QuadraticProgram() # Variables @@ -106,11 +93,7 @@ def map_pulp_to_qiskit(self, problem: any): qp.integer_var(lowerbound=lb, upperbound=ub, name=name) # Objective function - # Arguments: - obj_arguments = {} - for arg in problem["objective"]["coefficients"]: - obj_arguments[arg["name"]] = arg["value"] - + obj_arguments = {arg["name"]: arg["value"] for arg in problem["objective"]["coefficients"]} # Maximize if problem["parameters"]["sense"] == -1: qp.maximize(linear=obj_arguments) @@ -120,92 +103,85 @@ def map_pulp_to_qiskit(self, problem: any): # Constraints for constraint in problem["constraints"]: - const_arguments = {} - for arg in constraint["coefficients"]: - const_arguments[arg["name"]] = arg["value"] + const_arguments = {arg["name"]: arg["value"] for arg in constraint["coefficients"]} sense = constraint["sense"] - if sense == -1: - const_sense = "LE" - elif sense == 1: - const_sense = "GE" - else: - const_sense = "E" - qp.linear_constraint(linear=const_arguments, sense=const_sense, rhs=-1 * constraint["constant"], - name=constraint["name"]) + const_sense = "LE" if sense == -1 else "GE" if sense == 1 else "E" + qp.linear_constraint( + linear=const_arguments, + sense=const_sense, + rhs=-1 * constraint["constant"], + name=constraint["name"] + ) + return qp - def map(self, problem: any, config: Config) -> (dict, float): + def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ - Use Ising mapping of qiskit-optimize + Use Ising mapping of qiskit-optimize. + :param config: config with the parameters specified in Config class - :type config: Config :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() # Map Linear problem from dictionary (generated by pulp) to quadratic program qp = self.map_pulp_to_qiskit(problem) - # print(qp.prettyprint()) logging.info(qp.export_as_lp_string()) # convert quadratic problem to qubo to ising conv = QuadraticProgramToQubo() qubo = conv.convert(qp) - # get variables - variables = [] - for variable in qubo.variables: - variables.append(variable.name) - qubitOp, _ = qubo.to_ising() + variables = [variable.name for variable in qubo.variables] + qubit_op, _ = qubo.to_ising() self.global_variables = variables # reverse generate J and t out of qubit PauliSumOperator from qiskit - t_matrix = np.zeros(qubitOp.num_qubits, dtype=complex) - j_matrix = np.zeros((qubitOp.num_qubits, qubitOp.num_qubits), dtype=complex) - - for i in qubitOp: - pauli_str, coeff = i.primitive.to_list()[0] - logging.info((pauli_str, coeff)) - pauli_str_list = list(pauli_str) - index_pos_list = list(locate(pauli_str_list, lambda a: a == 'Z')) + t_matrix = np.zeros(qubit_op.num_qubits, dtype=complex) + j_matrix = np.zeros((qubit_op.num_qubits, qubit_op.num_qubits), dtype=complex) + + for pauli_op in qubit_op: + pauli_str, coeff = pauli_op.primitive.to_list()[0] + index_pos_list = list(locate(pauli_str, lambda a: a == 'Z')) + if len(index_pos_list) == 1: - # update t t_matrix[index_pos_list[0]] = coeff elif len(index_pos_list) == 2: j_matrix[index_pos_list[0]][index_pos_list[1]] = coeff return {"J": j_matrix, "t": t_matrix}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> Tuple(dict, float): """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. :param solution: bit_string containing the solution - :type solution: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() - if np.any(solution == "-1"): # ising model output from Braket QAOA + + if np.any(solution == "-1"): solution = self._convert_ising_to_qubo(solution) + result = {"status": [0]} variables = {} objective_value = 0 + for bit in solution: if solution[bit] > 0: - # We only care about assignments: if "x" in self.global_variables[bit]: variables[self.global_variables[bit]] = solution[bit] result["status"] = 'Optimal' objective_value += solution[bit] + result["variables"] = variables result["obj_value"] = objective_value + return result, end_time_measurement(start) @staticmethod - def _convert_ising_to_qubo(solution: any) -> any: + def _convert_ising_to_qubo(solution: Any) -> Any: solution = np.array(solution) with np.nditer(solution, op_flags=['readwrite']) as it: for x in it: diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index 6546ae37..c96a8b62 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -12,27 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Any, List, Tuple import re +import logging import numpy as np from qiskit_optimization import QuadraticProgram -from qiskit_optimization.converters import (QuadraticProgramToQubo, InequalityToEquality, IntegerToBinary, - LinearEqualityToPenalty) +from qiskit_optimization.converters import ( + QuadraticProgramToQubo, InequalityToEquality, IntegerToBinary, + LinearEqualityToPenalty +) -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement class Qubo(Mapping): """ QUBO formulation for the ACL. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -41,31 +43,22 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, + {"name": "numpy", "version": "1.26.4"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, ] - def get_parameter_options(self): + def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. :return: empty dictionary - :rtype: dict """ - return { - } + return {} class Config(TypedDict): """ @@ -73,18 +66,17 @@ class Config(TypedDict): """ pass - def map_pulp_to_qiskit(self, problem: any): + def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: """ Maps the problem dict to a quadratic program. :param problem: Problem formulation in dict form - :type problem: dict :return: quadratic program in qiskit-optimization format - :rtype: QuadraticProgram """ # Details at: # https://coin-or.github.io/pulp/guides/how_to_export_models.html # https://qiskit.org/documentation/stable/0.26/tutorials/optimization/2_converters_for_quadratic_programs.html + qp = QuadraticProgram() # Variables @@ -101,10 +93,7 @@ def map_pulp_to_qiskit(self, problem: any): qp.integer_var(lowerbound=lb, upperbound=ub, name=name) # Objective function - # Arguments: - obj_arguments = {} - for arg in problem["objective"]["coefficients"]: - obj_arguments[arg["name"]] = arg["value"] + obj_arguments = {arg["name"]: arg["value"] for arg in problem["objective"]["coefficients"] } # Maximize if problem["parameters"]["sense"] == -1: @@ -115,37 +104,35 @@ def map_pulp_to_qiskit(self, problem: any): # Constraints for constraint in problem["constraints"]: - const_arguments = {} - for arg in constraint["coefficients"]: - const_arguments[arg["name"]] = arg["value"] + const_arguments = {arg["name"]: arg["value"] for arg in constraint["coefficients"]} sense = constraint["sense"] - if sense == -1: - const_sense = "LE" - elif sense == 1: - const_sense = "GE" - else: - const_sense = "E" - qp.linear_constraint(linear=const_arguments, sense=const_sense, rhs=-1 * constraint["constant"], - name=constraint["name"]) + const_sense = "LE" if sense == -1 else "GE" if sense == 1 else "E" + + qp.linear_constraint( + linear=const_arguments, + sense=const_sense, + rhs=-1 * constraint["constant"], + name=constraint["name"] + ) + return qp - def convert_string_to_arguments(self, input_string: str): + def convert_string_to_arguments(self, input_string: str) -> List[Any]: """ - Converts QUBO in string format to a list of separated arguments, used to construct the QUBO matrix. + Converts QUBO in string format to a list of separated arguments, + used to construct the QUBO matrix. :param input_string: QUBO in raw string format - :type input_string: str :return: list of arguments - :rtype: list """ terms = re.findall(r'[+\-]?[^+\-]+', input_string) # Convert the penalty string to a list of lists of the individual arguments in the penalty term result = [term.strip() for term in terms] separated_arguments = [] first_item = True - # Loop over all arguments in the penalty + for argument in result: - if first_item is True: + if first_item: # Remove "maximize" or minimize string from the first argument argument = argument[8:] first_item = False @@ -155,25 +142,22 @@ def convert_string_to_arguments(self, input_string: str): # Convert string of numbers to floats new_argument = elements[0].strip() # Remove empty strings - new_argument = [int(new_argument.replace(" ", "")) if new_argument.replace(" ", "").isdigit() else - float(new_argument.replace(" ", ""))] - for el in elements[1:]: - new_argument += [el.strip()] + new_argument = [int(new_argument.replace(" ", "")) if new_argument.replace(" ", "").isdigit() + else float(new_argument.replace(" ", ""))] + new_argument += [el.strip() for el in elements[1:]] separated_arguments.append(new_argument) else: separated_arguments.append(argument) + return separated_arguments - def construct_qubo(self, penalty: list[list], variables: list): + def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarray: """ - Creates QUBO matrix Q to solve linear problem of the form x^T * Q + x + Creates QUBO matrix Q to solve linear problem of the form x^T * Q + x. :param penalty: list of lists containing all non-zero elements of the QUBO matrix as strings - :type penalty: list :param variables: listing of all variables used in the problem - :type variables: list :return: QUBO in numpy array format - :rtype: array """ # Create empty qubo matrix count_variables = len(variables) @@ -184,8 +168,9 @@ def construct_qubo(self, penalty: list[list], variables: list): for row, variable2 in enumerate(variables): # Save the parameters (values in the qubo) parameter = 0 + for argument in penalty: - if type(argument) is list: + if isinstance(argument, list): # squared variables in diagonals (x^2 == x) if len(argument) == 2: if any(isinstance(elem, str) and variable in elem for elem in argument) and col == row: @@ -195,65 +180,62 @@ def construct_qubo(self, penalty: list[list], variables: list): if variable in argument and variable2 in argument and variable > variable2: parameter += argument[0] # this value is already taking into account the factor 2 from quadratic term - # For the variables on the diagonal, if the parameter is zero, we still have to check the sign in - # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. - elif type(argument) is str: + # For the variables on the diagonal, if the parameter is zero, we still have to check the sign in + # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. + elif isinstance(argument, str): if variable in argument and variable2 in argument and variable == variable2: if "-" in argument: parameter += -1 + qubo[col, row] = parameter + # Minimization problem qubo = -qubo.astype(int) return qubo - def map(self, problem: any, config: Config) -> (dict, float): + def map(self, problem: dict, config: Config) -> Tuple[dict, float]: """ - Use Ising mapping of qiskit-optimize - Converts linear program created with pulp to quadratic program to Ising with qiskit to QUBO matrix + Converts linear program created with pulp to quadratic program to Ising with qiskit to QUBO matrix. :param config: config with the parameters specified in Config class - :type config: Config :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() # Map Linear problem from dictionary (generated by pulp) to quadratic program to QUBO qp = self.map_pulp_to_qiskit(problem) - # print(qp.prettyprint()) logging.info(qp.export_as_lp_string()) + ineq2eq = InequalityToEquality() qp_eq = ineq2eq.convert(qp) + int2bin = IntegerToBinary() qp_eq_bin = int2bin.convert(qp_eq) + lineq2penalty = LinearEqualityToPenalty(100) qubo = lineq2penalty.convert(qp_eq_bin) - # get variables - variables = [] - for variable in qubo.variables: - variables.append(variable.name) + variables = [variable.name for variable in qubo.variables] # convert penalty term to string to QUBO qubo_string = str(qubo.objective) arguments = self.convert_string_to_arguments(qubo_string) - qubo = self.construct_qubo(arguments, variables) + qubo_matrix = self.construct_qubo(arguments, variables) self.global_variables = variables - return {"Q": qubo}, end_time_measurement(start) + return {"Q": qubo_matrix}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> Tuple[dict, float]: """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. :param solution: bit_string containing the solution - :type solution: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() + result = {"status": [0]} objective_value = 0 variables = {} @@ -265,8 +247,10 @@ def reverse_map(self, solution: dict) -> (dict, float): variables[self.global_variables[bit]] = solution[bit] result["status"] = 'Optimal' # TODO: I do not think every solution with at least one car is optimal objective_value += solution[bit] + result["variables"] = variables result["obj_value"] = objective_value + return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: diff --git a/src/modules/applications/optimization/ACL/mappings/__init__.py b/src/modules/applications/optimization/ACL/mappings/__init__.py index b38557a3..d7364160 100644 --- a/src/modules/applications/optimization/ACL/mappings/__init__.py +++ b/src/modules/applications/optimization/ACL/mappings/__init__.py @@ -12,4 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for ACL mappings""" +""" +Module for ACL mappings. + +This module provides initializations for ACL related +mappings that are used in the QUARK framework. +""" diff --git a/src/modules/applications/optimization/MIS/MIS.py b/src/modules/applications/optimization/MIS/MIS.py index dbdf90e8..4b5c8f00 100644 --- a/src/modules/applications/optimization/MIS/MIS.py +++ b/src/modules/applications/optimization/MIS/MIS.py @@ -12,15 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Tuple, Dict import pickle +import logging -import networkx +import networkx as nx -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization -from modules.applications.optimization.MIS.data.graph_layouts import \ - generate_hexagonal_graph +from modules.applications.optimization.MIS.data.graph_layouts import generate_hexagonal_graph from utils import start_time_measurement, end_time_measurement # define R_rydberg @@ -29,8 +29,6 @@ class MIS(Optimization): """ - In planning problems, there will be tasks to be done, and some of them may be mutually exclusive. - We can translate this into a graph where the nodes are the tasks and the edges are the mutual exclusions. The maximum independent set (MIS) problem is a combinatorial optimization problem that seeks to find the largest subset of vertices in a graph such that no two vertices are adjacent. """ @@ -45,25 +43,35 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - ] + return [] def get_solution_quality_unit(self) -> str: + """ + Returns the unit of measurement for solution quality. + + :return: The unit of measure for solution quality + """ return "Set size" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + + :param option: Submodule option to retrieve + :return: Corresponding submodule object + :raises NotImplementedError: If the option is not implemented + """ if option == "NeutralAtom": from modules.applications.optimization.MIS.mappings.NeutralAtom import NeutralAtom # pylint: disable=C0415 return NeutralAtom() else: raise NotImplementedError(f"Mapping Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this application @@ -96,15 +104,14 @@ def get_parameter_options(self) -> dict: "description": "How large should your graph be?" }, "spacing": { - "values": [x/10 for x in range(3, 11, 2)], + "values": [x / 10 for x in range(3, 11, 2)], "custom_input": True, "allow_ranges": True, "postproc": float, - "description": "How much space do you want between your nodes," - " relative to Rydberg distance?" + "description": "How much space do you want between your nodes,relative to Rydberg distance?" }, "filling_fraction": { - "values": [x/10 for x in range(2, 11, 2)], + "values": [x / 10 for x in range(2, 11, 2)], "custom_input": True, "allow_ranges": True, "postproc": float, @@ -114,38 +121,31 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python + Configuration attributes for generating an MIS problem - size: int - spacing: float - filling_fraction: float + Attributes: + size (int): The number of nodes in the graph. + spacing (float): The spacing between nodes in the graph. + filling_fraction (float): The fraction of available places in the lattice filled with nodes """ size: int spacing: float filling_fraction: float - def generate_problem(self, config: Config) -> networkx.Graph: + def generate_problem(self, config: Config) -> nx.Graph: """ - Generates a graph to solve the MIS for. + Generates a graph to solve the MIS problem for. :param config: Config specifying the size and connectivity for the problem - :type config: Config :return: networkx graph representing the problem - :rtype: networkx.Graph """ - if config is None: - config = {"size": 3, - "spacing": 1, - "filling_fraction": 0.5} + config = {"size": 3, "spacing": 1, "filling_fraction": 0.5} - # check if config has the necessary information + # Ensure config has the necessary information assert all( - x in config.keys() - for x in ['size', 'spacing', 'filling_fraction'] + key in config for key in ['size', 'spacing', 'filling_fraction'] ) size = config.get('size') @@ -158,8 +158,7 @@ def generate_problem(self, config: Config) -> networkx.Graph: filling_fraction=filling_fraction, ) - logging.info("Created MIS problem with the generate hexagonal " - "graph method, with the following attributes:") + logging.info("Created MIS problem with the generate hexagonal graph method, with the following attributes:") logging.info(f" - Graph size: {size}") logging.info(f" - Spacing: {spacing}") logging.info(f" - Filling fraction: {filling_fraction}") @@ -167,27 +166,22 @@ def generate_problem(self, config: Config) -> networkx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: list) -> (list, float): + def process_solution(self, solution: List) -> Tuple[List, float]: """ Returns list of visited nodes and the time it took to process the solution :param solution: Unprocessed solution - :type solution: list :return: Processed solution and the time it took to process it - :rtype: tuple(list, float) """ start_time = start_time_measurement() - return solution, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: List) -> Tuple[bool, float]: """ Checks if the solution is an independent set :param solution: List containing the nodes of the solution - :type solution: list :return: Boolean whether the solution is valid and time it took to validate - :rtype: tuple(bool, float) """ start = start_time_measurement() is_valid = True @@ -215,8 +209,8 @@ def validate(self, solution: list) -> (bool, float): is_valid = False # Check if the solution is a subset of the original nodes - is_set = all(node in nodes for node in solution) - if is_set: + is_subset = all(node in nodes for node in solution) + if is_subset: logging.info("The solution is a subset of the problem") else: logging.warning("The solution is not a subset of the problem") @@ -224,14 +218,12 @@ def validate(self, solution: list) -> (bool, float): return is_valid, end_time_measurement(start) - def evaluate(self, solution: list) -> (int, float): + def evaluate(self, solution: List) -> Tuple[int, float]: """ Calculates the size of the solution :param solution: List containing the nodes of the solution - :type solution: list :return: Set size, time it took to calculate the set size - :rtype: tuple(int, float) """ start = start_time_measurement() set_size = len(solution) @@ -241,5 +233,11 @@ def evaluate(self, solution: list) -> (int, float): return set_size, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the generated problem graph to a file. + + :param path: Path to save the problem graph + :param iter_count: Iteration count for file versioning + """ with open(f"{path}/graph_iter_{iter_count}.gpickle", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/MIS/__init__.py b/src/modules/applications/optimization/MIS/__init__.py index e808d010..ec0e95aa 100644 --- a/src/modules/applications/optimization/MIS/__init__.py +++ b/src/modules/applications/optimization/MIS/__init__.py @@ -12,4 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for MIS""" +""" +Module for MIS mappings + +This module initialize the MIS package +""" + diff --git a/src/modules/applications/optimization/MIS/data/__init__.py b/src/modules/applications/optimization/MIS/data/__init__.py index 310100cb..1afbe94e 100644 --- a/src/modules/applications/optimization/MIS/data/__init__.py +++ b/src/modules/applications/optimization/MIS/data/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for MIS data""" +""" +Module for MIS data + +This module initialize the MIS package +""" diff --git a/src/modules/applications/optimization/MIS/data/graph_layouts.py b/src/modules/applications/optimization/MIS/data/graph_layouts.py index 44695408..79de4ac9 100644 --- a/src/modules/applications/optimization/MIS/data/graph_layouts.py +++ b/src/modules/applications/optimization/MIS/data/graph_layouts.py @@ -14,64 +14,62 @@ import math import random +from typing import Dict, List, Tuple -import networkx +import networkx as nx import pulser # define R_rydberg R_rydberg = 9.75 -def generate_hexagonal_graph(n_nodes:int, spacing:float, - filling_fraction:float=1.0) -> networkx.Graph: +def generate_hexagonal_graph( + n_nodes: int, + spacing: float, + filling_fraction: float=1.0 +)-> nx.Graph: """ - Generate a hexagonal graph layout based on the number of atoms and spacing. + Generate a hexagonal graph layout based on the number of nodes and spacing. Args: - n (int): The number of nodes in the graph. - spacing (float): The spacing between atoms. + n_nodes (int): The number of nodes in the graph. + spacing (float): The spacing between nodes (atoms). filling_fraction (float): The fraction of available places in the - lattice to be filled with atoms. (default: 1.0) + lattice to be filled with nodes. (default: 1.0) Returns: - Graph: networkx Graph representing the hexagonal graph layout. + nx.Graph: networkx Graph representing the hexagonal graph layout. """ - if filling_fraction > 1.0 or filling_fraction <= 0.0: - raise ValueError( - "The filling fraction must be in the domain of (0.0, 1.0]." - ) + if not (0.0 < filling_fraction <= 1.0): + raise ValueError("The filling fraction must be in the domain of (0.0, 1.0].") # Create a layout large enough to contain the desired number of atoms at # the filling fraction - n_traps = int(n_nodes/filling_fraction) + n_traps = int(n_nodes / filling_fraction) hexagonal_layout = pulser.register.special_layouts.TriangularLatticeLayout( - n_traps=n_traps, spacing=spacing) + n_traps=n_traps, spacing=spacing + ) # Fill the layout with traps reg = hexagonal_layout.hexagonal_register(n_traps) ids = reg._ids # pylint: disable=W0212 - coords = reg._coords # pylint: disable=W0212 - coords = [l.tolist() for l in coords] + coords = [coord.tolist() for coord in reg._coords] # pylint: disable=W0212 traps = dict(zip(ids, coords)) # Remove random atoms to get the desired number of atoms - # This is needed if the filling fraction is below 1.0 while len(traps) > n_nodes: atom_to_remove = random.choice(list(traps)) traps.pop(atom_to_remove) # Rename the atoms i = 0 - node_positions = {} - for trap in traps.keys(): # pylint: disable=C0206 - node_positions[i] = traps[trap] - i += 1 + node_positions = {i: traps[trap] for i, trap in enumerate(traps.keys())} # pylint: disable=C0206 # Create the graph - hexagonal_graph = networkx.Graph() + hexagonal_graph = nx.Graph() - # Add the nodes - for ID, coord in node_positions.items(): - hexagonal_graph.add_node(ID, pos=coord) + # Add nodes to the graph + for node_id, coord in node_positions.items(): + hexagonal_graph.add_node(node_id, pos=coord) # Generate the edges and add them to the graph edges = _generate_edges(node_positions=node_positions) @@ -80,43 +78,41 @@ def generate_hexagonal_graph(n_nodes:int, spacing:float, return hexagonal_graph def _generate_edges( - node_positions: dict, - radius: float = R_rydberg, - ) -> list[tuple]: + node_positions: Dict[int, List[float]], + radius: float = R_rydberg +) -> list[tuple[int, int]]: """Generate edges between vertices within a given distance 'radius', which defaults to R_rydberg. - Parameters - ---------- - node_positions: dict - A dictionary with the node ids as keys, and the node coordinates as - value. - radius: float - When the distance between two nodes is smaller than this radius, an - edge is generated between them. + Parameters: + node_positions (dict): A dictionary with the node ids as keys, and the node coordinates as values. + radius (float): When the distance between two nodes is smaller than this radius, an edge is generated between them. - Returns - ------- - edges: list[tuple] - A list of 2-tuples. Each 2-tuple contains two different node ids and - represents an edge between those two nodes. + Returns: + list[tuple]: A list of 2-tuples. Each 2-tuple contains two different node ids and represents an edge between those nodes. """ edges = [] vertex_keys = list(node_positions.keys()) for i, vertex_key in enumerate(vertex_keys): - for neighbor_key in vertex_keys[i+1:]: - distance = _vertex_distance(node_positions[vertex_key], - node_positions[neighbor_key]) + for neighbor_key in vertex_keys[i + 1:]: + distance = _vertex_distance( + node_positions[vertex_key], node_positions[neighbor_key] + ) if distance <= radius: edges.append((vertex_key, neighbor_key)) return edges -def _vertex_distance(v0: tuple, v1: tuple) -> float: +def _vertex_distance(v0: Tuple[float, ...], v1: Tuple[float, ...]) -> float: """ Calculates distance between two n-dimensional vertices. - For 2 dimensions: distance = sqrt((x0-x1)**2 + (y0-y1)**2) + For 2 dimensions: distance = sqrt((x0 - x1)**2 + (y0 - y1)**2) + + Args: + v0 (tuple): Coordinates of the first vertex. + v1 (tuple): Coordinates of the second vertex. + + Returns: + float: Distance between the vertices. """ - squared_difference = 0 - for coordinate0, coordinate1 in zip(v0, v1): - squared_difference += (coordinate0 -coordinate1)**2 + squared_difference = sum((coordinate0 - coordinate1) ** 2 for coordinate0, coordinate1 in zip(v0, v1)) return math.sqrt(squared_difference) diff --git a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py index 6dedc239..b2009a8d 100644 --- a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py +++ b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py @@ -12,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Dict, Tuple -import networkx -import numpy as np +import networkx as nx import pulser -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -37,53 +36,37 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: list of requirements of this module """ - return [ - { - "name": "pulser", - "version": "0.19.0" - } - ] + return [{"name": "pulser", "version": "0.19.0"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return {} + Returns the configurable settings for this mapping. + :return: Empty dictionary, as this mapping has no configurable settings """ return {} class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - pass + Configuration options for Neutral Atom MIS mapping """ pass - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Maps the networkx graph to a neutral atom MIS problem. - :param problem: networkx graph - :type problem: networkx.Graph + :param problem: Networkx graph representing the MIS problem :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with neutral MIS, time it took to map it - :rtype: tuple(dict, float) + :return: Tuple containing a dictionary with the neutral MIS and time it took to map it """ start = start_time_measurement() - pos = networkx.get_node_attributes(problem, 'pos') + pos = nx.get_node_attributes(problem, 'pos') register = pulser.Register(pos) neutral_atom_problem = { @@ -93,7 +76,13 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): return neutral_atom_problem, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + :param option: Submodule option to retrieve + :return: Corresponding submodule object + :raises NotImplementedError: If the option is not implemented + """ if option == "NeutralAtomMIS": from modules.solvers.NeutralAtomMIS import NeutralAtomMIS # pylint: disable=C0415 return NeutralAtomMIS() diff --git a/src/modules/applications/optimization/MIS/mappings/__init__.py b/src/modules/applications/optimization/MIS/mappings/__init__.py index a701f7ff..b7ca66cc 100644 --- a/src/modules/applications/optimization/MIS/mappings/__init__.py +++ b/src/modules/applications/optimization/MIS/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for MIS mappings""" +""" +Module for MIS mappings + +This module initialize the MIS package +""" diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index abe3c7b8..8583d97e 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -13,13 +13,15 @@ # limitations under the License. import itertools -from typing import TypedDict +from typing import TypedDict, List, Tuple import pickle +import logging +import os import networkx as nx import numpy as np -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement @@ -47,29 +49,28 @@ def __init__(self): self.submodule_options = ["Ising", "QUBO", "GreedyClassicalPVC", "ReverseGreedyClassicalPVC", "RandomPVC"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_solution_quality_unit(self) -> str: return "Tour cost" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + :param option: The submodule option to retrieve + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented + """ if option == "Ising": from modules.applications.optimization.PVC.mappings.ISING import Ising # pylint: disable=C0415 return Ising() @@ -90,15 +91,14 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. - :return: + :return: Dictionary containing parameter options .. code-block:: python return { "seams": { "values": list(range(1, 18)), - # Currently the graph can only be as large as the reference input graph "description": "How many seams does your graph need?" } } @@ -114,25 +114,20 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - - seams: int + Configuration attributes for PVC problem generation. + Attributes: + seams (int): Number of seams for the graph """ seams: int def generate_problem(self, config: Config) -> nx.Graph: """ - Uses the reference graph to generate a problem for a given config + Uses the reference graph to generate a problem for a given config. :param config: Config specifying the number of seams for the problem - :type config: Config :return: networkx graph representing the problem - :rtype: networkx.Graph """ - if config is None: config = {"seams": 3} seams = config['seams'] @@ -141,19 +136,17 @@ def generate_problem(self, config: Config) -> nx.Graph: with open(os.path.join(os.path.dirname(__file__), "data", "reference_graph.gpickle"), "rb") as file: graph = pickle.load(file) - # Remove seams until the target number of seams is reached # Get number of seam in graph seams_in_graph = list({x[0] for x in graph.nodes}) seams_in_graph.sort() - # Remove 0 as we always need the base node 0 (which is not a seam anyway) - seams_in_graph.remove(0) + seams_in_graph.remove(0) # always need the base node 0 (which is not a seam) if len(seams_in_graph) < seams: - raise ValueError("Too many seams! The original graph has less seams than that!") + logging.info("Too many seams! The original graph has less seams than that!") unwanted_seams = seams_in_graph[-len(seams_in_graph) + seams:] unwanted_nodes = [x for x in graph.nodes if x[0] in unwanted_seams] - # Remove one node after another + for node in unwanted_nodes: graph.remove_node(node) @@ -161,30 +154,25 @@ def generate_problem(self, config: Config) -> nx.Graph: logging.error("Graph is not connected!") raise ValueError("Graph is not connected!") + #Gather unique configurations and tools config = [x[2]['c_start'] for x in graph.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in graph.edges(data=True)])) - tool = [x[2]['t_start'] for x in graph.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in graph.edges(data=True)])) - # Now lets fill the rest of the missing edges with high values - # get current edges + # fill the rest of the missing edges with high values current_edges = [(edge[0], edge[1], edge[2]['t_start'], edge[2]['t_end'], edge[2]['c_start'], edge[2]['c_end']) for edge in graph.edges(data=True)] - - # get all possible edges all_possible_edges = list(itertools.product(list(graph.nodes), repeat=2)) all_possible_edges = [(edges[0], edges[1], t_start, t_end, c_start, c_end) for edges in all_possible_edges for c_end in config for c_start in config for t_end in tool for t_start in tool if edges[0] != edges[1]] - # calculate missing edges + missing_edges = [item for item in all_possible_edges if item not in current_edges] + # add these edges with very high values for edge in missing_edges: - weight = 100000 # TODO Check if this value is fine - # c_start, t_start, c_end, t_end - graph.add_edge(edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], - weight=weight) + graph.add_edge(edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], weight=100000) logging.info("Created PVC problem with the following attributes:") logging.info(f" - Number of seams: {seams}") @@ -194,24 +182,23 @@ def generate_problem(self, config: Config) -> nx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: dict) -> (list, bool): + def process_solution(self, solution: dict) -> tuple[List, bool]: """ - Converts dict to list of visited seams + Converts solution dictionary to list of visited seams. :param solution: Unprocessed solution - :type solution: dict :return: Processed solution and the time it took to process it - :rtype: tuple(list, bool) """ start_time = start_time_measurement() nodes = list(self.application.nodes()) start = ((0, 0), 1, 1) - # fill route with None values + route: list = [None] * int((len(self.application) - 1) / 2 + 1) visited_seams = [] - # get nodes from sample + if sum(value == 1 for value in solution.values()) > len(route): logging.warning("Result is longer than route! This might be problematic!") + # NOTE: Prevent duplicate node entries by enforcing only one occurrence per node along route for (node, config, tool, timestep), val in solution.items(): if val and (node[0] not in visited_seams): @@ -220,9 +207,8 @@ def process_solution(self, solution: dict) -> (list, bool): route[timestep] = (node, config, tool) visited_seams.append(node[0]) - # run heuristic replacing None values + # Fill missing values in the route if None in route: - # get not assigned nodes logging.info(f"Route until now is: {route}") nodes_unassigned = [(node, 1, 1) for node in nodes if node[0] not in visited_seams] nodes_unassigned = list(np.random.permutation(nodes_unassigned, dtype=object)) @@ -231,67 +217,63 @@ def process_solution(self, solution: dict) -> (list, bool): logging.info(nodes) for idx, node in enumerate(route): if node is None: - route[idx] = nodes_unassigned[0] - nodes_unassigned.remove(route[idx]) + route[idx] = nodes_unassigned.pop(0) # cycle solution to start at provided start location if start is not None and route[0] != start: - # rotate to put the start in front idx = route.index(start) route = route[idx:] + route[:idx] - # print route - parsed_route = ' ->\n'.join( - [f' Node {visit[0][1]} of Seam {visit[0][0]} using config {visit[1]} & tool {visit[2]}' for visit in route]) + parsed_route = ' ->\n'.join([f' Node {visit[0][1]} of Seam {visit[0][0]} using config {visit[1]} & tool {visit[2]}' for visit in route]) logging.info(f"Route found:\n{parsed_route}") + return route, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: list) -> tuple[bool, float]: """ - Checks if all seams and the home position are visited for a given solution + Checks if all seams and the home position are visited for a given solution. :param solution: List containing the nodes of the solution - :type solution: list :return: Boolean whether the solution is valid and time it took to validate - :rtype: tuple(bool, float) """ # Check if all seams are visited in route start = start_time_measurement() visited_seams = {seam[0][0] for seam in solution if seam is not None} if len(visited_seams) == len(solution): - logging.info( - f"All {len(solution) - 1} seams and the base node got visited (We only need to visit 1 node per seam)") + logging.info(f"All {len(solution) - 1} seams and the base node got visited (We only need to visit 1 node per seam)") return True, end_time_measurement(start) else: logging.error(f"Only {len(visited_seams) - 1} got visited") return False, end_time_measurement(start) - def evaluate(self, solution: list) -> (float, float): + def evaluate(self, solution: list) -> tuple[float, float]: """ Calculates the tour length for a given valid tour :param solution: List containing the nodes of the solution - :type solution: list - :return: Tour length, time it took to calculate the tour length - :rtype: tuple(float, float) + :return: Tour length, time it took to calculate the tour lengt """ start = start_time_measurement() + # get the total distance total_dist = 0 for idx, _ in enumerate(solution[:-1]): - edge = next(item for item in list(self.application[solution[idx][0]][solution[idx + 1][0]].values()) if - item["c_start"] == solution[idx][1] and item["t_start"] == solution[idx][2] and item[ - "c_end"] == solution[idx + 1][1] and item["t_end"] == solution[idx + 1][2]) + edge = next( + item for item in list(self.application[solution[idx][0]][solution[idx + 1][0]].values()) + if item["c_start"] == solution[idx][1] and item["t_start"] == solution[idx][2] and + item["c_end"] == solution[idx + 1][1] and item["t_end"] == solution[idx + 1][2] + ) dist = edge['weight'] total_dist += dist - logging.info(f"Total distance (without return): {total_dist}") # add distance between start and end point to complete cycle - return_edge = next(item for item in list(self.application[solution[0][0]][solution[-1][0]].values()) if - item["c_start"] == solution[0][1] and item["t_start"] == solution[0][2] and item[ - "c_end"] == solution[-1][1] and item["t_end"] == solution[-1][2]) + return_edge = next( + item for item in list(self.application[solution[0][0]][solution[-1][0]].values()) + if item["c_start"] == solution[0][1] and item["t_start"] == solution[0][2] and + item["c_end"] == solution[-1][1] and item["t_end"] == solution[-1][2] + ) return_distance = return_edge['weight'] logging.info(f"Distance between start and end: {return_distance}") @@ -302,5 +284,11 @@ def evaluate(self, solution: list) -> (float, float): return distance, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the generated problem graph to a file. + + :param path: Path to save the problem graph + :param iter_count: Iteration count for file versioning + """ with open(f"{path}/graph_iter_{iter_count}.gpickle", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/PVC/__init__.py b/src/modules/applications/optimization/PVC/__init__.py index e8853ea4..e9c96938 100644 --- a/src/modules/applications/optimization/PVC/__init__.py +++ b/src/modules/applications/optimization/PVC/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for PVC""" +""" +Module for PVC mappings. + +This module initializes the PVC mapping packages +""" diff --git a/src/modules/applications/optimization/PVC/data/createReferenceGraph.py b/src/modules/applications/optimization/PVC/data/createReferenceGraph.py index 097ae1a4..83a1ea5c 100644 --- a/src/modules/applications/optimization/PVC/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/PVC/data/createReferenceGraph.py @@ -15,31 +15,22 @@ import networkx as nx import pickle -# Read in the original graph +# Create the original graph as a MultiDiGraph graph = nx.MultiDiGraph() with open("reference_data.txt") as infile: for line in infile: - line_elements = line.split(" ") - + line_elements = line.split() + + # Print line elements for debugging purposes print(line_elements) - - r_start = int(line_elements[1]) - s_start = int(line_elements[2]) - n_start = int(line_elements[3]) - c_start = int(line_elements[4]) - t_start = int(line_elements[5]) - l_start = int(line_elements[6]) - - r_end = int(line_elements[8]) - s_end = int(line_elements[9]) - n_end = int(line_elements[10]) - c_end = int(line_elements[11]) - t_end = int(line_elements[12]) - l_end = int(line_elements[13]) - + + # Extract start and end attributes from line elements + r_start, s_start, n_start, c_start, t_start, l_start = map(int, line_elements[1:7]) + r_end, s_end, n_end, c_end, t_end, l_end = map(int, line_elements[8:14]) duration = float(line_elements[15]) + # Handle missing or invalid data with default values if s_start == -1: s_start = 0 t_start = 1 # TODO except of picking a hardcoded value here we should select 1 from the dataset itself @@ -52,12 +43,15 @@ n_start = 0 if n_end == -1: n_end = 0 - # c_start, t_start, c_end, t_end + # Reduce the number of tools and configurations for simplicity if c_end < 3 and c_start < 3 and t_start < 2 and t_end < 2: - # Let's reduce the number of tools and configs for now - graph.add_edge((s_start, n_start), (s_end, n_end), c_start=c_start, t_start=t_start, c_end=c_end, - t_end=t_end, weight=duration) + graph.add_edge( + (s_start, n_start), (s_end, n_end), + c_start=c_start, t_start=t_start, + c_end=c_end, t_end=t_end, weight=duration + ) +# Save the graph to a file in gpickle format with open("reference_graph.gpickle", "wb") as file: pickle.dump(graph, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index d98dca74..bd32e82e 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -12,60 +12,51 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Dict, Tuple +import logging -import networkx +import networkx as nx import numpy as np from dimod import qubo_to_ising -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.PVC.mappings.QUBO import QUBO from utils import start_time_measurement, end_time_measurement class Ising(Mapping): """ - Ising formulation for the PVC + Ising formulation for the PVC. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA"] self.key_mapping = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> list[Dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: List of dict with requirements of this module - :rtype: list[dict] + :return: List of dictionaries with requirements of this module """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, *QUBO.get_requirements() ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: Dictionary containing parameter options. .. code-block:: python return { @@ -85,46 +76,44 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - - lagrange_factor: float + Configuration attributes for Ising mapping. + Attributes: + lagrange_factor (float): Factor to multiply the Langrange. """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Uses the PVC QUBO formulation and converts it to an Ising - :param problem: networkx graph - :type problem: networkx.Graph - :param config: Dict with the mapping config - :type config: Config - :return: Dict with the ising and time it took to map it - :rtype: tuple(dict, float) + :param problem: Networkx graph representing the PVC problem + :param config: Config dictionary with the mapping configuration + :return: Tuple containing a dictionary with the ising problem and time it took to map it """ start = start_time_measurement() + + # Convert the PVC problem to QUBO qubo_mapping = QUBO() q, _ = qubo_mapping.map(problem, config) + + # Convert QUBO to ising using dimod t, j, _ = qubo_to_ising(q["Q"]) + # Extract unique configuration and tool attributes from the graph config = [x[2]['c_start'] for x in problem.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in problem.edges(data=True)])) - tool = [x[2]['t_start'] for x in problem.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in problem.edges(data=True)])) - # Convert Ising dict to matrix - timesteps = int((problem.number_of_nodes() - 1) / 2 + 1) # G.number_of_nodes() - + # Initialize J matrix and mapping + timesteps = int((problem.number_of_nodes() - 1) / 2 + 1) matrix_size = problem.number_of_nodes() * len(config) * len(tool) * timesteps j_matrix = np.zeros((matrix_size, matrix_size), dtype=float) - self.key_mapping = {} - index_counter = 0 + # Map J values to a matrix representation + index_counter = 0 for key, value in j.items(): if key[0] not in self.key_mapping: self.key_mapping[key[0]] = index_counter @@ -138,25 +127,28 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): return {"J": j_matrix, "t": np.array(list(t.values()))}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: """ Maps the solution back to the representation needed by the PVC class for validation/evaluation. :param solution: Dictionary containing the solution - :type solution: dict - :return: Solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :return: Tuple with the remapped solution and time it took to reverse map """ start = start_time_measurement() logging.info(f"Key Mapping: {self.key_mapping}") - result = {} - for key, value in self.key_mapping.items(): - result[key] = 1 if solution[value] == 1 else 0 + result = {key: 1 if solution[self.key_mapping[key]] == 1 else 0 for key in self.key_mapping} + return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + :param option: The submodule option to retrieve + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/PVC/mappings/QUBO.py b/src/modules/applications/optimization/PVC/mappings/QUBO.py index 05c062a2..c42e499d 100644 --- a/src/modules/applications/optimization/PVC/mappings/QUBO.py +++ b/src/modules/applications/optimization/PVC/mappings/QUBO.py @@ -14,23 +14,23 @@ import itertools from collections import defaultdict -from typing import TypedDict +from typing import TypedDict, Dict, Tuple +import logging -import networkx +import networkx as nx -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement class QUBO(Mapping): """ - QUBO formulation for the PVC - + QUBO formulation for the PVC. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -38,23 +38,17 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: list of dictionaries with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: Dictionary containing parameter options .. code-block:: python return { @@ -74,36 +68,30 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python + Configuration attributes of QUBO mapping. - lagrange_factor: float + Attributes: + lagrange_factor (float): Factor to multiply the Langrange. """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Maps the networkx graph to a QUBO formulation. - :param problem: a networkx graph - :type problem: networkx.Graph - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) + :param problem: Networkx graph representing the PVC problem + :param config: config dictionary with the mapping configuration + :return: Tuple containing the QUBO dictionary and the time it took to map it """ + # Inspired by https://dnx.readthedocs.io/en/latest/_modules/dwave_networkx/algorithms/tsp.html start = start_time_measurement() - lagrange = None lagrange_factor = config['lagrange_factor'] - weight = 'weight' - # Inspired by https://dnx.readthedocs.io/en/latest/_modules/dwave_networkx/algorithms/tsp.html + # Estimate lagrange if not provided n = problem.number_of_nodes() - # we only need this number of timesteps since we only need to visit 1 node per seam - # (plus we start and end at the base node) timesteps = int((n - 1) / 2 + 1) + # Let`s get the number of different configs and tools config = [x[2]['c_start'] for x in problem.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in problem.edges(data=True)])) @@ -111,25 +99,16 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): tool = [x[2]['t_start'] for x in problem.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in problem.edges(data=True)])) - if lagrange is None: - # If no lagrange parameter provided, set to 'average' tour length. - # Usually a good estimate for a lagrange parameter is between 75-150% - # of the objective function value, so we come up with an estimate for - # tour length and use that. - if problem.number_of_edges() > 0: - weights = [x[2]['weight'] for x in problem.edges(data=True)] - # At the moment we need to filter out the very high artificial values we added during generate_problem - # as this would mess up the lagrange - weights = list(filter(lambda a: a != max(weights), weights)) - lagrange = sum(weights) / len(weights) * timesteps - else: - lagrange = 2 - - lagrange = lagrange * lagrange_factor + if problem.number_of_edges() > 0: + weights = [x[2]['weight'] for x in problem.edges(data=True)] + weights = list(filter(lambda a: a != max(weights), weights)) + lagrange = sum(weights) / len(weights) * timesteps + else: + lagrange = 2 + lagrange *= lagrange_factor logging.info(f"Selected lagrange is: {lagrange}") - # some input checking if n in (1, 2) or len(problem.edges) < n * (n - 1) // 2: msg = "graph must be a complete graph with at least 3 nodes or empty" raise ValueError(msg) @@ -147,52 +126,36 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): for pos_1 in range(timesteps): # for number of timesteps for t_start in tool: for c_start in config: - q[((node, c_start, t_start, pos_1), - (node, c_start, t_start, - pos_1))] -= lagrange # lagrange # nodes to itself on the same timestep + q[((node, c_start, t_start, pos_1), (node, c_start, t_start, pos_1))] -= lagrange for t_end in tool: # for all configs and tools for c_end in config: if c_start != c_end or t_start != t_end: - q[((node, c_start, t_start, pos_1), - (node, c_end, t_end, pos_1))] += 1.0 * lagrange - for pos_2 in range(pos_1 + 1, - timesteps): # For each following timestep set value for u -> u + q[((node, c_start, t_start, pos_1), (node, c_end, t_end, pos_1))] += 1.0 * lagrange + for pos_2 in range(pos_1 + 1, timesteps): # penalize visiting same node again in another timestep - q[((node, c_start, t_start, pos_1), - (node, c_end, t_end, - pos_2))] += 2.0 * lagrange - + q[((node, c_start, t_start, pos_1), (node, c_end, t_end, pos_2))] += 2.0 * lagrange # penalize visiting other node of same seam if node != (0, 0): # (0,0) is the base node, it is not a seam # get the other nodes of the same seam - other_seam_nodes = [x for x in problem.nodes if x[0] == node[0] - and x[1] != node] + other_seam_nodes = [ + x for x in problem.nodes if x[0] == node[0] and x[1] != node + ] for other_seam_node in other_seam_nodes: # penalize visiting other node of same seam - q[((node, c_start, t_start, pos_1), - (other_seam_node, c_end, t_end, - pos_2))] += 2.0 * lagrange + q[((node, c_start, t_start, pos_1), (other_seam_node, c_end, t_end, pos_2))] += 2.0 * lagrange # Constraint to only visit a single node in a single timestep - for pos in range(timesteps): # for all timesteps - for node_1 in problem: # for all nodes + for pos in range(timesteps): + for node_1 in problem: for t_start in tool: for c_start in config: - q[((node_1, c_start, t_start, pos), - (node_1, c_start, t_start, pos))] -= lagrange + q[((node_1, c_start, t_start, pos), (node_1, c_start, t_start, pos))] -= lagrange for t_end in tool: for c_end in config: - # if c_start != c_end or t_start != t_end: - # Q[((node_1, c_start, t_start, pos), - # (node_1, c_end, t_end, pos))] += lagrange for node_2 in set(problem) - {node_1}: # for all nodes except node1 -> node1 - # QUBO coefficient is 2*lagrange, but we are placing this value - # above *and* below the diagonal, so we put half in each position. - # penalize from node1 -> node2 in the same timestep - q[((node_1, c_start, t_start, pos), (node_2, c_end, t_end, - pos))] += lagrange + q[((node_1, c_start, t_start, pos), (node_2, c_end, t_end, pos))] += lagrange # Objective that minimizes distance for u, v in itertools.combinations(problem.nodes, 2): @@ -202,25 +165,34 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): for c_start in config: for c_end in config: nextpos = (pos + 1) % timesteps - edge_u_v = next(item for item in list(problem[u][v].values()) if - item["c_start"] == c_start and item["t_start"] == t_start and item[ - "c_end"] == c_end and item["t_end"] == t_end) + edge_u_v = next( + item for item in list(problem[u][v].values()) + if item["c_start"] == c_start and item["t_start"] == t_start and + item["c_end"] == c_end and item["t_end"] == t_end + ) # since it is the other direction we switch start and end of tool and config - edge_v_u = next(item for item in list(problem[v][u].values()) if - item["c_start"] == c_end and item["t_start"] == t_end and item[ - "c_end"] == c_start and item["t_end"] == t_start) + edge_v_u = next( + item for item in list(problem[v][u].values()) + if item["c_start"] == c_end and item["t_start"] == t_end and + item["c_end"] == c_start and item["t_end"] == t_start + ) # going from u -> v - q[((u, c_start, t_start, pos), (v, c_end, t_end, nextpos))] += edge_u_v[weight] - + q[((u, c_start, t_start, pos), (v, c_end, t_end, nextpos))] += edge_u_v['weight'] # going from v -> u - q[((v, c_end, t_end, pos), (u, c_start, t_start, nextpos))] += edge_v_u[weight] + q[((v, c_end, t_end, pos), (u, c_start, t_start, nextpos))] += edge_v_u['weight'] logging.info("Created Qubo") return {"Q": q}, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + :param option: The submodule option to retrieve + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/PVC/mappings/__init__.py b/src/modules/applications/optimization/PVC/mappings/__init__.py index f7edfc4a..e9c96938 100644 --- a/src/modules/applications/optimization/PVC/mappings/__init__.py +++ b/src/modules/applications/optimization/PVC/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for PVC mappings""" +""" +Module for PVC mappings. + +This module initializes the PVC mapping packages +""" From c8bd76c1b56d677cb8b40965bf0f5fdc97050b3e Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 1 Oct 2024 12:12:02 +0200 Subject: [PATCH 02/40] Refactor code for PEP8 compliance and improved readability --- .../applications/optimization/SAT/SAT.py | 86 ++++++------ .../applications/optimization/SAT/__init__.py | 6 +- .../optimization/SAT/mappings/ChoiISING.py | 31 ++--- .../optimization/SAT/mappings/ChoiQUBO.py | 103 +++++++-------- .../optimization/SAT/mappings/DinneenISING.py | 44 +++--- .../optimization/SAT/mappings/DinneenQUBO.py | 69 +++++----- .../optimization/SAT/mappings/Direct.py | 46 +++---- .../optimization/SAT/mappings/QubovertQUBO.py | 60 ++++----- .../optimization/SAT/mappings/__init__.py | 6 +- .../applications/optimization/SCP/SCP.py | 36 ++--- .../applications/optimization/SCP/__init__.py | 6 +- .../optimization/SCP/data/__init__.py | 6 +- .../optimization/SCP/mappings/__init__.py | 6 +- .../optimization/SCP/mappings/qubovertQUBO.py | 35 +++-- .../applications/optimization/TSP/TSP.py | 84 ++++++------ .../applications/optimization/TSP/__init__.py | 6 +- .../TSP/data/createReferenceGraph.py | 1 + .../optimization/TSP/mappings/ISING.py | 125 +++++++----------- .../optimization/TSP/mappings/QUBO.py | 36 +++-- .../optimization/TSP/mappings/__init__.py | 6 +- 20 files changed, 383 insertions(+), 415 deletions(-) diff --git a/src/modules/applications/optimization/SAT/SAT.py b/src/modules/applications/optimization/SAT/SAT.py index 42db0cb2..c7309a1b 100644 --- a/src/modules/applications/optimization/SAT/SAT.py +++ b/src/modules/applications/optimization/SAT/SAT.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any import nnf import numpy as np from nnf import Var, And, Or from nnf.dimacs import dump -from modules.applications.Application import * +from modules.applications.Application import Application from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement +import logging class SAT(Optimization): @@ -38,35 +39,30 @@ def __init__(self): Constructor method """ super().__init__("SAT") - self.submodule_options = ["QubovertQUBO", "Direct", "ChoiQUBO", "DinneenQUBO", "ChoiIsing", "DinneenIsing"] + self.submodule_options = [ + "QubovertQUBO", "Direct", "ChoiQUBO", "DinneenQUBO", "ChoiIsing", "DinneenIsing" + ] self.literals = None self.num_tests = None self.num_constraints = None self.num_variables = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "nnf", "version": "0.4.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_solution_quality_unit(self) -> str: return "Evaluation" - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> Application: if option == "QubovertQUBO": from modules.applications.optimization.SAT.mappings.QubovertQUBO import \ @@ -91,11 +87,11 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Mapping Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this application - :return: + :return: Dict with cnfigurable settings .. code-block:: python return { @@ -182,16 +178,14 @@ class Config(TypedDict): problem_set: int max_tries: int - def generate_problem(self, config: Config) -> (nnf.And, list): + def generate_problem(self, config: Config) -> Tuple[nnf.And, list]: """ Generates a vehicle configuration problem out of a given config. Returns buildability constraints (hard constraints) and tests (soft constraints), the successful evaluation of which we try to maximize. Both are given in nnf form, which we then convert accordingly. - :param config: config with the parameters specified in Config class - :type config: Config - :return: - :rtype: tuple(nnf.And, list) + :param config: Configuration parameters for problem generation + :return: A tuple containing the problem, number of variables, and other details """ self.num_variables = config["variables"] @@ -204,9 +198,9 @@ def generate_problem(self, config: Config) -> (nnf.And, list): self.application = {} def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): - # iterate over the desired number of attempts: we break if we find a solvable instance. + # iterate over the desired number of attempts: break if we find a solvable instance. for attempt in range(nr_tries): - # initialize random number generator -- we multiply the attempt to traverse distinct random seeds + # initialize random number generator -- multiply the attempt to traverse distinct random seeds # for the hard and soft constraints, respectively (since rseed of the hard and soft constraints differs # by 1). rng = np.random.default_rng(rseed + attempt * 2) @@ -214,20 +208,18 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): # generate literal list to sample from lit_vars = [Var(f"L{i}") for i in range(nr_vars)] for _ in range(nr_clauses): - # we select three (non-repeated) literals and negate them randomly -- together constituting a clause + # Select three (non-repeated) literals and negate them randomly -- together constituting a clause chosen_literals = rng.choice(lit_vars, 3, replace=False) negate_literals = rng.choice([True, False], 3, replace=True) clause = [] - # we perform the random negations and append to clause: + # Perform the random negations and append to clause: for lit, neg in zip(chosen_literals, negate_literals): if neg: clause.append(lit.negate()) else: clause.append(lit) - # append the generated clause to the total container + # Append the generated clause to the total container clause_list.append(Or(clause)) - # we generate the conjunction of the problem, such that we can use the nnf native function and test its - # satisfiability. prob = And(clause_list) if not satisfiable or prob.satisfiable(): @@ -239,9 +231,9 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): raise ValueError("Unable to generate valid solution.") # we choose a random seed -- since we try at most max_tries times to generate a solvable instance, - # we space the initial random seeds by 2 * max_tries (because we need both hard and soft constraints). + # Space the initial random seeds by 2 * max_tries (because we need both hard and soft constraints). random_seed = 2 * config["problem_set"] * max_tries - # generate hard & soft constraints. We make both satisfiable, but this can in principle be tuned. + # generate hard & soft constraints. Make both satisfiable, but this can in principle be tuned. hard = And(_generate_3sat_clauses(num_constraints, self.num_variables, satisfiable=True, rseed=random_seed, nr_tries=max_tries)) # the random_seed + 1 ensures that a different set of seeds is sampled compared to the hard constraints. @@ -261,19 +253,16 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): f" and {self.num_tests} tests") return hard, soft - def validate(self, solution: dict) -> (bool, float): + def validate(self, solution: Dict) -> Tuple[bool, float]: """ - Checks given solution. + Validate a given solution against the constraints. - :param solution: - :type solution: dict - :return: Boolean whether the solution is valid, time it took to validate - :rtype: tuple(bool, float) + :param solution: The solution to validate + :return: True if the solution is valid, False otherwise, time it took to complete """ start = start_time_measurement() logging.info("Checking validity of solution:") - # logging.info(solution) nr_satisfied_hardcons = len(*np.where( [c.satisfied_by(solution) for c in self.application["constraints"].children] )) @@ -281,21 +270,18 @@ def validate(self, solution: dict) -> (bool, float): is_valid = ratio == 1.0 # prints the ratio of satisfied constraints and prints if all constraints are satisfied logging.info(f"Ratio of satisfied constraints: {ratio}\nSuccess:{['no', 'yes'][int(is_valid)]}") + return is_valid, end_time_measurement(start) - def evaluate(self, solution: dict) -> (float, float): + def evaluate(self, solution: Dict) -> Tuple[float, float]: """ Calculates the quality of the solution. - :param solution: - :type solution: dict + :param solution: dictionary containing the solution :return: Tour length, time it took to calculate the tour length - :rtype: tuple(float, float) """ start = start_time_measurement() - logging.info("Checking the quality of the solution:") - # logging.info(solution) # count the number of satisfied clauses nr_satisfied_tests = len(*np.where([test.satisfied_by(solution) for test in self.application["tests"]])) @@ -306,13 +292,21 @@ def evaluate(self, solution: dict) -> (float, float): return ratio_satisfied, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Save the constraints and tests to files in CNF format. + + :param path: The directory path where the files will be saved. + :param iter_count: The iteration count to include in the filenames. + """ with open(f"{path}/constraints_iter_{iter_count}.cnf", "w") as f_cons: dump( - obj=self.application["constraints"], fp=f_cons, + obj=self.application["constraints"], + fp=f_cons, var_labels={str(literal): idx + 1 for idx, literal in enumerate(self.literals)} ) with open(f"{path}/tests_iter_{iter_count}.cnf", "w") as f_test: dump( - obj=Or(self.application["tests"]), fp=f_test, + obj=Or(self.application["tests"]), + fp=f_test, var_labels={str(literal): idx + 1 for idx, literal in enumerate(self.literals)} ) diff --git a/src/modules/applications/optimization/SAT/__init__.py b/src/modules/applications/optimization/SAT/__init__.py index cd55763b..0ef93529 100644 --- a/src/modules/applications/optimization/SAT/__init__.py +++ b/src/modules/applications/optimization/SAT/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SAT""" +""" +Module for SAT mappings + +This module initializes the SAT application +""" diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py index a1bcad0e..2bdbde94 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Any, Dict, Tuple import numpy as np from dimod import qubo_to_ising -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.SAT.mappings.ChoiQUBO import ChoiQUBO from utils import start_time_measurement, end_time_measurement @@ -45,15 +45,8 @@ def get_requirements() -> list[dict]: :rtype: list[dict] """ return [ - - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, *ChoiQUBO.get_requirements() ] @@ -61,7 +54,7 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for this mapping - :return: + :return: Dictionary with parameter options .. code-block:: python return { @@ -101,16 +94,13 @@ class Config(TypedDict): hard_reward: float soft_reward: float - def map(self, problem: any, config) -> (dict, float): + def map(self, problem: Any, config: Config) -> Tuple[dict, float]: """ Uses the ChoiQUBO formulation and converts it to an Ising. :param problem: the SAT problem - :type problem: any :param config: dictionary with the mapping config - :type config: Config :return: dict with the ising, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() self.problem = problem @@ -132,14 +122,12 @@ def map(self, problem: any, config) -> (dict, float): return {"J": j_matrix, "t": t_vector}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. :param solution: dictionary containing the solution - :type: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() @@ -154,7 +142,12 @@ def reverse_map(self, solution: dict) -> (dict, float): return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Return the default submodule based on the given option. + :param option: the submodule option + :return: the default submodule + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py index c91b3d86..d918d531 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py @@ -13,11 +13,12 @@ # limitations under the License. from itertools import combinations, product -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple +import logging from nnf import Var, And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -36,25 +37,21 @@ def __init__(self): self.reverse_dict = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> list[Dict]: """ Return requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "nnf", - "version": "0.4.1" - } + {"name": "nnf", "version": "0.4.1"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: Dictionary with parameter options .. code-block:: python return { @@ -73,11 +70,17 @@ def get_parameter_options(self) -> dict: return { "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + "description": ( + "What Bh/A ratio do you want?" + "(How strongly to enforce hard cons.)" + ) }, "soft_reward": { "values": [0.1, 1, 2], - "description": "What Bh/Bs ratio do you want? This value is multiplied with the number of tests." + "description": ( + "What Bh/Bs ratio do you want?" + "This value is multiplied with the number of tests." + ) } } @@ -94,112 +97,103 @@ class Config(TypedDict): hard_reward: float soft_reward: float - def map(self, problem: (And, list), config) -> (dict, float): + def map(self, problem: Tuple[And, List], config: Config) -> Tuple[Dict, float]: """ Converts a MaxSAT instance with hard and soft constraints into a graph problem -- solving MaxSAT then corresponds to solving an instance of the Maximal Independent Set problem. See Andrew Lucas (2014), or the original publication by Choi (1004.2226). - :param problem: - :type problem: (nnf.And, list) + :param problem: A tuple conatining hard and soft constraints :param config: config with the parameters specified in Config class - :type config: Config - :return: - :rtype: tuple(dict, float) + :return: Dictionary containing the QUBO representation and the time taken """ start = start_time_measurement() hard_constraints, soft_constraints = problem - # in principle, one could use a different value of A -- it shouldn't play a role though. A = 1 Bh = config['hard_reward'] * A - # we divide Bh by the number of test clauses, such that fulfilling a test result is less favourable than - # satisfying a constraint, which we aim to prioritize. + # divide Bh by the number of test clauses, such that fulfilling a test result is less favourable than + # satisfying a constraint, which aim to prioritize. Bs = Bh * config['soft_reward'] / len(soft_constraints) - # we count the number of different variables that appear in the vehicle options problem: + + # Count the number of different variables that appear in the vehicle options problem: self.nr_vars = len(hard_constraints.vars().union(And(soft_constraints).vars())) - # edges variable holds all edges in the resulting graph + # Edges variable holds all edges in the resulting graph edges = {} # lit_occur is a dictionary which will store the information in which clause a certain literal will occur. lit_occur = {} def _add_clause(clause, curr_edges, curr_lit_occ, pos): - # iterating through the clauses, we add nodes corresponding to each literal - # the format is as follows: L12-5, means that literal 12 is present in clause nr. 5. literals = [f"{el}-{pos}" for el in clause.children] - # we connect the literals within one clause + # Connect the literals within one clause for cmb in combinations(literals, 2): - # we add a weight for each edge within clause + # Add a weight for each edge within clause curr_edges[cmb] = A - # we add the occurrences of the variables to the occurrences dictionary + # Add the occurrences of the variables to the occurrences dictionary for var in clause.children: if var.name not in curr_lit_occ.keys(): curr_lit_occ[var.name] = {True: [], False: []} - # we add occurrences and mark that they correspond to hard constraints + # Add occurrences and mark that they correspond to hard constraints curr_lit_occ[var.name][var.true].append(pos) return curr_edges, curr_lit_occ - # first convert the hard constraints into the graph + # Convert the hard constraints into the graph for idx, hard_constraint in enumerate(hard_constraints): edges, lit_occur = _add_clause(hard_constraint, edges, lit_occur, idx) - # we save the current total clause count: + # Save the current total clause count: constraints_max_ind = len(hard_constraints) - # we repeat the procedure for the soft constraints: + # Repeat the procedure for the soft constraints: for idx, soft_constraint in enumerate(soft_constraints): edges, lit_occur = _add_clause(soft_constraint, edges, lit_occur, idx + constraints_max_ind) - # we connect conflicting clauses using the lit_occur dict: + # Connect conflicting clauses using the lit_occur dict: for literal, positions_dict in lit_occur.items(): # for every literal lit, we check its occurrences and connect the non-negated and negated occurrences. for pos_true, pos_false in product(positions_dict[True], positions_dict[False]): - # we ensure that we do not add a penalty for contradicting literals in the if pos_true != pos_false: - # we employ the notation from nnf, where the tilde symbol ~ corresponds to negation. + # Employ the notation from nnf, where the tilde symbol ~ corresponds to negation. lit_true, lit_false = f"{literal}-{pos_true}", f"~{literal}-{pos_false}" - # we add a penalty for each such edge: + # Add a penalty for each such edge: edges[(lit_true, lit_false)] = A - # we collect all different nodes that we have in our graph, omitting repetitions: + # Collect all different nodes that we have in our graph, omitting repetitions: node_set = set([]) for nodes in edges.keys(): node_set = node_set.union(set(nodes)) node_list = sorted(node_set) - # we fix a mapping (node -> binary variable) + # Fix a mapping (node -> binary variable) relabel_dict = {v: i for i, v in enumerate(node_list)} - # we save the reverse mapping, which is later used to decode the solution. + # Save the reverse mapping, which is later used to decode the solution. self.reverse_dict = dict(enumerate(node_list)) def _remap_pair(pair): """Small helper function that maps the nodes of an edge to binary variables""" return relabel_dict[pair[0]], relabel_dict[pair[1]] - # we save the Qubo corresponding to the graph. + # Save the Qubo corresponding to the graph. Q = {_remap_pair(key): val for key, val in edges.items()} for v in node_list: - # we add different energy rewards depending on whether it is a hard or a soft constraint - # soft cons. have lower rewards, since we prioritize satisfying hard constraints. + # Add different energy rewards depending on whether it is a hard or a soft constraint if int(v.split('-')[-1]) < constraints_max_ind: - # if hard cons, we add -Bh as the reward + # if hard cons, add -Bh as the reward Q[_remap_pair((v, v))] = -Bh else: - # for soft constraints we add -Bs + # for soft constraints, add -Bs Q[_remap_pair((v, v))] = -Bs logging.info(f"Converted to Choi Qubo with {len(node_list)} binary variables. Bh={config['hard_reward']}," f" Bs={Bs}.") return {'Q': Q}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: Dict) -> Tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. :param solution: dictionary containing the solution - :type solution: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() # we define the literals list, so that we can check the self-consistency of the solution. That is, we save all @@ -209,11 +203,11 @@ def reverse_map(self, solution: dict) -> (dict, float): # assignments saves the actual solution assignments = [] for node, tf in solution.items(): - # we check if node is included in the set (i.e. if tf is True (1)) + # Check if node is included in the set (i.e. if tf is True (1)) if tf: # convert back to the language of literals lit_str = self.reverse_dict[node] - # we check if the literal is negated: + # Check if the literal is negated: if lit_str.startswith('~'): # remove the negation symbol lit_str = lit_str.replace('~', '') @@ -226,24 +220,29 @@ def reverse_map(self, solution: dict) -> (dict, float): lit = Var(lit_str) assignments.append(Var(lit_str.split('-')[0])) literals.append(lit) - # we check for self-consistency of solution; we check that the assignments of all literals are consistent: + # Check for self-consistency of solution; Check that the assignments of all literals are consistent: if not And(set(literals)).satisfiable(): logging.error('Generated solution is not self-consistent!') raise ValueError("Inconsistent solution for the ChoiQubo returned.") - # If the solution is consistent, we have to find and add potentially missing variables: + # If the solution is consistent, find and add potentially missing variables: assignments = sorted(set(assignments)) # find missing vars, or more precisely, their labels: missing_vars = set(range(self.nr_vars)) - {int(str(a).replace('L', '').replace('~', '')) for a in assignments} - # add the variables that we found were missing: + # add the variables that found were missing: for nr in missing_vars: assignments.append(Var(f'L{nr}')) return {list(v.vars())[0]: v.true for v in sorted(assignments)}, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Return the default submodule based on the given option. + :param option: the submodule option + :return: the default submodule + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py index f1b46410..d5deed04 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py @@ -12,13 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any import numpy as np from dimod import qubo_to_ising from nnf import And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.SAT.mappings.DinneenQUBO import DinneenQUBO from utils import start_time_measurement, end_time_measurement @@ -39,34 +39,24 @@ def __init__(self): self.qubo_mapping = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ Return requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, + {"name": "nnf", "version": "0.4.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, *DinneenQUBO.get_requirements() ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: dict with parameter options .. code-block:: python return { @@ -96,16 +86,13 @@ class Config(TypedDict): """ lagrange: float - def map(self, problem: any, config) -> (dict, float): + def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: """ Uses the DinneenQUBO formulation and converts it to an Ising. :param problem: the SAT problem - :type problem: any :param config: dictionary with the mapping config - :type config: Config :return: dict with the ising, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() self.problem = problem @@ -127,20 +114,16 @@ def map(self, problem: any, config) -> (dict, float): return {"J": j_matrix, "t": t_vector}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. :param solution: dictionary containing the solution - :type: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() # convert raw solution into the right format to use reverse_map() of ChoiQUBO.py - solution_dict = {} - for i, el in enumerate(solution): - solution_dict[i] = el + solution_dict = {i: el for i, el in enumerate(solution) } # reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) @@ -148,7 +131,12 @@ def reverse_map(self, solution: dict) -> (dict, float): return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Return the default submodule based on the given option. + :param option: the submodule option + :return: the default submodule + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py index a429ced1..3a8d203c 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py @@ -13,11 +13,12 @@ # limitations under the License. from itertools import combinations -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any +import logging from nnf import And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -35,25 +36,21 @@ def __init__(self): self.nr_vars = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "nnf", - "version": "0.4.1" - } + {"name": "nnf", "version": "0.4.1"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: dict with parameter options .. code-block:: python return { @@ -82,16 +79,13 @@ class Config(TypedDict): """ lagrange: float - def map(self, problem: (And, list), config: Config) -> (dict, float): + def map(self, problem: Tuple[And, List], config: Config) -> Tuple[Dict, float]: """ Performs the mapping into a QUBO formulation, as given by Dinneen. See also the QUARK paper. - :param problem: - :type problem: any + :param problem: SAT problem :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) + :return: tuple with the QUBO, time it took to map it """"" start = start_time_measurement() # extract hard and soft constraints from the generated problem @@ -102,27 +96,33 @@ def map(self, problem: (And, list), config: Config) -> (dict, float): # lagrange parameter is a factor of the number of soft constraints. lagrange *= len(soft) - def _add_clause(curr_qubo_dict, clause, pos, weight): + def _add_clause( + curr_qubo_dict: Dict[Tuple[int, int], float], + clause: Any, + pos: int, + weight: float + ) -> Dict[Tuple[int, int], float]: + """ Function that adds the QUBO terms corresponding to the clause and updates the QUBO dictionary accordingly. Additionally, the weight of the clause is taken into account. - :param curr_qubo_dict: - :param clause: - :param pos: - :param weight: - :return: + :param curr_qubo_dict: current QUBO dictionary + :param clause: clause to be added + :param pos: position of the auxiliary variable + :param weight: weight of the clause + :return: updated QUBO dictionary """ - def _check_and_add(dictionary, key, value): + def _check_and_add(dictionary: Dict, key: Tuple[int, int], value: float) -> Dict: """ Helper function that checks if key is present or not in dictionary and adds a value, adding the key if missing. - :param dictionary: - :param key: - :param value: - :return: + :param dictionary: dictionary to be updated + :param key: key to check in the dictionary + :param value: value to add to the key + :return: updated dictionary """ key = tuple(sorted(key)) if key not in dictionary.keys(): @@ -154,10 +154,10 @@ def _check_and_add(dictionary, key, value): return curr_qubo_dict qubo_dict = {} - # first we add the hard constraints -- we add the lagrange parameter as weight + # Add the hard constraints and add the lagrange parameter as weight for clause_ind, hard_clause in enumerate(hard): qubo_dict = _add_clause(qubo_dict, hard_clause, self.nr_vars + clause_ind, lagrange) - # next, we add the soft constraints -- we start the enumeration at the final index corresponding to hard cons. + # Add the soft constraints and start the enumeration at the final index corresponding to hard cons. for clause_ind, soft_clause in enumerate(soft): qubo_dict = _add_clause(qubo_dict, soft_clause, self.nr_vars + clause_ind + len(hard), 1) @@ -165,14 +165,12 @@ def _check_and_add(dictionary, key, value): f" Lagrange parameter used was: {config['lagrange']}.") return {"Q": qubo_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: """ Reverse mapping of the solution obtained from the Dinneen QUBO. :param solution: dictionary containing the solution - :type solution: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() mapped_sol = {} @@ -185,7 +183,12 @@ def reverse_map(self, solution: dict) -> (dict, float): return mapped_sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Return the default submodule based on the given option. + :param option: the submodule option + :return: the default submodule + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/SAT/mappings/Direct.py b/src/modules/applications/optimization/SAT/mappings/Direct.py index d140e9b9..4e098cc3 100644 --- a/src/modules/applications/optimization/SAT/mappings/Direct.py +++ b/src/modules/applications/optimization/SAT/mappings/Direct.py @@ -13,13 +13,14 @@ # limitations under the License. import io -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any +import logging from nnf import And from nnf.dimacs import dump from pysat.formula import CNF, WCNF -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -36,34 +37,24 @@ def __init__(self): self.submodule_options = ["ClassicalSAT", "RandomSAT"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ Return requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "python-sat", - "version": "1.8.dev13" - } + {"name": "nnf", "version": "0.4.1"}, + {"name": "python-sat", "version": "1.8.dev13"} ] - def get_parameter_options(self): + def get_parameter_options(self) -> Dict: """ Returns empty dict as this mapping has no configurable settings. :return: empty dict - :rtype: dict """ - return { - - } + return {} class Config(TypedDict): """ @@ -71,16 +62,13 @@ class Config(TypedDict): """ pass - def map(self, problem: (And, list), config: Config) -> (WCNF, float): + def map(self, problem: Tuple[And, List], config: Config) -> Tuple[WCNF, float]: """ We map from the nnf library into the python-sat library. - :param problem: - :type problem: (nnf.And, list) - :param config: empty dict - :type config: Config + :param problem: SAT problem + :param config: config with the parameters specified in Config class :return: mapped problem and the time it took to map it - :rtype: tuple(WCNF, float) """ start = start_time_measurement() hard_constraints, soft_constraints = problem @@ -111,7 +99,13 @@ def map(self, problem: (And, list), config: Config) -> (WCNF, float): return total_wcnf, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "ClassicalSAT": from modules.solvers.ClassicalSAT import ClassicalSAT # pylint: disable=C0415 return ClassicalSAT() @@ -121,14 +115,12 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Solver Option {option} not implemented") - def reverse_map(self, solution: list) -> (dict, float): + def reverse_map(self, solution: List) -> Tuple[dict, float]: """ Maps the solution returned by the pysat solver into the reference format. - :param solution: dictionary containing the solution - :type solution: list + :param solution: list containing the solution :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() diff --git a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py index 452132d3..ce3d4543 100644 --- a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any from qubovert.sat import NOT, OR, AND from nnf import And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement +import logging class QubovertQUBO(Mapping): @@ -34,29 +35,22 @@ def __init__(self): self.nr_vars = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> list[Dict]: """ Return requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "qubovert", - "version": "1.2.5" - } + {"name": "nnf", "version": "0.4.1"}, + {"name": "qubovert", "version": "1.2.5"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: dict with configurable settings .. code-block:: python return { @@ -90,10 +84,8 @@ def _constraints2qubovert(constraints: dict) -> AND: """ Converts the constraints nnf to a pubo in the qubovert library. - :param constraints: - :type constraints: dict - :return: - :rtype: AND + :param constraints: constraints in nnf format + :return: constraints in qubovert format """ clauses = [] for c in constraints.children: @@ -106,10 +98,8 @@ def _tests2qubovert(test_clauses: dict) -> sum: """ Converts the list of test clauses in the nnf format to a pubo. - :param test_clauses: - :type test_clauses: dict - :return: - :rtype: sum + :param test_clauses: test clauses in nnf format + :return: sum of mapped test clauses """ mapped_tests = [] @@ -118,16 +108,13 @@ def _tests2qubovert(test_clauses: dict) -> sum: return sum(mapped_tests) - def map(self, problem: any, config: Config) -> (dict, float): + def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: """ Converts the problem to a Qubo in dictionary format. Problem is a CNF formula from the nnf library. - :param problem: - :type problem: any + :param problem: SAT problem :param config: config with the parameters specified in Config class - :type config: Config :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() lagrange = config['lagrange'] @@ -137,10 +124,10 @@ def map(self, problem: any, config: Config) -> (dict, float): # find number of the variables that appear in the tests and constraints, to verify the reverse mapping. self.nr_vars = len(constraints.vars().union(And(test_clauses).vars())) - # first we convert the constraints to qubovert: + # Convert the constraints to qubovert: constraints_pubo = self._constraints2qubovert(constraints) - # next, we convert the tests into qubovert: + # Convert the tests into qubovert: tests_pubo = self._tests2qubovert(test_clauses) logging.info(f'{tests_pubo.to_qubo().num_terms} number of terms in tests qubo') lagrange *= len(test_clauses) @@ -153,7 +140,7 @@ def map(self, problem: any, config: Config) -> (dict, float): logging.info(f"Converted to QUBO with {qubo_problem.num_binary_variables} Variables." f" Lagrange parameter: {config['lagrange']}.") - # now we need to convert it to the right format to be accepted by Braket / Dwave + # Convert it to the right format to be accepted by Braket / Dwave q_dict = {} for k, v in qubo_problem.items(): @@ -172,26 +159,33 @@ def map(self, problem: any, config: Config) -> (dict, float): return {"Q": q_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> Tuple[Dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. :param solution: dictionary containing the solution - :type solution: dict :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() pubo_sol = self.pubo_problem.convert_solution(solution) + # Let's check if all variables appear in the solution. missing_vars = {f'L{i}' for i in range(self.nr_vars)} - set(pubo_sol.keys()) + # add values for the missing variables -- if they do not appear, then their assignment does not matter. for missing_var in missing_vars: pubo_sol[missing_var] = True + return pubo_sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/SAT/mappings/__init__.py b/src/modules/applications/optimization/SAT/mappings/__init__.py index ba060a30..cc4501cb 100644 --- a/src/modules/applications/optimization/SAT/mappings/__init__.py +++ b/src/modules/applications/optimization/SAT/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SAT mappings""" +""" +Module for SAT mappings + +This module initializes the SAT application +""" \ No newline at end of file diff --git a/src/modules/applications/optimization/SCP/SCP.py b/src/modules/applications/optimization/SCP/SCP.py index 7f06b043..1541e795 100644 --- a/src/modules/applications/optimization/SCP/SCP.py +++ b/src/modules/applications/optimization/SCP/SCP.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Set import pickle +import os -from modules.applications.Application import * +from modules.applications.Application import Application from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement @@ -38,8 +39,7 @@ def __init__(self): def get_solution_quality_unit(self) -> str: return "Number of selected subsets" - def get_default_submodule(self, option: str) -> Core: - + def get_default_submodule(self, option: str) -> Application: if option == "qubovertQUBO": from modules.applications.optimization.SCP.mappings.qubovertQUBO import QubovertQUBO # pylint: disable=C0415 return QubovertQUBO() @@ -72,14 +72,12 @@ def get_parameter_options(self): class Config(TypedDict): model_select: str - def generate_problem(self, config: Config) -> (set, list): + def generate_problem(self, config: Config) -> Tuple[set, List]: """ Generates predefined instances of the SCP. :param config: Config specifying the selected problem instances - :type config: Config :return: the union of all elements of an instance and a set of subsets, each covering a part of the union - :rtype: tuple(set, list) """ model_select = config['model_select'] self.application = {} @@ -90,8 +88,10 @@ def generate_problem(self, config: Config) -> (set, list): elif model_select == "Small": self.application["elements_to_cover"] = set(range(1, 15)) - self.application["subsets"] = [{1, 3, 4, 6, 7, 13}, {4, 6, 8, 12}, {2, 5, 9, 11, 13}, {1, 2, 7, 14, 15}, - {3, 10, 12, 14}, {7, 8, 14, 15}, {1, 2, 6, 11}, {1, 2, 4, 6, 8, 12}] + self.application["subsets"] = [ + {1, 3, 4, 6, 7, 13}, {4, 6, 8, 12}, {2, 5, 9, 11, 13}, {1, 2, 7, 14, 15}, + {3, 10, 12, 14}, {7, 8, 14, 15}, {1, 2, 6, 11}, {1, 2, 4, 6, 8, 12} + ] else: self.application["elements_to_cover"] = set(range(1, 100)) @@ -107,41 +107,35 @@ def generate_problem(self, config: Config) -> (set, list): return self.application["elements_to_cover"], self.application["subsets"] - def process_solution(self, solution: list) -> (list, float): + def process_solution(self, solution: List) -> Tuple[List, float]: """ Returns list of selected subsets and the time it took to process the solution. :param solution: Unprocessed solution - :type solution: list :return: Processed solution and the time it took to process it - :rtype: tuple(list, float) """ start_time = start_time_measurement() selected_subsets = [list(self.application["subsets"][i]) for i in solution] return selected_subsets, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: List) -> Tuple[bool, float]: """ Checks if the elements of the subsets that are part of the solution cover every element of the instance. :param solution: list containing all subsets that are part of the solution - :type solution: list :return: Boolean whether the solution is valid and time it took to validate - :rtype: tuple(bool, float) """ start = start_time_measurement() covered = set.union(*[set(subset) for subset in solution]) return covered == self.application["elements_to_cover"], end_time_measurement(start) - def evaluate(self, solution: list) -> (int, float): + def evaluate(self, solution: List) -> Tuple[int, float]: """ Calculates the number of subsets that are of the solution. :param solution: List containing all subsets that are part of the solution - :type solution: list :return: Number of subsets and the time it took to calculate it - :rtype: tuple(int, float) """ start = start_time_measurement() selected_num = len(solution) @@ -149,5 +143,11 @@ def evaluate(self, solution: list) -> (int, float): return selected_num, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the SCP instance to a file. + + :param path: Path to save the SCP instance + :param iter_count: Iteration count + """ with open(f"{path}/SCP_instance", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/SCP/__init__.py b/src/modules/applications/optimization/SCP/__init__.py index 9596ba2b..863c0dfe 100644 --- a/src/modules/applications/optimization/SCP/__init__.py +++ b/src/modules/applications/optimization/SCP/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing the SCP""" +""" +Module for SCP mappings + +This module initializes the SCP application +""" diff --git a/src/modules/applications/optimization/SCP/data/__init__.py b/src/modules/applications/optimization/SCP/data/__init__.py index 47c23f57..34b3f0c0 100644 --- a/src/modules/applications/optimization/SCP/data/__init__.py +++ b/src/modules/applications/optimization/SCP/data/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SCP data""" +""" +Module for SCP mappings + +This module initializes the SCP application +""" \ No newline at end of file diff --git a/src/modules/applications/optimization/SCP/mappings/__init__.py b/src/modules/applications/optimization/SCP/mappings/__init__.py index 85fad453..863c0dfe 100644 --- a/src/modules/applications/optimization/SCP/mappings/__init__.py +++ b/src/modules/applications/optimization/SCP/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SCP mappings""" +""" +Module for SCP mappings + +This module initializes the SCP application +""" diff --git a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py index a5a27daf..1c88e884 100644 --- a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py +++ b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Dict, List, Tuple, Set from qubovert.problems import SetCover -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement +import logging class QubovertQUBO(Mapping): @@ -31,21 +32,17 @@ def __init__(self): self.submodule_options = ["Annealer"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "qubovert", - "version": "1.2.5" - } + {"name": "qubovert", "version": "1.2.5"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping @@ -85,17 +82,14 @@ class Config(TypedDict): """ penalty_weight: float - def map(self, problem: tuple, config: Config) -> (dict, float): + def map(self, problem: Tuple, config: Config) -> Tuple[Dict, float]: """ Maps the SCP to a QUBO matrix. :param problem: tuple containing the set of all elements of an instance and a list of subsets each covering some of these elements - :type problem: tuple :param config: config with the parameters specified in Config class - :type config: Config :return: dict with QUBO matrix, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() penalty_weight = config['penalty_weight'] @@ -107,7 +101,7 @@ def map(self, problem: tuple, config: Config) -> (dict, float): logging.info(f"Converted to QUBO with {self.SCP_qubo.num_binary_variables} Variables.") - # now we need to convert it to the right format to be accepted by Braket / Dwave + # Convert it to the right format to be accepted by Braket / Dwave q_dict = {} for key, val in self.SCP_qubo.items(): @@ -126,21 +120,26 @@ def map(self, problem: tuple, config: Config) -> (dict, float): return {"Q": q_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (set, float): + def reverse_map(self, solution: Dict) -> Tuple[set, float]: """ Maps the solution of the QUBO to a set of subsets included in the solution. :param solution: QUBO matrix in dict form - :type solution: dict :return: tuple with set of subsets that are part of the solution and the time it took to map it - :rtype: tuple(set, float) """ start = start_time_measurement() sol = self.SCP_problem.convert_solution(solution) + return sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/TSP/TSP.py b/src/modules/applications/optimization/TSP/TSP.py index 46ef45bd..d1b62052 100644 --- a/src/modules/applications/optimization/TSP/TSP.py +++ b/src/modules/applications/optimization/TSP/TSP.py @@ -12,20 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Any, Tuple import pickle +import logging +import os import networkx as nx import numpy as np -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement class TSP(Optimization): """ - \"The famous travelling salesman problem (also called the travelling salesperson problem or in short TSP) is a + "The famous travelling salesman problem (also called the travelling salesperson problem or in short TSP) is a well-known NP-hard problem in combinatorial optimization, asking for the shortest possible route that visits each node exactly once, given a list of nodes and the distances between each pair of nodes. Applications of the TSP can be found in planning, logistics, and the manufacture of microchips. In these applications, the general @@ -35,7 +37,7 @@ class TSP(Optimization): graph. Taking an undirected weighted graph, nodes correspond to the graph's nodes, with paths corresponding to the graph's edges, and a path's distance is the edge's weight. Typically, the graph is complete where each pair of nodes is connected by an edge. If no connection exists between two nodes, one can add an arbitrarily long edge to complete - the graph without affecting the optimal tour.\" + the graph without affecting the optimal tour." (source: https://github.com/aws/amazon-braket-examples/tree/main/examples) """ @@ -47,23 +49,32 @@ def __init__(self): self.submodule_options = ["Ising", "QUBO", "GreedyClassicalTSP", "ReverseGreedyClassicalTSP", "RandomTSP"] @staticmethod - def get_requirements() -> list: + def get_requirements() -> List: + """ + Return requirements of this module + + :return: list of dict with requirements of this module + """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_solution_quality_unit(self) -> str: + """ + Returns the unit of measurement for the solution quality. + + :return: The unit of measurement for the solution quality. + """ return "Tour cost" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the given option. + :param option: The chosen submodule option. + :return: The corresponding submodule instance. + """ if option == "Ising": from modules.applications.optimization.TSP.mappings.ISING import Ising # pylint: disable=C0415 return Ising() @@ -82,11 +93,11 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Mapping Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this application - :return: + :return: Dictionary with configurable settings. .. code-block:: python return { @@ -124,10 +135,8 @@ def _get_tsp_matrix(graph: nx.Graph) -> np.ndarray: """ Creates distance matrix out of given coordinates. - :param graph: - :type graph: networkx.Graph - :return: - :rtype: np.ndarray + :param graph: The input graph + :return: Distance matrix """ number_of_nodes = len(graph) matrix = np.zeros((number_of_nodes, number_of_nodes)) @@ -136,16 +145,15 @@ def _get_tsp_matrix(graph: nx.Graph) -> np.ndarray: for j in distance_dist.items(): matrix[i[0] - 1][j[0] - 1] = j[1] matrix[j[0] - 1][i[0] - 1] = matrix[i[0] - 1][j[0] - 1] + return matrix def generate_problem(self, config: Config) -> nx.Graph: """ Uses the reference graph to generate a problem for a given config. - :param config: - :type config: Config - :return: graph with the problem - :rtype: networkx.Graph + :param config: Configuration dictionary + :return: Graph with the problem """ if config is None: @@ -167,6 +175,7 @@ def generate_problem(self, config: Config) -> nx.Graph: unwanted_nodes = nodes_in_graph[-len(nodes_in_graph) + nodes:] unwanted_nodes = [x for x in graph.nodes if x in unwanted_nodes] + # Remove one node after another for node in unwanted_nodes: graph.remove_node(node) @@ -180,24 +189,23 @@ def generate_problem(self, config: Config) -> nx.Graph: graph = nx.from_numpy_array(cost_matrix) self.application = graph + return graph - def process_solution(self, solution: dict) -> (list, float): + def process_solution(self, solution: Dict) -> Tuple[List, float]: """ Convert dict to list of visited nodes. - :param solution: - :type solution: dict + :param solution: Dictionary with solution :return: processed solution and the time it took to process it - :rtype: tuple(list, float) """ start_time = start_time_measurement() nodes = self.application.nodes() start = np.min(nodes) # fill route with None values route: list = [None] * len(self.application) - # get nodes from sample - # NOTE: Prevent duplicate node entries by enforcing only one occurrence per node along route + + # Get nodes from sample logging.info(str(solution.items())) for (node, timestep), val in solution.items(): @@ -243,16 +251,15 @@ def process_solution(self, solution: dict) -> (list, float): # print route parsed_route = ' ->\n'.join([f' Node {visit}' for visit in route]) logging.info(f"Route found:\n{parsed_route}") + return route, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: List) -> Tuple[bool, float]: """ Checks if it is a valid TSP tour. :param solution: list containing the nodes of the solution - :type solution: list :return: Boolean whether the solution is valid, time it took to validate - :rtype: tuple(bool, float) """ start = start_time_measurement() nodes = self.application.nodes() @@ -266,14 +273,12 @@ def validate(self, solution: list) -> (bool, float): logging.error(f"{len([node for node in list(nodes) if node not in solution])} nodes were NOT visited") return False, end_time_measurement(start) - def evaluate(self, solution: list) -> (float, float): + def evaluate(self, solution: List) -> Tuple[float, float]: """ Find distance for given route e.g. [0, 4, 3, 1, 2] and original data. - :param solution: - :type solution: list + :param solution: List containing the nodes of the solution :return: Tour cost and the time it took to calculate it - :rtype: tuple(float, float) """ start = start_time_measurement() # get the total distance without return @@ -286,7 +291,6 @@ def evaluate(self, solution: list) -> (float, float): # add distance between start and end point to complete cycle return_distance = self.application[solution[0]][solution[-1]]['weight'] - # logging.info('Distance between start and end: ' + return_distance) # get distance for full cycle distance_with_return = total_dist + return_distance @@ -295,5 +299,11 @@ def evaluate(self, solution: list) -> (float, float): return distance_with_return, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Save the current application state to a file. + + :param path: The directory path where the file will be saved + :param iter_count: The iteration count to include in the filename + """ with open(f"{path}/graph_iter_{iter_count}.gpickle", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/TSP/__init__.py b/src/modules/applications/optimization/TSP/__init__.py index 3a943cad..9ddfa960 100644 --- a/src/modules/applications/optimization/TSP/__init__.py +++ b/src/modules/applications/optimization/TSP/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing the TSP""" +""" +Module for TSP mappings + +This module initializes the TSP application +""" diff --git a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py index c9383b29..ca3d442f 100644 --- a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py @@ -27,6 +27,7 @@ for edge in graph.edges: if edge[0] == edge[1]: graph.remove_edge(edge[0], edge[1]) + print("Loaded graph:") print(nx.info(graph)) diff --git a/src/modules/applications/optimization/TSP/mappings/ISING.py b/src/modules/applications/optimization/TSP/mappings/ISING.py index 88553ad3..d90e4824 100644 --- a/src/modules/applications/optimization/TSP/mappings/ISING.py +++ b/src/modules/applications/optimization/TSP/mappings/ISING.py @@ -13,7 +13,8 @@ # limitations under the License. import re -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any +import logging import networkx as nx import numpy as np @@ -24,7 +25,7 @@ from qiskit_optimization.converters import QuadraticProgramToQubo from qiskit.quantum_info import SparsePauliOp -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.TSP.mappings.QUBO import QUBO from utils import start_time_measurement, end_time_measurement @@ -45,42 +46,23 @@ def __init__(self): self.config = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, - { - "name": "more-itertools", - "version": "10.5.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, - { - "name": "pyqubo", - "version": "1.4.0" - }, + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, + {"name": "more-itertools", "version": "10.5.0"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, + {"name": "pyqubo", "version": "1.4.0"}, *QUBO.get_requirements() ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping @@ -123,16 +105,13 @@ class Config(TypedDict): lagrange_factor: float mapping: str - def map(self, problem: nx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Maps the networkx graph to an Ising formulation. :param problem: networkx graph - :type problem: networkx.Graph :param config: config with the parameters specified in Config class - :param config: Config :return: dict with Ising, time it took to map it - :rtype: tuple(dict, float) """ self.graph = problem self.config = config @@ -149,14 +128,12 @@ def map(self, problem: nx.Graph, config: Config) -> (dict, float): raise ValueError(f"Unknown mapping {mapping}.") @staticmethod - def _create_pyqubo_model(cost_matrix: list) -> any: + def _create_pyqubo_model(cost_matrix: List) -> Any: """ This PyQubo formulation of the TSP was kindly provided by AWS. - :param cost_matrix: - :type cost_matrix: list - :return: - :rtype: any + :param cost_matrix: cost matrix of the TSP + :return: compiled PyQubo model """ n = len(cost_matrix) x = Array.create('c', (n, n), 'BINARY') @@ -186,10 +163,11 @@ def _create_pyqubo_model(cost_matrix: list) -> any: # Compile model model = H.compile() + return model @staticmethod - def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: + def _get_matrix_index(ising_index_string: Any, number_nodes: Any) -> Any: """ Converts dictionary index (e.g. 'c[0][2]') in PyQubo to matrix index. @@ -199,12 +177,9 @@ def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: ('c[1][0]', 'c[2][1]'): 0.720033199087941, ... } - :param ising_index_string: - :type ising_index_string: any - :param number_nodes: - :type number_nodes: any - :return: - :rtype: any + :param ising_index_string: Index string from PyQubo + :param number_nodes: Number of nodes in the graph + :return: Matrix index """ x = 0 y = 0 @@ -217,20 +192,15 @@ def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: return idx - def _map_pyqubo(self, graph: nx.Graph, config: Config) -> (dict, float): + def _map_pyqubo(self, graph: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Use Qubo / Ising model defined in PyQubo. :param graph: networkx graph - :type graph: networkx.Graph :param config: config with the parameters specified in Config class - :type config: Config :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() - # cost_matrix = nx.to_numpy_matrix(graph) #self.get_tsp_matrix(graph) - # cost_matrix = self.get_tsp_matrix(graph) cost_matrix = np.array(nx.to_numpy_array(graph, weight="weight")) model = self._create_pyqubo_model(cost_matrix) feed_dict = {'A': 2.0} @@ -254,23 +224,17 @@ def _map_pyqubo(self, graph: nx.Graph, config: Config) -> (dict, float): x = self._get_matrix_index(key[0], graph.number_of_nodes()) y = self._get_matrix_index(key[1], graph.number_of_nodes()) j_matrix[x][y] = value - # j_matrix = np.triu(j_matrix, k=1).astype(np.float64) - # print("Number items in Ising dict: {} Number of non-zero entries in matrix: {}".\ - # format(len(quad.items()), len(j_matrix.nonzero()))) return {"J": j_matrix, "J_dict": quad, "t_dict": linear, "t": t_matrix}, end_time_measurement(start) - def _map_ocean(self, graph: nx.Graph, config: Config) -> (dict, float): + def _map_ocean(self, graph: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Use D-Wave/Ocean TSP QUBO/Ising model: https://docs.ocean.dwavesys.com/en/stable/docs_dnx/reference/algorithms/generated/dwave_networkx.algorithms.tsp.traveling_salesperson_qubo.html#dwave_networkx.algorithms.tsp.traveling_salesperson_qubo :param graph: networkx graph - :type graph: networkx.Graph :param config: config with the parameters specified in Config class - :param config: Config :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() @@ -280,9 +244,6 @@ def _map_ocean(self, graph: nx.Graph, config: Config) -> (dict, float): # Convert ISING dict to matrix timesteps = graph.number_of_nodes() - - # number_of_edges = graph.number_of_edges() - # timesteps = len(Q)/number_of_edges matrix_size = graph.number_of_nodes() * timesteps j_matrix = np.zeros((matrix_size, matrix_size), dtype=float) @@ -302,25 +263,19 @@ def _map_ocean(self, graph: nx.Graph, config: Config) -> (dict, float): logging.info(j_matrix) logging.info(j_matrix.shape) - # j_matrix = self.convert_to_upper_triangular_form(j_matrix) - # logging.info("Upper triangle form: ") - # logging.info(j_matrix) return {"J": j_matrix, "t": np.array(list(t.values())), "J_dict": j}, end_time_measurement(start) @staticmethod - def _map_qiskit(graph: nx.Graph, config: Config) -> (dict, float): + def _map_qiskit(graph: nx.Graph, config: Config) -> Tuple[Dict, float]: """ Use Ising Mapping of Qiskit Optimize: TSP class: https://qiskit.org/documentation/optimization/stubs/qiskit_optimization.applications.Tsp.html Example notebook: https://qiskit.org/documentation/tutorials/optimization/6_examples_max_cut_and_tsp.html :param graph: networkx graph - :type graph: networkx.Graph :param config: config with the parameters specified in Config class - :type config: Config :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() tsp = Tsp(graph) @@ -329,10 +284,12 @@ def _map_qiskit(graph: nx.Graph, config: Config) -> (dict, float): qp2qubo = QuadraticProgramToQubo() qubo = qp2qubo.convert(qp) qubitOp, _ = qubo.to_ising() + # reverse generate J and t out of qubit PauliSumOperator from qiskit t_matrix = np.zeros(qubitOp.num_qubits, dtype=complex) j_matrix = np.zeros((qubitOp.num_qubits, qubitOp.num_qubits), dtype=complex) pauli_list = qubitOp.to_list() + for pauli_str, coeff in pauli_list: logging.info((pauli_str, coeff)) pauli_str_list = list(pauli_str) @@ -345,14 +302,12 @@ def _map_qiskit(graph: nx.Graph, config: Config) -> (dict, float): return {"J": j_matrix, "t": t_matrix}, end_time_measurement(start) - def reverse_map(self, solution: any) -> (dict, float): + def reverse_map(self, solution: Any) -> Tuple[Dict, float]: """ Maps the solution back to the representation needed by the TSP class for validation/evaluation. :param solution: list or array containing the solution - :type solution: any :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() if -1 in solution: # ising model output from Braket QAOA @@ -377,30 +332,46 @@ def reverse_map(self, solution: any) -> (dict, float): for key, value in self.key_mapping.items(): result[key] = 1 if solution[value] == 1 else 0 - logging.info(result) return result, end_time_measurement(start) @staticmethod - def _flip_bits_in_bitstring(solution: any) -> any: - # depending on used solver 0 or 1 can indicate a node per timestep - # if np.count_nonzero(solution) > n: + def _flip_bits_in_bitstring(solution: Any) -> Any: + """ + Flip bits in the solution bitstring to unify different mappings. + + :param solution: Solution bitstring + :return: Flipped solution bitstring + """ solution = np.array(solution) with np.nditer(solution, op_flags=['readwrite']) as it: for x in it: x[...] = 1 - x + return solution @staticmethod - def _convert_ising_to_qubo(solution: any) -> any: + def _convert_ising_to_qubo(solution: Any) -> Any: + """ + Convert Ising model output to QUBO format. + + :param solution: Ising model output + :return: QUBO format solution + """ solution = np.array(solution) with np.nditer(solution, op_flags=['readwrite']) as it: for x in it: if x == -1: x[...] = 0 + return solution def get_default_submodule(self, option: str) -> Core: + """ + Get the default submodule based on the given option. + :param option: Submodule option + :return: Corresponding submodule + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/TSP/mappings/QUBO.py b/src/modules/applications/optimization/TSP/mappings/QUBO.py index e1d762d2..c531800d 100644 --- a/src/modules/applications/optimization/TSP/mappings/QUBO.py +++ b/src/modules/applications/optimization/TSP/mappings/QUBO.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple +import logging import dwave_networkx as dnx import networkx -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -35,29 +36,22 @@ def __init__(self): self.submodule_options = ["Annealer"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "dwave_networkx", - "version": "0.8.15" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "dwave_networkx", "version": "0.8.15"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this mapping - :return: + :return: Dictionary with configurable settings .. code-block:: python return { @@ -92,22 +86,18 @@ class Config(TypedDict): """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: networkx.Graph, config: Config) -> Tuple[Dict, float]: """ Maps the networkx graph to a QUBO formulation. :param problem: networkx graph - :type problem: networkx.Graph :param config: config with the parameters specified in Config class - :type config: Config :return: dict with QUBO, time it took to map it - :rtype: tuple(dict, float) """ start = start_time_measurement() lagrange = None lagrange_factor = config['lagrange_factor'] weight = 'weight' - # get corresponding QUBO step by step if lagrange is None: # If no lagrange parameter provided, set to 'average' tour length. @@ -129,6 +119,12 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): return {"Q": q}, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Get the default submodule based on the given option. + + :param option: Submodule option + :return: Corresponding submodule + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/TSP/mappings/__init__.py b/src/modules/applications/optimization/TSP/mappings/__init__.py index 432113b3..9ddfa960 100644 --- a/src/modules/applications/optimization/TSP/mappings/__init__.py +++ b/src/modules/applications/optimization/TSP/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for TSP mappings""" +""" +Module for TSP mappings + +This module initializes the TSP application +""" From 0013578021f6ebbc0aba4e8e475664572b45d851 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 2 Oct 2024 15:26:28 +0200 Subject: [PATCH 03/40] Refactor code for PEP8 compliance and improved readability --- src/modules/applications/Application.py | 18 +- src/modules/applications/Mapping.py | 38 ++-- src/modules/applications/QML/QML.py | 23 ++- .../generative_modeling/GenerativeModeling.py | 50 ++--- .../data/data_handler/ContinuousData.py | 35 ++-- .../data/data_handler/DataHandler.py | 54 ++--- .../data/data_handler/DiscreteData.py | 38 ++-- .../data_handler/MetricsGeneralization.py | 31 +-- .../mappings/CustomQiskitNoisyBackend.py | 89 +++------ .../generative_modeling/mappings/Library.py | 40 ++-- .../mappings/LibraryPennylane.py | 65 ++---- .../mappings/LibraryQiskit.py | 49 +---- .../mappings/PresetQiskitNoisyBackend.py | 80 ++------ .../transformations/MinMax.py | 46 ++--- .../transformations/PIT.py | 42 ++-- .../transformations/Transformation.py | 53 +---- .../applications/optimization/Optimization.py | 67 +++---- .../applications/optimization/__init__.py | 4 +- src/modules/circuits/Circuit.py | 30 +-- src/modules/circuits/CircuitCardinality.py | 22 +- src/modules/circuits/CircuitCopula.py | 41 ++-- src/modules/circuits/CircuitStandard.py | 51 ++--- src/modules/devices/Device.py | 42 ++-- src/modules/devices/HelperClass.py | 25 ++- src/modules/devices/Local.py | 20 +- .../devices/SimulatedAnnealingSampler.py | 36 ++-- src/modules/devices/braket/Braket.py | 172 +++++++++------- src/modules/devices/braket/Ionq.py | 25 ++- src/modules/devices/braket/LocalSimulator.py | 24 ++- src/modules/devices/braket/OQC.py | 29 ++- src/modules/devices/braket/Rigetti.py | 29 ++- src/modules/devices/braket/SV1.py | 28 ++- src/modules/devices/braket/TN1.py | 21 +- .../devices/pulser/MockNeutralAtomDevice.py | 23 ++- src/modules/devices/pulser/Pulser.py | 27 +-- src/modules/solvers/Annealer.py | 65 +++--- src/modules/solvers/ClassicalSAT.py | 50 ++--- src/modules/solvers/GreedyClassicalPVC.py | 72 ++++--- src/modules/solvers/GreedyClassicalTSP.py | 65 +++--- src/modules/solvers/MIPsolverACL.py | 58 +++--- src/modules/solvers/NeutralAtomMIS.py | 91 ++++++--- src/modules/solvers/PennylaneQAOA.py | 173 +++++++--------- src/modules/solvers/QAOA.py | 138 ++++++------- src/modules/solvers/QiskitQAOA.py | 189 +++++++----------- src/modules/solvers/RandomClassicalPVC.py | 68 +++---- src/modules/solvers/RandomClassicalSAT.py | 52 ++--- src/modules/solvers/RandomClassicalTSP.py | 55 ++--- .../solvers/ReverseGreedyClassicalPVC.py | 70 +++---- .../solvers/ReverseGreedyClassicalTSP.py | 68 +++---- src/modules/solvers/Solver.py | 30 +-- 50 files changed, 1167 insertions(+), 1544 deletions(-) diff --git a/src/modules/applications/Application.py b/src/modules/applications/Application.py index a23b34ff..1ea35a03 100644 --- a/src/modules/applications/Application.py +++ b/src/modules/applications/Application.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.Core import * +from abc import ABC, abstractmethod +from typing import Any +from modules.Core import Core class Application(Core, ABC): @@ -23,30 +25,26 @@ class Application(Core, ABC): def __init__(self, application_name: str): """ - Constructor method + Constructor method. """ super().__init__(application_name) self.application_name = self.name self.application = None - def get_application(self) -> any: + def get_application(self) -> Any: """ - Gets the application + Gets the application. :return: self.application - :rtype: any """ return self.application @abstractmethod def save(self, path: str, iter_count: int) -> None: """ - Saves the concrete problem + Saves the concrete problem. + :param path: path of the experiment directory for this run - :type path: str :param iter_count: the iteration count - :type iter_count: int - :return: - :rtype: None """ pass diff --git a/src/modules/applications/Mapping.py b/src/modules/applications/Mapping.py index 9fa95b10..652dbf4c 100644 --- a/src/modules/applications/Mapping.py +++ b/src/modules/applications/Mapping.py @@ -12,72 +12,60 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.Core import * +from abc import ABC, abstractmethod +from typing import Any, Dict, Tuple +from modules.Core import Core class Mapping(Core, ABC): """ - This module translates the input data and problem specification from the parent module, e.g., - the application into a mathematical formulation suitable the submodule, e.g., a solver. + This module translates the input data and problem specification from the parent module, + e.g., the application into a mathematical formulation suitable the submodule, e.g., a solver. """ - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: """ - Maps the data to the correct target format + Maps the data to the correct target format. :param input_data: Data which should be mapped - :type input_data: any :param config: Config of the mapping - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: Tuple with mapped problem and the time it took to map it - :rtype: (any, float) """ output, preprocessing_time = self.map(input_data, config) return output, preprocessing_time - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: """ Reverse transformation/mapping from the submodule's format to the mathematical formulation - suitable for the parent module + suitable for the parent module. :param input_data: Data which should be reverse-mapped - :type input_data: any :param config: Config of the reverse mapping - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param kwargs: Optional keyword arguments :return: Tuple with reverse-mapped problem and the time it took to map it - :rtype: (any,float) """ output, postprocessing_time = self.reverse_map(input_data) return output, postprocessing_time @abstractmethod - def map(self, problem: any, config: dict) -> (any, float): + def map(self, problem: Any, config: Dict) -> Tuple[Any, float]: """ - Maps the given problem into a specific format suitable for the submodule, e.g., a solver + Maps the given problem into a specific format suitable for the submodule, e.g., a solver. :param config: Instance of class Config specifying the mapping settings - :type config: dict :param problem: Problem instance which should be mapped to the target representation - :type problem: any :return: Mapped problem and the time it took to map it - :rtype: tuple(any, float) """ pass - def reverse_map(self, solution) -> (any, float): + def reverse_map(self, solution: Any) -> Tuple[Any, float]: """ Maps the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. :param solution: Solution provided by submodule, e.g., the Solver class - :type solution: any :return: Reverse-mapped solution and the time it took to create it - :rtype: tuple(any, float) - """ return solution, 0 diff --git a/src/modules/applications/QML/QML.py b/src/modules/applications/QML/QML.py index 15ab5699..c40855f2 100644 --- a/src/modules/applications/QML/QML.py +++ b/src/modules/applications/QML/QML.py @@ -12,7 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.applications.Application import * +from abc import ABC, abstractmethod +from typing import Any, Dict + +from modules.applications.Application import Application class QML(Application, ABC): @@ -21,16 +24,20 @@ class QML(Application, ABC): """ @abstractmethod - def generate_problem(self, config) -> any: + def generate_problem(self, config: Dict) -> Any: """ - Creates a concrete problem and returns it - :param config: - :type config: dict - :return: - :rtype: any + Creates a concrete problem and returns it. + + :param config: Configuration dictionary + :return: Generated problem """ pass def save(self, path: str, iter_count: int) -> None: - # Transform tensorboard output file to pandas dataframe + """ + Placeholder method for saving output to a file. + + :param path: Path to save the file + :param iter_count: Iteration count + """ pass diff --git a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py b/src/modules/applications/QML/generative_modeling/GenerativeModeling.py index 5a71ef51..f703177c 100644 --- a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py +++ b/src/modules/applications/QML/generative_modeling/GenerativeModeling.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union +from typing import Union, Dict, Tuple, List from utils import start_time_measurement, end_time_measurement -from modules.applications.Application import * +from modules.applications.Application import Application from modules.applications.QML.QML import QML from modules.applications.QML.generative_modeling.data.data_handler.DiscreteData import DiscreteData from modules.applications.QML.generative_modeling.data.data_handler.ContinuousData import ContinuousData @@ -38,12 +38,11 @@ def __init__(self): self.data = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dicts with requirements of this module - :rtype: list[dict] """ return [] @@ -59,11 +58,11 @@ def get_default_submodule(self, option: str) -> Union[ContinuousData, DiscreteDa raise NotImplementedError(f"Transformation Option {option} not implemented") return self.data - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. - :return: + :return: Dictionary of configurable parameters .. code-block:: python return { @@ -81,52 +80,41 @@ def get_parameter_options(self) -> dict: } } - def generate_problem(self, config: dict) -> dict: + def generate_problem(self, config: Dict) -> Dict: """ The number of qubits is chosen for this problem. - :param config: dictionary including the number of qubits - :type config: dict - :return: dictionary with the number of qubits - :rtype: dict + :param config: Dictionary including the number of qubits + :return: Dictionary with the number of qubits """ application_config = {"n_qubits": config["n_qubits"]} return application_config - def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: + def preprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Dict, float]: """ Generate the actual problem instance in the preprocess function. - :param input_data: Usually not used for this method. - :type input_data: dict - :param config: config for the problem creation. - :type config: dict - :param kwargs: Optional additional arguments - :type kwargs: dict - :param kwargs: optional additional arguments. - :return: tuple containing qubit number and the function's computation time - :rtype: tuple[dict, float] + :param input_data: Usually not used for this method + :param config: Config for the problem creation + :param kwargs: Optional additional arguments + :param kwargs: optional additional arguments + :return: Tuple containing qubit number and the function's computation time """ start = start_time_measurement() output = self.generate_problem(config) output["store_dir_iter"] = f"{kwargs['store_dir']}/rep_{kwargs['rep_count']}" return output, end_time_measurement(start) - def postprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: + def postprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Dict, float]: """ Process the solution here, then validate and evaluate it. :param input_data: A representation of the quantum machine learning model that will be trained - :type input_data: dict :param config: Config specifying the parameters of the training - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict - :return: tuple with input_data and the function's computation time - :rtype: tuple[dict, float] + :param kwargs: Optional keyword arguments + :return: Tuple with input_data and the function's computation time """ - start = start_time_measurement() return input_data, end_time_measurement(start) diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py index a206ae9c..740c26f7 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py @@ -12,17 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Union +from typing import TypedDict, Union, List, Any, Tuple, Dict import logging import numpy as np import pkg_resources from utils import start_time_measurement, end_time_measurement - from modules.applications.QML.generative_modeling.transformations.MinMax import MinMax from modules.applications.QML.generative_modeling.transformations.PIT import PIT -from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import * +from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import DataHandler class ContinuousData(DataHandler): @@ -46,18 +45,14 @@ def __init__(self): self.n_qubits = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ Returns requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Union[PIT, MinMax]: @@ -69,11 +64,11 @@ def get_default_submodule(self, option: str) -> Union[PIT, MinMax]: raise NotImplementedError(f"Transformation Option {option} not implemented") return self.transformation - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this application - :return: + :return: Dictionary of parameter options .. code-block:: python @@ -116,23 +111,22 @@ class Config(TypedDict): data_set: int train_size: float - def data_load(self, gen_mod: dict, config: Config) -> dict: + def data_load(self, gen_mod: dict, config: Config) -> Dict: """ The chosen dataset is loaded and split into a training set. :param gen_mod: Dictionary with collected information of the previous modules - :type gen_mod: dict :param config: Config specifying the parameters of the data handler - :type config: Config :return: dictionary including the mapped problem - :rtype: dict """ self.dataset_name = config["data_set"] self.n_qubits = gen_mod["n_qubits"] - filename = pkg_resources.resource_filename('modules.applications.QML.generative_modeling.data', - f"{self.dataset_name}.npy") + filename = pkg_resources.resource_filename( + 'modules.applications.QML.generative_modeling.data', + f"{self.dataset_name}.npy" + ) self.dataset = np.load(filename) application_config = { @@ -144,15 +138,13 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: return application_config - def evaluate(self, solution: dict) -> tuple[float, float]: + def evaluate(self, solution: Dict) -> Tuple[float, float]: """ Calculate KL in original space. :param solution: a dictionary containing the solution data, including histogram_generated_original and histogram_train_original - :type solution: dict :return: Kullback-Leibler (KL) divergence for the generated samples and the time it took to calculate it - :rtype: tuple[float, float] """ start = start_time_measurement() @@ -175,10 +167,7 @@ def kl_divergence(self, target: np.ndarray, q: np.ndarray) -> float: Function to calculate KL divergence :param target: Probability mass function of the target distribution - :type target: np.ndarray :param q: Probability mass function generated by the quantum circuit - :type q: np.ndarray :return: Kullback-Leibler divergence - :rtype: float """ return np.sum(target * np.log(target / q)) diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py index 22999cd3..f372ac48 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py @@ -14,13 +14,14 @@ import pickle import os +from abc import ABC, abstractmethod from qiskit import qpy import numpy as np import pandas as pd from tensorboard.backend.event_processing.event_accumulator import EventAccumulator -from modules.Core import * +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -30,7 +31,7 @@ class DataHandler(Core, ABC): and problem specification into preproccesed format. """ - def __init__(self, name): + def __init__(self, name: str): """ Constructor method """ @@ -44,21 +45,11 @@ def get_requirements() -> list[dict]: Returns requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "pandas", - "version": "2.2.2" - }, - { - "name": "tensorboard", - "version": "2.17.0" - } + {"name": "numpy", "version": "1.26.4"}, + {"name": "pandas", "version": "2.2.2"}, + {"name": "tensorboard", "version": "2.17.0"} ] def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: @@ -66,13 +57,9 @@ def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, fl In this module, the preprocessing step is transforming the data to the correct target format. :param input_data: collected information of the benchmarking process - :type input_data: dict :param config: config specifying the parameters of the training - :type config: dict :param kwargs: optional additional settings - :type kwargs: dict :return: tuple with transformed problem and the time it took to map it - :rtype: tuple[dict, float] """ start = start_time_measurement() output = self.data_load(input_data, config) @@ -86,14 +73,10 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f """ In this module, the postprocessing step is transforming the data to the correct target format. - :param input_data: any - :type input_data: dict + :param input_data: Any :param config: config specifying the parameters of the training - :type config: dict :param kwargs: optional additional settings - :type kwargs: dict :return: tuple with an output_dictionary and the time it took - :rtype: tuple[dict, float] """ start = start_time_measurement() store_dir_iter = input_data["store_dir_iter"] @@ -122,7 +105,9 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f if "inference" not in input_data.keys(): DataHandler.tb_to_pd(logdir=store_dir_iter, rep=str(kwargs['rep_count'])) self.metrics.add_metric_batch( - {"metrics_pandas": os.path.relpath(f"{store_dir_iter}/data.pkl", current_directory)}) + {"metrics_pandas": os.path.relpath(f"{store_dir_iter}/data.pkl", current_directory)} + ) + if self.generalization_mark is not None: np.save(f"{store_dir_iter}/histogram_generated.npy", evaluation["histogram_generated"]) else: @@ -135,17 +120,20 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f histogram_generated = input_data["histogram_generated"] np.save(f"{store_dir_iter}/histogram_generated.npy", histogram_generated) self.metrics.add_metric_batch({"histogram_generated": os.path.relpath( - f"{store_dir_iter}/histogram_generated.npy_{kwargs['rep_count']}.npy", current_directory)}) + f"{store_dir_iter}/histogram_generated.npy_{kwargs['rep_count']}.npy", current_directory)} + ) # Save histogram generated dataset np.save(f"{store_dir_iter}/histogram_train.npy", input_data.pop("histogram_train")) self.metrics.add_metric_batch({"histogram_train": os.path.relpath( - f"{store_dir_iter}/histogram_train.npy_{kwargs['rep_count']}.npy", current_directory)}) + f"{store_dir_iter}/histogram_train.npy_{kwargs['rep_count']}.npy", current_directory)} + ) # Save best parameters np.save(f"{store_dir_iter}/best_parameters_{kwargs['rep_count']}.npy", input_data.pop("best_parameter")) self.metrics.add_metric_batch({"best_parameter": os.path.relpath( - f"{store_dir_iter}/best_parameters_{kwargs['rep_count']}.npy", current_directory)}) + f"{store_dir_iter}/best_parameters_{kwargs['rep_count']}.npy", current_directory)} + ) # Save training results input_data.pop("circuit_transpiled") @@ -174,11 +162,8 @@ def data_load(self, gen_mod: dict, config: dict) -> tuple[any, float]: patterns and structure of the data, and produce high-quality outputs. :param gen_mod: dictionary with collected information of the previous modules - :type gen_mod: dict :param config: config specifying the parameters of the data handler - :type config: dict :return: mapped problem and the time it took to create the mapping - :rtype: tuple[any, float] """ pass @@ -187,8 +172,6 @@ def generalisation(self) -> tuple[dict, float]: Compute generalisation metrics :return: Evaluation and the time it took to create it - :rtype: tuple[dict, float] - """ # Compute your metrics here metrics = {} # Replace with actual metric calculations @@ -201,10 +184,7 @@ def evaluate(self, solution: any) -> tuple[any, float]: Compute the best loss values. :param solution: solution data - :type solution: any :return: evaluation data and the time it took to create it - :rtype: tuple[any, float] - """ return None, 0.0 @@ -215,9 +195,7 @@ def tb_to_pd(logdir: str, rep: str) -> None: into a pandas DataFrame and saves it as a pickle file. :param logdir: path to the log directory containing TensorBoard event files - :type logdir: str :param rep: repetition counter - :type rep: str """ event_acc = EventAccumulator(logdir) event_acc.Reload() diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py index 5df8389c..20790057 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Any, List, Dict, Tuple import itertools import logging from pprint import pformat @@ -20,7 +20,8 @@ import numpy as np from modules.circuits.CircuitCardinality import CircuitCardinality -from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import * +from utils import start_time_measurement, end_time_measurement +from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import DataHandler from modules.applications.QML.generative_modeling.data.data_handler.MetricsGeneralization import MetricsGeneralization @@ -45,33 +46,27 @@ def __init__(self): self.solution_set = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> CircuitCardinality: - if option == "CircuitCardinality": return CircuitCardinality() else: - raise NotImplementedError( - f"Circuit Option {option} not implemented") + raise NotImplementedError(f"Circuit Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this application - :return: + :return: A dictionary of parameter options .. code-block:: python return { @@ -101,16 +96,13 @@ class Config(TypedDict): train_size: int - def data_load(self, gen_mod: dict, config: Config) -> dict: + def data_load(self, gen_mod: dict, config: Config) -> Dict: """ The cardinality constrained dataset is created and split into a training set. :param gen_mod: Dictionary with collected information of the previous modules - :type gen_mod: dict :param config: Config specifying the parameters of the data handler - :type config: Config :return: dictionary including the mapped problem - :rtype: dict """ dataset_name = "Cardinality_Constraint" self.n_qubits = gen_mod["n_qubits"] @@ -152,7 +144,8 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: "n_registers": 2, "histogram_solution": self.histogram_solution, "histogram_train": self.histogram_train, - "store_dir_iter": gen_mod["store_dir_iter"]} + "store_dir_iter": gen_mod["store_dir_iter"] + } if self.train_size != 1: self.generalization_metrics = MetricsGeneralization( @@ -165,12 +158,11 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: return application_config - def generalisation(self) -> tuple[dict, float]: + def generalisation(self) -> Tuple[Dict, float]: """ Calculate generalization metrics for the generated. :return: a tuple containing a dictionary of generalization metrics and the execution time - :rtype: tuple[dict, float] """ start = start_time_measurement() results = self.generalization_metrics.get_metrics(self.samples) @@ -179,16 +171,14 @@ def generalisation(self) -> tuple[dict, float]: return results, end_time_measurement(start) - def evaluate(self, solution: dict) -> tuple[dict, float]: + def evaluate(self, solution: Dict) -> Tuple[Dict, float]: """ Evaluates a given solution and calculates the histogram of generated samples and the minimum KL divergence value. :param solution: dictionary containing the solution data, including generated samples and KL divergence values. - :type solution: dict :return: a tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence value, and the time it took to evaluate the solution. - :rtype: tuple[dict, float] """ start = start_time_measurement() self.samples = solution["best_sample"] diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py b/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py index 68f715f1..788357ee 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py @@ -13,7 +13,6 @@ # limitations under the License. import math - import numpy as np @@ -31,15 +30,7 @@ class MetricsGeneralization: :type n_qubits: int """ - def __init__( - - self, - train_set, - train_size, - solution_set, - n_qubits, - - ) -> None: + def __init__(self, train_set, train_size, solution_set, n_qubits) -> None: self.train_set = train_set self.train_size = train_size self.solution_set = solution_set @@ -50,12 +41,10 @@ def __init__( def get_masks(self) -> tuple[np.array, np.array]: """ - Method to determine the masks, on which the generalization metrics are based on + Method to determine the masks, on which the generalization metrics are based. - :return: masks needed to determine the generalization metrics for a given train and solution set - :rtype: tuple[np.array, np.array] + :return: Masks needed to determine the generalization metrics for a given train and solution set """ - mask_new = np.ones(self.n_states, dtype=bool) mask_new[self.train_set] = 0 @@ -70,9 +59,7 @@ def get_metrics(self, generated: np.array) -> dict: Method that determines all generalization metrics of a given multiset of generated samples :param generated: generated samples - :type generated: np.array :return: dictionary with generalization metrics - :rtype: dict """ g_new = np.sum(generated[self.mask_new]) g_sol = np.sum(generated[self.mask_sol]) @@ -95,11 +82,8 @@ def fidelity(self, g_new: float, g_sol: float) -> float: Method to determine the fidelity :param g_new: multi-subset of unseen queries (noisy or valid) - :type g_new: float :param g_sol: multi-subset of unseen and valid queries - :type g_sol: float :return: fidelity - :rtype: float """ return g_sol / g_new @@ -108,9 +92,7 @@ def coverage(self, g_sol_unique: float) -> float: Method to determine the coverage :param g_sol_unique: subset of unique unseen and valid queries - :type g_sol_unique: float :return: coverage - :rtype: float """ return g_sol_unique / (math.ceil(1 - self.train_size) * len(self.solution_set)) @@ -119,9 +101,7 @@ def normalized_rate(self, g_sol: float) -> float: Method to determine the normalized_rate :param g_sol: multi-subset of unseen and valid queries - :type g_sol: float :return: normalized_rate - :rtype: float """ return g_sol / ((1 - self.train_size) * self.n_shots) @@ -130,9 +110,7 @@ def exploration(self, g_new: float) -> float: Method to determine the exploration :param g_new: multi-subset of unseen queries (noisy or valid) - :type g_new: float :return: exploration - :rtype: float """ return g_new / self.n_shots @@ -141,10 +119,7 @@ def precision(self, g_sol: float, g_train: float) -> float: Method to determine the precision :param g_sol: multi-subset of unseen and valid queries - :type g_sol: float :param g_train: number of queries that were memorized from the training set - :type g_train: float :return: precision - :rtype: float """ return (np.sum(g_sol) + np.sum(g_train)) / self.n_shots diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py index b5561e0b..748e8cb3 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -54,28 +54,18 @@ def get_requirements() -> list[dict]: Returns requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "qiskit", "version": "1.1.0"}, + {"name": "qiskit_aer", "version": "0.15.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: + :return: Dictionary with configurable settings .. code-block:: python return { @@ -178,7 +168,12 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: + """ + Returns the default submodule based on the provided option. + :param option: The option to select the submodule + :return: The selected submodule + """ if option == "QCBM": return QCBM() elif option == "Inference": @@ -192,9 +187,7 @@ def sequence_to_circuit(self, input_data: dict) -> dict: to its Qiskit implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by it Qiskit implementation - :rtype: dict """ n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] @@ -245,53 +238,48 @@ def sequence_to_circuit(self, input_data: dict) -> dict: input_data.pop("gate_sequence") logging.info(param_counter) input_data["n_params"] = len(circuit.parameters) + return input_data @staticmethod def select_backend(config: str, n_qubits: int) -> Backend: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ if config == "aer_simulator_gpu": - # from qiskit import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") elif config == "aer_simulator_cpu": - # from qiskit import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="CPU") - else: raise NotImplementedError(f"Device Configuration {config} not implemented") return backend - def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pylint: disable=W0221 - config: str, config_dict: dict) -> tuple[any, any]: + def get_execute_circuit( + self, + circuit: QuantumCircuit, + backend: Backend, + config: str, + config_dict: dict + ) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. :param circuit: Qiskit implementation of the quantum circuit - :type circuit: qiskit.circuit.QuantumCircuit :param backend: Configured qiskit backend - :type backend: qiskit.providers.Backend :param config: Name of a backend - :type config: str :param config_dict: Contains information about config - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits @@ -303,6 +291,7 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli logging.info(f'Using {optimization_level=} with seed: {seed_transp}') coupling_map = self.get_coupling_map(config_dict, n_qubits) print(f"Generated coupling map: {coupling_map}") + # Create a manual layout if needed (you can customize the layout based on your use case) manual_layout = Layout({circuit.qubits[i]: i for i in range(n_qubits)}) @@ -357,18 +346,18 @@ def decompile_noisy_config(self, config_dict: dict, num_qubits: int) -> Backend: to the 'aer_simulator' backend. It returns the configured backend. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ backend_config = config_dict['backend'] device = 'GPU' if 'gpu' in backend_config else 'CPU' - simulation_method, device = self.get_simulation_method_and_device(device, config_dict['simulation_method']) + simulation_method, device = self.get_simulation_method_and_device( + device, config_dict['simulation_method'] + ) backend = self.get_custom_config(config_dict, num_qubits) \ - if config_dict['noise_configuration'] == "Custom configurations" else Aer.get_backend("aer_simulator") + if config_dict['noise_configuration'] == "Custom configurations" \ + else Aer.get_backend("aer_simulator") backend.set_options(device=device, method=simulation_method) self.log_backend_options(backend) @@ -380,13 +369,8 @@ def get_simulation_method_and_device(self, device: str, simulation_config: str) This method specifies the simulation methode and processing unit. :param device: Contains information about processing unit - :type device: str :param simulation_config: Contains information about qiskit simulation method - :type simulation_config: str - - :return: simulation_config: Contains information about qiskit simulation method - device: Contains information about processing unit - :rtype: tuple[str, str] + :return: Tuple containing the simulation method and device """ simulation_method = { "statevector": "statevector", @@ -404,11 +388,8 @@ def get_transpile_routine(self, transpile_config: int) -> int: This method returns the transpile routine based on the provided configuration. :param transpile_config: Configuration for transpile routine - :type transpile_config: int :return: Transpile routine level - :rtype: int """ - return transpile_config if transpile_config in [0, 1, 2, 3] else 1 def get_custom_config(self, config_dict: dict, num_qubits: int) -> Backend: @@ -416,11 +397,8 @@ def get_custom_config(self, config_dict: dict, num_qubits: int) -> Backend: This method creates a custom backend configuration based on the provided configuration dictionary. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Custom configured qiskit backend - :rtype: qiskit.providers.Backend """ noise_model = self.build_noise_model(config_dict) coupling_map = self.get_coupling_map(config_dict, num_qubits) @@ -429,6 +407,7 @@ def get_custom_config(self, config_dict: dict, num_qubits: int) -> Backend: backend = AerSimulator(noise_model=noise_model, coupling_map=coupling_map) else: backend = AerSimulator(noise_model=noise_model) + return backend def build_noise_model(self, config_dict: dict) -> NoiseModel: @@ -436,15 +415,14 @@ def build_noise_model(self, config_dict: dict) -> NoiseModel: This method builds a noise model based on the provided configuration dictionary. :param config_dict: Contains information about config - :type config_dict: dict :return: Constructed noise model - :rtype: NoiseModel """ noise_model = noise.NoiseModel() if config_dict['custom_readout_error']: readout_error = config_dict['custom_readout_error'] noise_model.add_all_qubit_readout_error( - [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]]) + [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]] + ) self.add_quantum_errors(noise_model, config_dict) return noise_model @@ -455,9 +433,7 @@ def add_quantum_errors(self, noise_model: NoiseModel, config_dict: dict) -> None configuration dictionary. :param noise_model: Noise model to which quantum errors are added - :type noise_model: NoiseModel :param config_dict: Contains information about config - :type config_dict: dict """ if config_dict['two_qubit_depolarizing_errors'] is not None: two_qubit_error = noise.depolarizing_error(config_dict['two_qubit_depolarizing_errors'], 2) @@ -474,11 +450,8 @@ def get_coupling_map(self, config_dict: dict, num_qubits: int) -> CouplingMap: This method returns the coupling map based on the provided configuration dictionary and number of qubits. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Coupling map - :rtype: CouplingMap """ layout = config_dict['qubit_layout'] if layout == 'linear': @@ -487,12 +460,6 @@ def get_coupling_map(self, config_dict: dict, num_qubits: int) -> CouplingMap: return CouplingMap.from_ring(num_qubits) elif layout == 'fully_connected': return CouplingMap.from_full(num_qubits) - # IBM layout will be added with release 2.1 - # elif layout == "ibm_brisbane": - # service = QiskitRuntimeService() - # backend = service.backend("ibm_brisbane") - # logging.info(f'Loaded with IBMQ Account {backend.name}, {backend.version}, {backend.num_qubits}') - # return backend.coupling_map elif layout is None: logging.info('No coupling map specified, using default.') return None diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index cec62f7a..8105da40 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -11,10 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - from abc import ABC, abstractmethod import logging -from typing import TypedDict +from typing import TypedDict, Any, Tuple, Dict from utils import start_time_measurement, end_time_measurement @@ -26,9 +25,9 @@ class Library(Core, ABC): This class is an abstract base class for mapping a library-agnostic gate sequence to a library such as Qiskit """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. """ self.name = name super().__init__() @@ -46,19 +45,15 @@ class Config(TypedDict): backend: str n_shots: int - def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, float]: + def preprocess(self, input_data: Dict, config: Config, **kwargs) -> Tuple[Dict, float]: """ Base class for mapping the gate sequence to a library such as Qiskit. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :param kwargs: optional keyword arguments - :type kwargs: dict :return: tuple including dictionary with the function to execute the quantum circuit on a simulator or quantum hardware and the computation time of the function - :rtype: tuple[dict, float] """ start = start_time_measurement() @@ -68,7 +63,8 @@ def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, output["circuit"], backend, config["backend"], - config) + config + ) output["backend"] = config["backend"] output["n_shots"] = config["n_shots"] logging.info("Library created") @@ -76,46 +72,37 @@ def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, return output, end_time_measurement(start) - def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: + def postprocess(self, input_data: Dict, config: dict, **kwargs) -> Tuple[Dict, float]: """ This method corresponds to the identity and passes the information of the subsequent module back to the preceding module in the benchmarking process. - :param input_data: Collected information of the benchmarking process - :type input_data: dict + :param input_data: Collected information of the benchmarking procesS :param config: Config specifying the number of qubits of the circuit - :type config: Config :param kwargs: optional keyword arguments - :type kwargs: dict :return: tuple with input dictionary and the computation time of the function - :rtype: tuple[dict, float] """ start = start_time_measurement() return input_data, end_time_measurement(start) @abstractmethod - def sequence_to_circuit(self, input_data): + def sequence_to_circuit(self, input_data: Dict) -> Dict: pass @staticmethod @abstractmethod - def get_execute_circuit(circuit: any, backend: any, config: str, config_dict: dict) -> ( + def get_execute_circuit(circuit: Any, backend: Any, config: str, config_dict: Dict) -> ( tuple)[any, any]: """ This method combines the circuit implementation and the selected backend and returns a function that will be called during training. - :param circuit: Implementation of the quantum circuit - :type circuit: any + :param circuit: Implementation of the quantum circuiT :param backend: Configured backend - :type backend: any - :param config: Name of the PennyLane device - :type config: str + :param config: Name of the PennyLane devicE :param config_dict: Dictionary including the number of shots - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ pass @@ -126,10 +113,7 @@ def select_backend(config: str, n_qubits: int) -> any: This method configures the backend :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured backend - :rtype: any """ return diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py index 5e6b1690..41ab6eea 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from typing import Union +from typing import Union, Any, Dict, Tuple, List import numpy as np import pennylane as qml @@ -34,41 +33,25 @@ def __init__(self): self.submodule_options = ["QCBM", "QGAN", "Inference"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "jax", - "version": "0.4.30" - }, - { - "name": "jaxlib", - "version": "0.4.30" - } + {"name": "pennylane", "version": "0.37.0"}, + {"name": "pennylane-lightning", "version": "0.38.0"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "jax", "version": "0.4.30"}, + {"name": "jaxlib", "version": "0.4.30"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for the PennyLane Library. - :return: + :return: Dictionary with configurable settings. .. code-block:: python return { @@ -89,7 +72,6 @@ def get_parameter_options(self) -> dict: "values": ["default.qubit", "default.qubit.jax", "lightning.qubit", "lightning.gpu"], "description": "Which device do you want to use?" }, - "n_shots": { "values": [100, 1000, 10000, 1000000], "description": "How many shots do you want use for estimating the PMF of the model?" @@ -107,19 +89,19 @@ def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: else: raise NotImplementedError(f"Training option {option} not implemented") - def sequence_to_circuit(self, input_data: dict) -> dict: + def sequence_to_circuit(self, input_data: Dict) -> Dict: """ Method that maps the gate sequence, that specifies the architecture of a quantum circuit to its PennyLane implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by its PennyLane implementation - :rtype: dict """ gate_sequence = input_data["gate_sequence"] n_qubits = input_data["n_qubits"] - num_parameters = sum(1 for gate, _ in gate_sequence if gate in ["RZ", "RX", "RY", "RXX", "RYY", "RZZ", "CRY"]) + num_parameters = sum( + 1 for gate, _ in gate_sequence if gate in ["RZ", "RX", "RY", "RXX", "RYY", "RZZ", "CRY"] + ) def create_circuit(params): param_counter = 0 @@ -165,52 +147,41 @@ def create_circuit(params): return input_data @staticmethod - def select_backend(config: str, n_qubits: int) -> any: + def select_backend(config: str, n_qubits: int) -> Any: """ This method configures the backend :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured backend - :rtype: any """ if config == "lightning.gpu": backend = qml.device(name="lightning.gpu", wires=n_qubits) - elif config == "lightning.qubit": backend = qml.device(name="lightning.qubit", wires=n_qubits) - elif config == "default.qubit": backend = qml.device(name="default.qubit", wires=n_qubits) - elif config == "default.qubit.jax": backend = qml.device(name="default.qubit.jax", wires=n_qubits) - else: raise NotImplementedError(f"Device Configuration {config} not implemented") return backend @staticmethod - def get_execute_circuit(circuit: callable, backend: qml.device, config: str, config_dict: dict) -> tuple[any, any]: + def get_execute_circuit( + circuit: callable, backend: qml.device, config: str, config_dict: Dict + ) -> Tuple[any, any]: """ This method combines the PennyLane circuit implementation and the selected backend and returns a function that will be called during training. :param circuit: PennyLane implementation of the quantum circuit - :type circuit: callable :param backend: Configured PennyLane device - :type backend: pennylane.device :param config: Name of the PennyLane device - :type config: str :param config_dict: Dictionary including the number of shots - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters twice - :rtype: tuple[any, any] """ - n_shots = config_dict["n_shots"] if config == "default.qubit.jax": diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py index 765a486f..6d649ab0 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from typing import Union +from typing import Union, Dict, List, Tuple, Any import logging from qiskit import QuantumCircuit, transpile from qiskit.circuit import Parameter @@ -41,25 +40,18 @@ def __init__(self): self.submodule_options = ["QCBM", "QGAN", "Inference"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "qiskit", "version": "1.1.0"}, + {"name": "numpy", "version": "1.26.4"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for the Qiskit Library. @@ -100,7 +92,6 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: - if option == "QCBM": return QCBM() elif option == "QGAN": @@ -110,15 +101,13 @@ def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: else: raise NotImplementedError(f"Option {option} not implemented") - def sequence_to_circuit(self, input_data: dict) -> dict: + def sequence_to_circuit(self, input_data: Dict) -> Dict: """ Maps the gate sequence, that specifies the architecture of a quantum circuit to its Qiskit implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by its Qiskit implementation - :rtype: dict """ n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] @@ -126,47 +115,35 @@ def sequence_to_circuit(self, input_data: dict) -> dict: circuit = QuantumCircuit(n_qubits, n_qubits) param_counter = 0 for gate, wires in gate_sequence: - if gate == "Hadamard": circuit.h(wires[0]) - elif gate == "CNOT": circuit.cx(wires[0], wires[1]) - elif gate == "RZ": circuit.rz(Parameter(f"x_{param_counter:03d}"), wires[0]) param_counter += 1 - elif gate == "RX": circuit.rx(Parameter(f"x_{param_counter:03d}"), wires[0]) param_counter += 1 - elif gate == "RY": circuit.ry(Parameter(f"x_{param_counter:03d}"), wires[0]) param_counter += 1 - elif gate == "RXX": circuit.rxx(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "RYY": circuit.ryy(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "RZZ": circuit.rzz(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "CRY": circuit.cry(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "Barrier": circuit.barrier() - elif gate == "Measure": circuit.measure(wires[0], wires[0]) - else: raise NotImplementedError(f"Gate {gate} not implemented") @@ -182,11 +159,8 @@ def select_backend(config: str, n_qubits: int) -> any: This method configures the backend :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured qiskit backend - :rtype: any """ if config == "cusvaer_simulator (only available in cuQuantum appliance)": import cusvaer # pylint: disable=C0415 @@ -253,23 +227,18 @@ def select_backend(config: str, n_qubits: int) -> any: return backend @staticmethod - def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict) \ - -> tuple[any, any]: # pylint: disable=W0221,R0915 + def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: Dict) \ + -> Tuple[Any, Any]: # pylint: disable=W0221,R0915 """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. :param circuit: Qiskit implementation of the quantum circuit - :type circuit: qiskit.circuit.QuantumCircuit :param backend: Configured qiskit backend - :type backend: qiskit.providers.Backend :param config: Name of a backend - :type config: str :param config_dict: Contains information about config - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py index a6ef4f05..c3a020f1 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union +from typing import Union, Any, List, Dict, Tuple import logging from time import perf_counter import numpy as np @@ -46,33 +46,20 @@ def __init__(self): circuit_transpiled = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_ibm_runtime", - "version": "0.29.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "qiskit", "version": "1.1.0"}, + {"name": "qiskit_ibm_runtime", "version": "0.29.0"}, + {"name": "qiskit_aer", "version": "0.15.0"}, + {"name": "numpy", "version": "1.26.4"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for the Qiskit Library. @@ -98,13 +85,10 @@ def get_parameter_options(self) -> dict: } """ - provider = FakeProviderForBackendV2() backends = provider.backends() value_list = [] value_list.append('No noise') - # value_list.append('ibm_osaka 127 Qubits') - # value_list.append('ibm_brisbane 127 Qubits') for backend in backends: if backend.num_qubits >= 6: value_list.append(f'{backend.name} V{backend.version} {backend.num_qubits} Qubits') @@ -124,7 +108,6 @@ def get_parameter_options(self) -> dict: "n_shots": { "values": [100, 1000, 10000, 1000000], "description": "How many shots do you want use for estimating the PMF of the model?" - # (If the aer_statevector_simulator selected, only relevant for studying generalization)" }, "transpile_optimization_level": { @@ -141,7 +124,6 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: - if option == "QCBM": return QCBM() elif option == "Inference": @@ -149,15 +131,13 @@ def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: else: raise NotImplementedError(f"Option {option} not implemented") - def sequence_to_circuit(self, input_data: dict) -> dict: + def sequence_to_circuit(self, input_data: Dict) -> Dict: """ Maps the gate sequence, that specifies the architecture of a quantum circuit to its Qiskit implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by it Qiskit implementation - :rtype: dict """ n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] @@ -216,45 +196,32 @@ def select_backend(config: str, n_qubits: int) -> Backend: This method configures the backend :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ - if config == "aer_simulator_gpu": - # from qiskit import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") - elif config == "aer_simulator_cpu": - # from qiskit import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="CPU") - else: raise NotImplementedError(f"Device Configuration {config} not implemented") return backend def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pylint: disable=W0221 - config: str, config_dict: dict) -> tuple[any, any]: + config: str, config_dict: Dict) -> Tuple[Any, Any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. :param circuit: Qiskit implementation of the quantum circuit - :type circuit: qiskit.circuit.QuantumCircuit :param backend: Configured qiskit backend - :type backend: qiskit.providers.Backend :param config: Name of a backend - :type config: str :param config_dict: Contains information about config - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits @@ -268,7 +235,7 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli circuit_transpiled = transpile(circuit, backend=backend, optimization_level=optimization_level, seed_transpiler=seed_transp) logging.info(f'Circuit operations before transpilation: {circuit.count_ops()}') - logging.info(f'Circuit operations before transpilation: {circuit_transpiled.count_ops()}') + logging.info(f'Circuit operations after transpilation: {circuit_transpiled.count_ops()}') logging.info(perf_counter() - start) if config in ["aer_simulator_cpu", "aer_simulator_gpu"]: @@ -308,11 +275,8 @@ def decompile_noisy_config(self, config_dict: dict, num_qubits: int) -> Backend: to the 'aer_simulator' backend. It returns the configured backend. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ backend_config = config_dict['backend'] device = 'GPU' if 'gpu' in backend_config else 'CPU' @@ -329,11 +293,8 @@ def select_backend_configuration(self, noise_configuration: str, num_qubits: int This method selects the backend configuration based on the provided noise configuration. :param noise_configuration: Noise configuration type - :type noise_configuration: str :param num_qubits: Number of qubits - :type num_qubits: int :return: Selected backend configuration - :rtype: qiskit.providers.Backend """ if "fake" in noise_configuration: return self.get_FakeBackend(noise_configuration, num_qubits) @@ -342,7 +303,6 @@ def select_backend_configuration(self, noise_configuration: str, num_qubits: int elif noise_configuration in ['ibm_brisbane 127 Qubits', 'ibm_osaka 127 Qubits']: logging.warning("Not yet implemented. Please check upcoming QUARK versions.") raise ValueError(f"Noise configuration '{noise_configuration}' not yet implemented.") - # return self.get_ibm_backend(noise_configuration) else: raise ValueError(f"Unknown noise configuration: {noise_configuration}") @@ -362,11 +322,8 @@ def configure_backend(self, backend: Backend, device: str, simulation_method: st This method configures the backend with the specified device and simulation method. :param backend: Backend to be configured - :type backend: qiskit.providers.Backend :param device: Device type (CPU/GPU) - :type device: str :param simulation_method: Simulation method - :type simulation_method: str """ backend.set_options(device=device) backend.set_options(method=simulation_method) @@ -375,16 +332,13 @@ def log_backend_info(self, backend: Backend): logging.info(f'Backend configuration: {backend.configuration()}') logging.info(f'Simulation method: {backend.options.method}') - def get_simulation_method_and_device(self, device: str, simulation_config: str) -> tuple[str, str]: + def get_simulation_method_and_device(self, device: str, simulation_config: str) -> Tuple[str, str]: """ This method determines the simulation method and device based on the provided configuration. - :param device: Contains information about processing unit - :type device: str - :param simulation_config: Contains information about qiskit simulation method - :type simulation_config: str + :param device: Contains information about processing unit + :param simulation_config: Contains information about qiskit simulation method :return: Tuple containing the simulation method and device - :rtype: tuple[str, str] """ simulation_methods = { "statevector": "statevector", @@ -401,9 +355,7 @@ def get_transpile_routine(self, transpile_config: int) -> int: This method returns the transpile routine based on the provided configuration. :param transpile_config: Configuration for transpile routine - :type transpile_config: int :return: Transpile routine level - :rtype: int """ return transpile_config if transpile_config in [0, 1, 2, 3] else 1 @@ -412,11 +364,8 @@ def get_FakeBackend(self, noise_configuration: str, num_qubits: int) -> Backend: This method returns a fake backend based on the provided noise configuration and number of qubits. :param noise_configuration: Noise configuration type - :type noise_configuration: str :param num_qubits: Number of qubits - :type num_qubits: int :return: Fake backend simulator - :rtype: qiskit.providers.Backend """ backend_name = str(self.split_string(noise_configuration)) provider = FakeProviderForBackendV2() @@ -439,4 +388,5 @@ def get_FakeBackend(self, noise_configuration: str, num_qubits: int) -> Backend: noise_model = NoiseModel.from_backend(backend) logging.info(f'Using {backend_name} with coupling map: {backend.coupling_map}') logging.info(f'Using {backend_name} with noise model: {noise_model}') + return AerSimulator.from_backend(backend) diff --git a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py index 991101aa..a349bfd8 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union +from typing import Union, List, Dict, Any, Tuple import numpy as np -from modules.applications.QML.generative_modeling.transformations.Transformation import * +from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation from modules.circuits.CircuitStandard import CircuitStandard from modules.circuits.CircuitCardinality import CircuitCardinality @@ -41,22 +41,15 @@ def __init__(self): self.histogram_train_original = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_default_submodule(self, option: str) -> Union[CircuitStandard, CircuitCardinality]: - if option == "CircuitStandard": return CircuitStandard() elif option == "CircuitCardinality": @@ -64,27 +57,22 @@ def get_default_submodule(self, option: str) -> Union[CircuitStandard, CircuitCa else: raise NotImplementedError(f"Circuit Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns empty dict as this transformation has no configurable settings :return: empty dict - :rtype: dict """ - return {} - def transform(self, input_data: dict, config: dict) -> dict: + def transform(self, input_data: Dict, config: Dict) -> Dict: """ Transforms the input dataset using MinMax transformation and computes histograms of the training dataset in the transformed space. :param input_data: A dictionary containing information about the dataset and application configuration. - :type input_data: dict :param config: A dictionary with parameters specified in the Config class. - :type config: dict :return: A tuple containing a dictionary with MinMax-transformed data. - :rtype: dict """ self.dataset_name = input_data["dataset_name"] self.dataset = input_data["dataset"] @@ -136,14 +124,12 @@ def transform(self, input_data: dict, config: dict) -> dict: return self.transform_config - def reverse_transform(self, input_data: dict) -> dict: + def reverse_transform(self, input_data: Dict) -> Dict: """ Transforms the solution back to the representation needed for validation/evaluation. :param input_data: dictionary containing the solution - :type input_data: dict :return: solution transformed accordingly - :rtype: dict """ best_results = input_data["best_sample"] depth = input_data["depth"] @@ -201,13 +187,11 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: Method that performs the min max normalization :param data: Data to be fitted - :type data: np.ndarray :return: fitted data - :rtype: np.ndarray """ - self.min = data.min() - self.max = data.max() - data.min() - data = (data - self.min) / self.max + self.data_min = data.min() + self.data_max = data.max() - self.data_min + data = (data - self.data_min) / self.data_max return data @@ -216,11 +200,9 @@ def inverse_transform(self, data: np.ndarray) -> np.ndarray: Method that performs the inverse min max normalization :param data: Data to be fitted - :type data: np.ndarray :return: data in original space - :rtype: np.ndarray """ - self.min = data.min() - self.max = data.max() - data.min() + self.data_min = data.min() + self.data_max = data.max() - self.data_min - return data * self.max + self.min + return data * self.data_max + self.data_min diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/QML/generative_modeling/transformations/PIT.py index f37fc269..f257992f 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/QML/generative_modeling/transformations/PIT.py @@ -11,11 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Dict, Tuple import numpy as np import pandas as pd -from modules.applications.QML.generative_modeling.transformations.Transformation import * +from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation from modules.circuits.CircuitCopula import CircuitCopula @@ -39,51 +40,39 @@ def __init__(self): self.histogram_transformed = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "pandas", - "version": "2.2.2" - } + {"name": "numpy", "version": "1.26.4"}, + {"name": "pandas", "version": "2.2.2"} ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns empty dict as this transformation has no configurable settings :return: empty dict - :rtype: dict """ return {} def get_default_submodule(self, option: str) -> CircuitCopula: - if option == "CircuitCopula": return CircuitCopula() else: raise NotImplementedError(f"Circuit Option {option} not implemented") - def transform(self, input_data: dict, config: dict) -> dict: + def transform(self, input_data: dict, config: Dict) -> Dict: """ Transforms the input dataset using PIT transformation and computes histograms of the training dataset in the transformed space. :param input_data: dataset - :type input_data: dict :param config: config with the parameters specified in Config class - :type config: dict :return: dict with PIT transformation, time it took to map it - :rtype: dict """ self.dataset_name = input_data["dataset_name"] self.dataset = input_data["dataset"] @@ -141,14 +130,12 @@ def transform(self, input_data: dict, config: dict) -> dict: return self.transform_config - def reverse_transform(self, input_data: dict) -> dict: + def reverse_transform(self, input_data: Dict) -> Dict: """ Transforms the solution back to the representation needed for validation/evaluation. :param input_data: dictionary containing the solution - :type input_data: dict :return: dictionary with solution transformed accordingly - :rtype: dict """ depth = input_data["depth"] architecture_name = input_data["architecture_name"] @@ -205,9 +192,7 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: Takes the data points and applies the PIT :param data: data samples - :type data: np.ndarray :return: Transformed data points - :rtype: np.ndarray """ df = pd.DataFrame(data) @@ -222,14 +207,12 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: self.reverse_epit_lookup = self.reverse_epit_lookup.values return df.values - def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> list[float]: + def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> List[float]: """ Takes one data point and applies the inverse PIT :param values: data point - :type values: np.ndarray :return: Data point after applying the inverse transformation - :rtype: list[float] """ values = values * (np.shape(self.reverse_epit_lookup)[1] - 1) rows = np.shape(self.reverse_epit_lookup)[0] @@ -251,12 +234,10 @@ def inverse_transform(self, data: np.ndarray) -> np.ndarray: Applies the inverse transformation to the full data set :param data: data set - :type data: np.ndarray :return: Data set after applying the inverse transformation - :rtype: np.ndarray """ - res = [self._reverse_emp_integral_trans_single(row) for row in data] + return np.array(res)[:, 0, :] def emp_integral_trans(self, data: np.ndarray) -> np.ndarray: @@ -264,4 +245,5 @@ def emp_integral_trans(self, data: np.ndarray) -> np.ndarray: length = data.size ecdf = np.linspace(0, 1, length, dtype=np.float64) ecdf_biject = ecdf[rank] + return ecdf_biject diff --git a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py index c53320ce..29f51d5d 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py +++ b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py @@ -13,10 +13,12 @@ # limitations under the License. from itertools import product +from abc import ABC, abstractmethod +from typing import Dict, Tuple, List import numpy as np -from modules.Core import * +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -34,32 +36,22 @@ def __init__(self, name): self.transformation_name = name @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ Returns requirements of this module :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] - def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: + def preprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Dict, float]: """ In this module, the preprocessing step is transforming the data to the correct target format. :param input_data: Collected information of the benchmarking process - :type input_data: dict :param config: Config specifying the parameters of the transformation - :type config: dict :param kwargs: Additional optional arguments - :type kwargs: dict :return: tuple with transformed problem and the time it took to map it - :rtype: tuple[dict, float] """ start = start_time_measurement() @@ -67,52 +59,43 @@ def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[di return output, end_time_measurement(start) - def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: + def postprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, float]: """ Does the reverse transformation :param input_data: Dictionary containing information of previously executed modules - :type input_data: dict :param config: Dictionary containing additional information - :type config: dict :param kwargs: Dictionary containing additional information - :type kwargs: dict :return: tuple with the dictionary and the time the postprocessing took - :rtype: tuple[dict, float] """ start = start_time_measurement() - output = self.reverse_transform(input_data) output["Transformation"] = True if "inference" in input_data: output["inference"] = input_data["inference"] + return output, end_time_measurement(start) @abstractmethod - def transform(self, input_data: dict, config: dict) -> dict: + def transform(self, input_data: Dict, config: Dict) -> Dict: """ Helps to ensure that the model can effectively learn the underlying patterns and structure of the data, and produce high-quality outputs. :param input_data: Input data for transformation. - :type input_data: dict :param config: Configuration parameters for the transformation. - :type config: dict :return: Transformed data. - :rtype: dict """ return input_data - def reverse_transform(self, input_data: dict) -> dict: + def reverse_transform(self, input_data: Dict) -> Dict: """ Transforms the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. :param input_data: The input data to be transformed. - :type input_data: dict :return: Transformed data. - :rtype: dict """ return input_data @@ -122,11 +105,8 @@ def compute_discretization(n_qubits: int, n_registered: int) -> np.ndarray: Compute discretization for the grid. :param n_qubits: Total number of qubits. - :type n_qubits: int :param n_registered: Number of qubits to be registered. - :type n_registered: int :return: Discretization data. - :rtype: np.ndarray """ n = 2 ** (n_qubits // n_registered) n_bins = n ** n_registered @@ -144,11 +124,8 @@ def compute_discretization_efficient(n_qubits: int, n_registers: int) -> np.ndar Compute grid discretization. :param n_qubits: Total number of qubits. - :type n_qubits: int :param n_registers: Number of qubits to be registered. - :type n_registers: int :return: Discretization data. - :rtype: np.ndarray """ n = 2 ** (n_qubits // n_registers) n_bins = n ** n_registers @@ -170,15 +147,10 @@ def generate_samples(results: np.ndarray, bin_data: np.ndarray, n_registers: int Generate samples based on measurement results and the grid bins. :param results: Results of measurements. - :type results: np.ndarray :param bin_data: Binned data. - :type bin_data: np.ndarray :param n_registers: Number of registers. - :type n_registers: int :param noisy: Flag indicating whether to add noise. - :type noisy: bool, optional :return: Generated samples. - :rtype: np.ndarray """ n_shots = np.sum(results) width = 1 / len(bin_data) ** (1 / n_registers) @@ -202,15 +174,10 @@ def generate_samples_efficient(results, bin_data: np.ndarray, n_registers: int, Generate samples efficiently using numpy arrays based on measurement results and the grid bins :param results: Results of measurements. - :type results: np.ndarray :param bin_data: Binned data. - :type bin_data: np.ndarray :param n_registers: Number of registers. - :type n_registers: int :param noisy: Flag indicating whether to add noise. - :type noisy: bool, optional :return: Generated samples. - :rtype: np.ndarray """ n_shots = np.sum(results) width = 1 / len(bin_data) ** (1 / n_registers) diff --git a/src/modules/applications/optimization/Optimization.py b/src/modules/applications/optimization/Optimization.py index ec45bef0..63c4f663 100644 --- a/src/modules/applications/optimization/Optimization.py +++ b/src/modules/applications/optimization/Optimization.py @@ -11,8 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from abc import ABC, abstractmethod +from typing import List, Dict, Tuple, Any +import logging -from modules.applications.Application import * +from modules.applications.Application import Application from utils import start_time_measurement, end_time_measurement @@ -22,15 +25,12 @@ class Optimization(Application, ABC): """ @abstractmethod - def validate(self, solution) -> (bool, float): + def validate(self, solution: Any) -> Tuple[bool, float]: """ Checks if the solution is a valid solution :param solution: Proposed solution - :type solution: any :return: bool value if solution is valid and the time it took to validate the solution - :rtype: tuple(bool, float) - """ pass @@ -40,100 +40,85 @@ def get_solution_quality_unit(self) -> str: Returns the unit of the evaluation :return: String with the unit - :rtype: str """ pass @abstractmethod - def evaluate(self, solution: any) -> (float, float): + def evaluate(self, solution: Any) -> Tuple[float, float]: """ Checks how good the solution is :param solution: Provided solution - :type solution: any :return: Evaluation and the time it took to create it - :rtype: tuple(any, float) - """ pass @abstractmethod - def generate_problem(self, config) -> any: + def generate_problem(self, config: Dict) -> Any: """ Creates a concrete problem and returns it - :param config: - :type config: dict - :return: - :rtype: any + :param config: Configuration for problem creation + :return: Generated problem """ pass - def process_solution(self, solution) -> (any, float): + def process_solution(self, solution: Any) -> Tuple[Any, float]: """ Most of the time the solution has to be processed before it can be validated and evaluated. This might not be necessary in all cases, so the default is to return the original solution. :param solution: Proposed solution - :type solution: any :return: Processed solution and the execution time to process it - :rtype: tuple(any, float) - """ return solution, 0.0 - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: """ For optimization problems, we generate the actual problem instance in the preprocess function. :param input_data: Input data (usually not used in this method) - :type input_data: any :param config: Config for the problem creation - :type config: dict :param kwargs: Optional additional arguments - :type kwargs: dict :return: Tuple with output and the preprocessing time - :rtype: (any, float) """ start = start_time_measurement() output = self.generate_problem(config) return output, end_time_measurement(start) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: Any, config: dict, **kwargs) -> Tuple[Any, float]: """ For optimization problems, we process the solution here, then validate and evaluate it. :param input_data: Data which should be evaluated for this optimization problem - :type input_data: any - :param config: Config - :type config: dict + :param config: Config for the problem creation :param kwargs: Optional additional arguments - :type kwargs: dict :return: Tuple with results and the postprocessing time - :rtype: (any, float) """ processed_solution = None try: - processed_solution, time_to_process_solution = self.process_solution( - input_data) - solution_validity, time_to_validation = self.validate( - processed_solution) + processed_solution, time_to_process_solution = self.process_solution(input_data) + solution_validity, time_to_validation = self.validate(processed_solution) except Exception as e: logging.exception(f"Exception on processing the solution: {e}") solution_validity = False time_to_process_solution = None time_to_validation = None + if solution_validity and (processed_solution is not None): solution_quality, time_to_evaluation = self.evaluate(processed_solution) else: solution_quality = None time_to_evaluation = None - self.metrics.add_metric_batch({"application_score_value": solution_quality, - "application_score_unit": self.get_solution_quality_unit(), - "application_score_type": str(float), - "processed_solution": processed_solution, - "time_to_process_solution": time_to_process_solution, - "time_to_validation": time_to_validation, - "time_to_evaluation": time_to_evaluation}) + self.metrics.add_metric_batch({ + "application_score_value": solution_quality, + "application_score_unit": self.get_solution_quality_unit(), + "application_score_type": str(float), + "processed_solution": processed_solution, + "time_to_process_solution": time_to_process_solution, + "time_to_validation": time_to_validation, + "time_to_evaluation": time_to_evaluation + }) + return solution_validity, sum(filter(None, [time_to_process_solution, time_to_validation, time_to_evaluation])) diff --git a/src/modules/applications/optimization/__init__.py b/src/modules/applications/optimization/__init__.py index 7bbc966c..2bda246e 100644 --- a/src/modules/applications/optimization/__init__.py +++ b/src/modules/applications/optimization/__init__.py @@ -12,4 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing all optimization applications""" +""" +Module containing all optimization applications +""" diff --git a/src/modules/circuits/Circuit.py b/src/modules/circuits/Circuit.py index 15b3e571..579a7312 100644 --- a/src/modules/circuits/Circuit.py +++ b/src/modules/circuits/Circuit.py @@ -13,6 +13,7 @@ # limitations under the License. from abc import ABC, abstractmethod +from typing import Any, Dict, Tuple from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -22,54 +23,53 @@ class Circuit(Core, ABC): This module is abstract base class for the library-agnostic gate sequence, that define a quantum circuit. """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. + + :param name : The name of the circuit architecture """ super().__init__() self.architecture_name = name @abstractmethod - def generate_gate_sequence(self, input_data: dict, config: any) -> dict: + def generate_gate_sequence(self, input_data: Dict, config: Any) -> Dict: """ Generates the library agnostic gate sequence, a well-defined definition of the quantum circuit. + + :param input_data: Input data required to generate the gate sequence + :param config: Configuration for the gate sequence + :return: Generated gate sequence """ pass - def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: + def preprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, float]: """ Library-agnostic implementation of the gate sequence, that will be mapped to backend such as Qiskit in the subsequent module. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param kwargs: Optional keyword arguments :return: Dictionary including the dataset, the gate sequence needed for circuit construction, and the time it took generate the gate sequence. - :rtype: tuple[dict, float] """ start = start_time_measurement() circuit_constr = self.generate_gate_sequence(input_data, config) - if "generalization_metrics" in list(input_data.keys()): + if "generalization_metrics" in input_data: circuit_constr["generalization_metrics"] = input_data["generalization_metrics"] + return circuit_constr, end_time_measurement(start) - def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: + def postprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, float]: """ Method that passes back information of the subsequent modules to the preceding modules. :param input_data: Collected information of the benchmarking process - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: dict :param kwargs: optional keyword arguments - :type kwargs: dict :return: Same dictionary like input_data with architecture_name - :rtype: tuple[dict, float] """ start = start_time_measurement() input_data["architecture_name"] = self.architecture_name diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/circuits/CircuitCardinality.py index 2be59116..e0a2e8ec 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/circuits/CircuitCardinality.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from typing import TypedDict +from typing import Union, TypedDict, Any, Tuple, Dict from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit @@ -38,13 +37,14 @@ def __init__(self): "LibraryQiskit", "LibraryPennylane", "CustomQiskitNoisyBackend", - "PresetQiskitNoisyBackend"] + "PresetQiskitNoisyBackend" + ] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this circuit. - :return: + :return: Dictionary with parameter options .. code-block:: python return { @@ -63,8 +63,9 @@ def get_parameter_options(self) -> dict: }, } - def get_default_submodule(self, option: str) ->\ - Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule( + self, option: str + ) ->Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": @@ -87,16 +88,13 @@ class Config(TypedDict): """ depth: int - def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: + def generate_gate_sequence(self, input_data: Dict, config: Config) -> Dict: """ - Returns gate sequence of cardinality circuit architecture + Returns gate sequence of cardinality circuit architecture. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :return: Dictionary including the gate sequence of the Cardinality Circuit - :rtype: dict """ n_qubits = input_data["n_qubits"] depth = config["depth"] // 2 diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/circuits/CircuitCopula.py index 461c7be5..f2b9e157 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/circuits/CircuitCopula.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from typing import TypedDict +from typing import Union, TypedDict, Any, Dict, Tuple, List from itertools import combinations from scipy.special import binom @@ -46,24 +45,17 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "scipy", - "version": "1.12.0" - } - ] + return [{"name": "scipy", "version": "1.12.0"}] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this Copula Circuit. - :return: - + :return: Dictionary of parameter options .. code-block:: python return { @@ -81,8 +73,16 @@ def get_parameter_options(self) -> dict: }, } - def get_default_submodule(self, option: str) -> \ - Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule( + self, option: str + ) -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule. + :raises NotImplemented: If the provided option is not implemented + """ if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": @@ -96,7 +96,7 @@ def get_default_submodule(self, option: str) -> \ class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -105,16 +105,13 @@ class Config(TypedDict): """ depth: int - def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: + def generate_gate_sequence(self, input_data: Dict, config: Config) -> Dict: """ - Returns gate sequence of copula architecture + Returns gate sequence of copula architecture. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :return: Dictionary including the gate sequence of the Copula Circuit - :rtype: dict """ n_registers = input_data["n_registers"] n_qubits = input_data["n_qubits"] diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/circuits/CircuitStandard.py index fb4a5d91..260d1265 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/circuits/CircuitStandard.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from typing import TypedDict +from typing import Union, TypedDict, Any, List, Tuple, Dict from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit @@ -30,38 +29,38 @@ class CircuitStandard(Circuit): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("DiscreteStandard") self.submodule_options = [ "LibraryQiskit", "LibraryPennylane", "CustomQiskitNoisyBackend", - "PresetQiskitNoisyBackend"] + "PresetQiskitNoisyBackend" + ] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this standard circuit. - :return: - .. code-block:: python + :return: Dictionary of parameter options. + .. code-block:: python - return { - "depth": { - "values": [1, 2, 3], - "description": "What depth do you want?" - } - } + return { + "depth": { + "values": [1, 2, 3], + "description": "What depth do you want?" + } + } """ return { @@ -72,8 +71,15 @@ def get_parameter_options(self) -> dict: } } - def get_default_submodule(self, option: str) -> \ - Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule( + self, option: str + ) -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule + """ if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": @@ -87,7 +93,7 @@ def get_default_submodule(self, option: str) -> \ class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -96,16 +102,13 @@ class Config(TypedDict): """ depth: int - def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: + def generate_gate_sequence(self, input_data: Dict, config: Config) -> Dict: """ - Returns gate sequence of standard architecture + Returns gate sequence of standard architecture. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :return: Dictionary including the gate sequence of the Standard Circuit - :rtype: dict """ n_registers = input_data["n_registers"] n_qubits = input_data["n_qubits"] diff --git a/src/modules/devices/Device.py b/src/modules/devices/Device.py index dcf71103..50a5f4d4 100644 --- a/src/modules/devices/Device.py +++ b/src/modules/devices/Device.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.Core import * +from typing import Dict, Any, Tuple +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -23,19 +24,20 @@ class Device(Core, ABC): def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: Name of the device """ super().__init__(device_name) self.device = None self.config = None self.device_name = self.name - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns the parameters to fine-tune the device + Returns the parameters to fine-tune the device. Should always be in this format: - .. code-block:: json { @@ -46,61 +48,55 @@ def get_parameter_options(self) -> dict: } :return: Available device settings for this device - :rtype: dict """ return {} def set_config(self, config): + """ + Sets the device configuration. + + :param config: Configuration settings for the device + """ self.config = config - def preprocess(self, input_data, config, **kwargs): + def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: """ - Returns instance of device class (self) and time it takes to call config + Returns instance of device class (self) and time it takes to call config. :param input_data: Input data (not used) - :type input_data: any :param config: Config for the device - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: Output and time needed - :rtype: (any, float) """ start = start_time_measurement() self.config = config return self, end_time_measurement(start) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: """ - Returns input data and adds device name to the metrics class instance + Returns input data and adds device name to the metrics class instance. :param input_data: Input data passed by the parent module - :type input_data: any :param config: solver config - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: Output and time needed - :rtype: (any, float) """ start = start_time_measurement() self.metrics.add_metric("device", self.get_device_name()) return input_data, end_time_measurement(start) - def get_device(self) -> any: + def get_device(self) -> Any: """ - Returns device + Returns device. :return: Instance of the device class - :rtype: any """ return self.device def get_device_name(self) -> str: """ - Returns device name + Returns the device name. :return: Name of the device - :rtype: str """ return self.device_name diff --git a/src/modules/devices/HelperClass.py b/src/modules/devices/HelperClass.py index 47e04502..128f759d 100644 --- a/src/modules/devices/HelperClass.py +++ b/src/modules/devices/HelperClass.py @@ -12,34 +12,41 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict from modules.devices.Device import Device from modules.Core import Core class HelperClass(Device): """ - Some Solvers like Pennylane only needs strings for setting up the device and not a standalone class + Some Solvers like Pennylane, only needs strings for setting up the device and not a standalone class. + TODO: Maybe refactor this once we think of a better structure for this """ def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: The name of the device """ super().__init__(device_name=device_name) self.device = device_name self.submodule_options = [] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/Local.py b/src/modules/devices/Local.py index 2ec48d6e..d5f7dbc2 100644 --- a/src/modules/devices/Local.py +++ b/src/modules/devices/Local.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict from modules.devices.Device import Device from modules.Core import Core @@ -24,22 +25,25 @@ class Local(Device): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__(device_name="local") self.device = None self.submodule_options = [] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/SimulatedAnnealingSampler.py b/src/modules/devices/SimulatedAnnealingSampler.py index 8e43bb6f..59677e28 100644 --- a/src/modules/devices/SimulatedAnnealingSampler.py +++ b/src/modules/devices/SimulatedAnnealingSampler.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Dict import dwave.samplers from modules.devices.Device import Device @@ -20,42 +21,39 @@ class SimulatedAnnealingSampler(Device): """ - Class for D-Waves neal simulated annealer + Class for D-Waves neal simulated annealer. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__(device_name="simulated annealer") self.device = dwave.samplers.SimulatedAnnealingSampler() self.submodule_options = [] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ] + return [{"name": "dwave-samplers", "version": "1.3.0"}] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index 98cfe745..0aa32579 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -17,6 +17,7 @@ import os from abc import ABC from datetime import datetime +from typing import List, Dict, Tuple import boto3 from botocore.config import Config @@ -33,7 +34,7 @@ class Braket(Device, ABC): def __init__(self, device_name: str, region: str = None, arn: str = None): """ - Constructor method + Constructor method. """ super().__init__(device_name) self.device = None @@ -44,83 +45,111 @@ def __init__(self, device_name: str, region: str = None, arn: str = None): # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return + if device_name != "LocalSimulator": - if 'HTTP_PROXY' in os.environ: - proxy_definitions = { - 'http': os.environ['HTTP_PROXY'], - 'https': os.environ['HTTP_PROXY'] - } - os.environ['HTTPS_PROXY'] = os.environ['HTTP_PROXY'] - else: - logging.warning( - 'No HTTP_PROXY was set as env variable! This might cause trouble if you are using a vpn') - proxy_definitions = None - - if region is not None: - pass - elif 'AWS_REGION' in os.environ: - region = os.environ['AWS_REGION'] - else: - region = 'us-east-1' - logging.info(f"No AWS_REGION specified, using default region: {region}") - logging.info(region) - my_config = Config( - region_name=region, - proxies=proxy_definitions - ) - if 'AWS_PROFILE' in os.environ: - profile_name = os.environ['AWS_PROFILE'] - elif "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" in os.environ: - logging.info("Assuming you are running on AWS container, getting credentials from " - "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") - profile_name = None + self._configure_aws_session(region) + + def _configure_aws_session(self, region: str): + """ + Configures the AWS session for the Braket device. + + :param region: AWS region to use + """ + proxy_definitions = self._setup_proxy() + region = self._set_region(region) + my_config = Config(region_name=region, proxies=proxy_definitions) + + profile_name = self._set_profile() + self._initialize_aws_session(profile_name, region, my_config) + + @staticmethod + def _setup_proxy(): + """ + Sets up proxy configuration if available in the environment variables. + + :return: Proxy definitions + """ + if 'HTTP_PROXY' in os.environ: + proxy_definitions = { + 'http': os.environ['HTTP_PROXY'], + 'https': os.environ['HTTP_PROXY'] + } + os.environ['HTTPS_PROXY'] = os.environ['HTTP_PROXY'] + else: + logging.warning('No HTTP_PROXY set as an environment variable. ' + 'This might cause trouble if you are using a VPN.') + proxy_definitions = None + return proxy_definitions + + @staticmethod + def _set_region(region: str) -> str: + """ + Sets the AWS region from the environment variable or defaults to 'us-east-1'. + + :param region: Provided region + :return: Final region to be used + """ + if region is None: + region = os.environ.get('AWS_REGION', 'us-east-1') + logging.info(f"No AWS_REGION specified, using default region: {region}") + return region + + @staticmethod + def _set_profile() -> str: + """ + Determines the AWS profile to use for the session. + + :return: AWS profile name + """ + if 'AWS_PROFILE' in os.environ: + return os.environ['AWS_PROFILE'] + elif "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" in os.environ: + logging.info("Assuming AWS container environment, using container credentials.") + return None + else: + profile_name = 'quantum_computing' + os.environ['AWS_PROFILE'] = profile_name + logging.info(f"No AWS_PROFILE specified, using default profile: {profile_name}") + return profile_name + + def _initialize_aws_session(self, profile_name: str, region: str, my_config: Config): + """ + Initializes the AWS session for interacting with Amazon Braket. + + :param profile_name: AWS profile name + :param region: AWS region + :param my_config: Boto3 configuration + :raises Exception: If the AWS profile is not found + """ + try: + if profile_name is None: + self.boto_session = boto3.Session(region_name=region) else: - profile_name = 'quantum_computing' - os.environ['AWS_PROFILE'] = profile_name - logging.info(f"No AWS_PROFILE specified, using default profile: {profile_name}") - - try: - if profile_name is None: - self.boto_session = boto3.Session(region_name=region) - else: - self.boto_session = boto3.Session(profile_name=profile_name, region_name=region) - self.aws_session = AwsSession(boto_session=self.boto_session, config=my_config) - except ProfileNotFound as exc: - logging.error(f"AWS-Profile {profile_name} could not be found! Please set env-variable AWS_PROFILE. " - f"Only LocalSimulator is available.") - raise Exception("Please refer to logged error message.") from exc + self.boto_session = boto3.Session(profile_name=profile_name, region_name=region) + self.aws_session = AwsSession(boto_session=self.boto_session, config=my_config) + except ProfileNotFound as exc: + logging.error(f"AWS-Profile {profile_name} could not be found! Please set the AWS_PROFILE env variable. " + "Only LocalSimulator is available.") + raise Exception("Please refer to the logged error message.") from exc @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dictionaries with requirements """ return [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } + {"name": "amazon-braket-sdk", "version": "1.87.0"}, + {"name": "botocore", "version": "1.35.20"}, + {"name": "boto3", "version": "1.35.20"} ] def init_s3_storage(self, folder_name: str) -> None: """ - Calls function to create a s3 folder that is needed for Amazon Braket. + Initializes an S3 storage bucket for Amazon Braket. :param folder_name: Name of the s3 folder - :type folder_name: str - :return: - :rtype: None """ run_timestamp = datetime.today().date() username = getpass.getuser() @@ -132,16 +161,21 @@ def init_s3_storage(self, folder_name: str) -> None: @staticmethod def _create_s3_bucket(boto3_session: boto3.Session, bucket_name: str = 'quark-benchmark-framework', region: str = 'us-east-1'): + """ + Creates an S3 bucket with specific configurations. + + :param boto3-session: Boto3 session + :param bucket_name: Name of the S3 bucket + :param region: AWS region + """ s3_client = boto3_session.client('s3', region_name=region) - # https://github.com/boto/boto3/issues/125 + if region == "us-east-1": s3_client.create_bucket(Bucket=bucket_name) else: location = {"LocationConstraint": region} - s3_client.create_bucket( - Bucket=bucket_name, - CreateBucketConfiguration=location - ) + s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) + s3_client.put_public_access_block( Bucket=bucket_name, PublicAccessBlockConfiguration={ diff --git a/src/modules/devices/braket/Ionq.py b/src/modules/devices/braket/Ionq.py index 556b36ba..0113ee9b 100644 --- a/src/modules/devices/braket/Ionq.py +++ b/src/modules/devices/braket/Ionq.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +from typing import Dict from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core @@ -25,27 +27,32 @@ class Ionq(Braket): def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-east-1::device/qpu/ionq/Harmony'): """ - Constructor method + Constructor method for initializing IonQ device on Amazon Braket """ super().__init__(region="us-east-1", device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("ionq") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: An empty dictionary """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/LocalSimulator.py b/src/modules/devices/braket/LocalSimulator.py index 7d509040..fb035edb 100644 --- a/src/modules/devices/braket/LocalSimulator.py +++ b/src/modules/devices/braket/LocalSimulator.py @@ -13,6 +13,7 @@ # limitations under the License. from braket.devices import LocalSimulator as LocalSimulatorBraket +from typing import Dict from modules.devices.braket.Braket import Braket from modules.Core import Core @@ -20,27 +21,32 @@ class LocalSimulator(Braket): """ - Class for using the local Amazon Braket simulator + Class for using the local Amazon Braket simulator. """ def __init__(self, device_name: str): """ - Constructor method + Constructor method for initializing the LocalSimulator class. + + :param device_name: Name of the device. """ super().__init__(device_name=device_name) self.device = LocalSimulatorBraket() self.submodule_options = [] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: An empty dictionary """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/OQC.py b/src/modules/devices/braket/OQC.py index 58bbe734..01277dd0 100644 --- a/src/modules/devices/braket/OQC.py +++ b/src/modules/devices/braket/OQC.py @@ -12,40 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from braket.aws import AwsDevice +from typing import Dict -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class OQC(Braket): """ - Class for using the Oxford Quantum Circuits (OQC) devices on Amazon Braket + Class for using the Oxford Quantum Circuits (OQC) devices on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy'): """ - Constructor method + Constructor method. """ super().__init__(region="eu-west-2", device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("oqc") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns an empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/Rigetti.py b/src/modules/devices/braket/Rigetti.py index d5cc01eb..b2842107 100644 --- a/src/modules/devices/braket/Rigetti.py +++ b/src/modules/devices/braket/Rigetti.py @@ -12,40 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +from typing import Dict from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class Rigetti(Braket): """ - Class for using the Rigetti devices on Amazon Braket + Class for using the Rigetti devices on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3'): """ - Constructor method + Constructor method. """ super().__init__(region="us-west-1", device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("rigetti") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns an empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/SV1.py b/src/modules/devices/braket/SV1.py index 8099b20f..96e21792 100644 --- a/src/modules/devices/braket/SV1.py +++ b/src/modules/devices/braket/SV1.py @@ -12,40 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +from typing import Dict from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class SV1(Braket): """ - Class for using the SV1 simulator on Amazon Braket + Class for using the SV1 simulator on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum-simulator/amazon/sv1'): """ - Constructor method + Constructor method. """ super().__init__(device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("sv1") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dicionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ return { - - } +} def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/TN1.py b/src/modules/devices/braket/TN1.py index 3db4cd3b..21b6c4bd 100644 --- a/src/modules/devices/braket/TN1.py +++ b/src/modules/devices/braket/TN1.py @@ -12,27 +12,34 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +from typing import Dict from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class TN1(Braket): """ - Class for using the TN1 simulator on Amazon Braket + Class for using the TN1 simulator on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum-simulator/amazon/tn1'): """ - Constructor method + Constructor method. + + :param device_name: Name of the device + :param arn: Amazon Resource Name for the TN1 simulator. """ super().__init__(device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("tn1") self.device = AwsDevice(arn, aws_session=self.aws_session) @@ -48,4 +55,10 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/pulser/MockNeutralAtomDevice.py b/src/modules/devices/pulser/MockNeutralAtomDevice.py index 97d93cda..cd00fbcf 100644 --- a/src/modules/devices/pulser/MockNeutralAtomDevice.py +++ b/src/modules/devices/pulser/MockNeutralAtomDevice.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Dict import pulser from pulser.devices import MockDevice @@ -24,21 +24,23 @@ class MockNeutralAtomDevice(Pulser): """ - Class for using the local mock Pulser simulator for neutral atom devices + Class for using the local mock Pulser simulator for neutral atom devices. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__(device_name="mock neutral atom device") self.device = MockDevice self.backend = QutipBackend self.submodule_options = [] - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. + + :return: Configurable settings for the mock neutral atom device """ return { "doppler": { @@ -61,7 +63,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. """ doppler: bool amplitude: bool @@ -70,10 +72,9 @@ class Config(TypedDict): def get_backend_config(self) -> pulser.backend.config.EmulatorConfig: """ - Returns backend configurations + Returns backend configurations. :return: backend config for the emulator - :rtype: pulser.backend.config.EmulatorConfig """ noise_types = [key for key, value in self.config.items() if value] noise_model = pulser.backend.noise_model.NoiseModel(noise_types=noise_types) @@ -81,4 +82,10 @@ def get_backend_config(self) -> pulser.backend.config.EmulatorConfig: return emulator_config def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules. + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/pulser/Pulser.py b/src/modules/devices/pulser/Pulser.py index 950d167a..87e100c9 100644 --- a/src/modules/devices/pulser/Pulser.py +++ b/src/modules/devices/pulser/Pulser.py @@ -13,6 +13,7 @@ # limitations under the License. from abc import ABC, abstractmethod +from typing import Any, List, Dict from modules.devices.Device import Device @@ -24,42 +25,36 @@ class Pulser(Device, ABC): def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: Name of the Pulser device. """ super().__init__(device_name) self.device = None self.backend = None - def get_backend(self) -> any: + def get_backend(self) -> Any: """ - Returns backend + Returns backend. :return: Instance of the backend class - :rtype: any """ return self.backend @abstractmethod - def get_backend_config(self) -> any: + def get_backend_config(self) -> Any: """ - Returns backend configurations + Returns backend configurations. :return: Instance of the backend config class - :rtype: any """ pass @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "pulser", - "version": "0.19.0" - }, - ] + return [{"name": "pulser","version": "0.19.0"}] diff --git a/src/modules/solvers/Annealer.py b/src/modules/solvers/Annealer.py index e9e5293f..064698e1 100644 --- a/src/modules/solvers/Annealer.py +++ b/src/modules/solvers/Annealer.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Dict, Any, Tuple +import logging -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -25,31 +27,37 @@ class Annealer(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Simulated Annealer"] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Simulated Annealer": from modules.devices.SimulatedAnnealingSampler import SimulatedAnnealingSampler # pylint: disable=C0415 return SimulatedAnnealingSampler() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns the configurable settings for this solver + Returns the configurable settings for this solver. - :return: - .. code-block:: python + :return: Dictionary of parameter options + .. code-block:: python - return { - "number_of_reads": { - "values": [100, 250, 500, 750, 1000], - "description": "How many reads do you need?" - } - } + return { + "number_of_reads": { + "values": [100, 250, 500, 750, 1000], + "description": "How many reads do you need?" + } + } """ return { @@ -70,56 +78,33 @@ class Config(TypedDict): """ number_of_reads: int - def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[dict, float]: """ Annealing Solver. :param mapped_problem: dictionary with the key 'Q' where its value should be the QUBO - :type mapped_problem: dict :param device_wrapper: Annealing device - :type device_wrapper: any :param config: Annealing settings - :type config: Config - :param kwargs: - :type kwargs: any + :param kwargs: Additional keyword arguments :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ Q = mapped_problem['Q'] additional_solver_information = {} device = device_wrapper.get_device() start = start_time_measurement() + if device_wrapper.device_name != "simulated annealer": logging.error("Only simulated annealer available at the moment!") logging.error("Please select another solver module.") logging.error("The benchmarking run terminates with exception.") - # TODO: Check what to do with this.. - # This section was used to leverage the D-Wave devices previously available on Amazon Braket - - # Embed QUBO - # start_embedding = time() * 1000 - # __, target_edgelist, target_adjacency = device.structure - # emb = find_embedding(Q, target_edgelist, verbose=1) - # sampler = FixedEmbeddingComposite(device, emb) - # additional_solver_information["embedding_time"] = round(time() * 1000 - start_embedding, 3) - # - # additional_solver_information["logical_qubits"] = len(emb.keys()) - # additional_solver_information["physical_qubits"] = sum(len(chain) for chain in emb.values()) - # logging.info(f"Number of logical variables: {additional_solver_information['logical_qubits']}") - # logging.info(f"Number of physical qubits used in embedding: " - # f"{additional_solver_information['physical_qubits']}") - # - # response = sampler.sample_qubo(Q, num_reads=config['number_of_reads'], answer_mode="histogram") - # # Add timings https://docs.dwavesys.com/docs/latest/c_qpu_timing.html - # additional_solver_information.update(response.info["additionalMetadata"]["dwaveMetadata"]["timing"]) raise Exception("Please refer to the logged error message.") + response = device.sample_qubo(Q, num_reads=config['number_of_reads']) time_to_solve = end_time_measurement(start) # take the result with the lowest energy: sample = response.lowest().first.sample - # logging.info("Result:" + str({k: v for k, v in sample.items() if v == 1})) logging.info(f'Annealing finished in {time_to_solve} ms.') return sample, time_to_solve, additional_solver_information diff --git a/src/modules/solvers/ClassicalSAT.py b/src/modules/solvers/ClassicalSAT.py index d4ae66e2..d77d907b 100644 --- a/src/modules/solvers/ClassicalSAT.py +++ b/src/modules/solvers/ClassicalSAT.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Any, Tuple +import logging from pysat.examples.rc2 import RC2 from pysat.formula import WCNF -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -28,65 +30,57 @@ class ClassicalSAT(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "python-sat", - "version": "1.8.dev13" - } - ] + return [{"name": "python-sat", "version": "1.8.dev13"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: dict) -> (list, float): + def run(self, mapped_problem: WCNF, device_wrapper: Any, config: Any, **kwargs: Dict) -> Tuple[List, float]: """ The given application is a problem instance from the pysat library. This uses the rc2 maxsat solver given in that library to return a solution. - :param mapped_problem: - :type mapped_problem: WCNF + :param mapped_problem: Problem instance from the pysat library :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config + :param config: Empty dict :param kwargs: no additionally settings needed - :type kwargs: any :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ logging.info( diff --git a/src/modules/solvers/GreedyClassicalPVC.py b/src/modules/solvers/GreedyClassicalPVC.py index e31ec4e1..a5cbeab1 100644 --- a/src/modules/solvers/GreedyClassicalPVC.py +++ b/src/modules/solvers/GreedyClassicalPVC.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any -import networkx +import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,92 +28,87 @@ class GreedyClassicalPVC(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: any, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Any, **kwargs: Dict) -> Tuple[Dict, float]: """ Solve the PVC graph in a greedy fashion. :param mapped_problem: graph representing a PVC problem - :type mapped_problem: networkx.Graph :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() - # We always start at the base node + #Start at the base node current_node = ((0, 0), 1, 1) idx = 1 tour = {current_node + (0,): 1} # Tour needs to cover all nodes, if there are 2 nodes left we can finish since these 2 nodes belong - # to the same seam while len(mapped_problem.nodes) > 2: # Get the minimum neighbor edge from the current node - next_node = min((x for x in mapped_problem.edges(current_node[0], data=True) if - x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2]), - key=lambda x: x[2]['weight']) + next_node = min( + ( + x for x in mapped_problem.edges(current_node[0], data=True) + if x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] + ), + key=lambda x: x[2]['weight']) + next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) - # Make the step - add distance to cost, add the best node to tour, + # Make the step - add distance to cost, add the best node to tour tour[next_node + (idx,)] = 1 # Remove all node of that seam to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) + current_node = next_node idx += 1 diff --git a/src/modules/solvers/GreedyClassicalTSP.py b/src/modules/solvers/GreedyClassicalTSP.py index e1028c01..6d5cbbb0 100644 --- a/src/modules/solvers/GreedyClassicalTSP.py +++ b/src/modules/solvers/GreedyClassicalTSP.py @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Any, Tuple -import networkx +import networkx as nx from networkx.algorithms import approximation as approx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -28,80 +29,70 @@ class GreedyClassicalTSP(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: any, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Any, **kwargs: Dict) ->Tuple[Dict, float]: """ Solve the TSP graph in a greedy fashion. :param mapped_problem: graph representing a TSP - :type mapped_problem: networkx.Graph :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() + #Use NetworkX approximation for a greedy TSP solution tour = approx.greedy_tsp(mapped_problem) - # We remove the duplicate node as we don't want a cycle - # https://stackoverflow.com/a/7961390/10456906 + # Remove the duplicate node as we don't want a cycle + # Reference: https://stackoverflow.com/a/7961390/10456906 tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later - result = {} - for idx, node in enumerate(tour): - result[(node, idx)] = 1 - # Tour needs to look like + result = {(node, idx): 1 for idx, node in enumerate(tour)} + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/MIPsolverACL.py b/src/modules/solvers/MIPsolverACL.py index b3c43096..c8244917 100644 --- a/src/modules/solvers/MIPsolverACL.py +++ b/src/modules/solvers/MIPsolverACL.py @@ -27,10 +27,11 @@ # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. -from typing import TypedDict +from typing import TypedDict, List, Dict, Any, Tuple import pulp -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -41,64 +42,56 @@ class MIPaclp(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "pulp", - "version": "2.9.0" - }, - ] + return [{"name": "pulp", "version": "2.9.0"},] - def get_default_submodule(self, option: str) -> any: + def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: """ - Solve the ACL problem as a mixed integer problem (MIP) + Solve the ACL problem as a mixed integer problem (MIP). - :param mapped_problem: linear problem in form of a dictionary - :type mapped_problem: dict + :param mapped_problem: Linear problem in form of a dictionary :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(dict, float, dict) """ # Convert dict of problem instance to LP problem _, problem_instance = pulp.LpProblem.from_dict(mapped_problem) @@ -112,4 +105,5 @@ def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwarg for v in problem_instance.variables(): variables[v.name] = v.varValue solution_data["variables"] = variables + return solution_data, end_time_measurement(start), {} diff --git a/src/modules/solvers/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py index a13205a7..d0fd3d0d 100644 --- a/src/modules/solvers/NeutralAtomMIS.py +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Any, Dict, List, Tuple +import logging import numpy as np import pulser -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -28,36 +30,38 @@ class NeutralAtomMIS(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["MockNeutralAtomDevice"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "pulser", - "version": "0.19.0" - } - ] + return [{"name": "pulser", "version": "0.19.0"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "MockNeutralAtomDevice": from modules.devices.pulser.MockNeutralAtomDevice import MockNeutralAtomDevice # pylint: disable=C0415 return MockNeutralAtomDevice() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns the configurable settings for this solver + Returns the configurable settings for this solver. + + :return: Dictionary of configurable settings. """ return { "samples": { @@ -71,35 +75,29 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. samples (int): How many times to sample the final state from the quantum computer per measurement """ samples: int - def run(self, mapped_problem: dict, device_wrapper: any, config: any, **kwargs: dict) -> (list, float, dict): + def run(self, mapped_problem: Dict, device_wrapper: Any, config: Any, **kwargs: Dict) -> Tuple[List, float, Dict]: """ The given application is a problem instance from the pysat library. This uses the rc2 maxsat solver given in that library to return a solution. - :param mapped_problem: - :type mapped_problem: dict with graph and register + :param mapped_problem: Dictionary with graph and register :param device_wrapper: Device to run the problem on - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Solver Configuration + :param kwargs: Additional settings (not used) :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ register = mapped_problem.get('register') graph = mapped_problem.get('graph') nodes = list(graph.nodes()) edges = list(graph.edges()) - logging.info( - f"Got problem with {len(graph.nodes)} nodes, {len(graph.edges)} edges." - ) + + logging.info(f"Got problem with {len(graph.nodes)} nodes, {len(graph.edges)} edges.") device = device_wrapper.get_device() device.validate_register(register) @@ -127,6 +125,10 @@ def _create_sequence(self, register:pulser.Register, device:pulser.devices._devi pulser.Sequence): """ Creates a pulser sequence from a register and a device. + + :param register: The quantum register + :param device: The device being used + :return: The created sequence """ pulses = self._create_pulses(device) sequence = pulser.Sequence(register, device) @@ -135,7 +137,7 @@ def _create_sequence(self, register:pulser.Register, device:pulser.devices._devi sequence.add(pulse, "Rydberg global") return sequence - def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: + def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> List[pulser.Pulse]: """ Creates pulses tuned to MIS problem. @@ -144,6 +146,9 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p We found this configuration in the documentation of the pulser documentation and it works for MIS. We are hesitant to make them parametrizable, because setting the wrong values will break your whole MIS. Though parameterization of pulses is a feature that we might implement in the future. + + :param device: The device being used + :return: List of pulses """ Omega_max = 2.3 * 2 * np.pi delta_factor = 2 * np.pi @@ -176,7 +181,15 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p return pulses - def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> dict: + def _filter_invalid_states(self, state_counts:Dict, nodes:List, edges:List) -> Dict: + """ + Filters out invalid states that do not meet the problem constraints. + + :param state_counts: Counts of each sampled data + :param nodes: List of nodes in the graph + :param edges: List of edges in the graph + :return: Dictionary of valid state counts + """ valid_state_counts = {} for state, count in state_counts.items(): selected_nodes = self._translate_state_to_nodes(state, nodes) @@ -191,10 +204,24 @@ def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> d return valid_state_counts - def _translate_state_to_nodes(self, state:str, nodes:list) -> list: + def _translate_state_to_nodes(self, state:str, nodes:List) -> List: + """ + Translates a state string into the corresponding list of nodes. + + :param state: State string + :param nodes: List of nodes + :return: List of nodes corresponding to the states + """ return [key for index, key in enumerate(nodes) if state[index] == '1'] - def _select_best_state(self, states:dict, nodes=list) -> str: + def _select_best_state(self, states:Dict, nodes:List) -> str: + """ + Selects the best state from the available valid states. + + :param states: Dictionary of valid states and their counts + :param nodes: List of nodes + :return: The best state as a string + """ # TODO: Implement the samplers try: best_state = max(states, key=lambda k: states[k]) diff --git a/src/modules/solvers/PennylaneQAOA.py b/src/modules/solvers/PennylaneQAOA.py index 71140ef9..783b3d5e 100644 --- a/src/modules/solvers/PennylaneQAOA.py +++ b/src/modules/solvers/PennylaneQAOA.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import ast import inspect import json @@ -19,14 +20,15 @@ from collections import Counter from functools import partial, wraps from time import time -from typing import TypedDict +from typing import TypedDict, Dict, Any, List, Tuple import matplotlib.pyplot as plt import numpy as np import pennylane as qml from pennylane import numpy as npqml -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -37,49 +39,44 @@ class PennylaneQAOA(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() - self.submodule_options = ["arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", - "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", - "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "braket.local.qubit", - "default.qubit", - "default.qubit.autograd", - "qulacs.simulator", - "lightning.gpu", - "lightning.qubit"] + self.submodule_options = [ + "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", + "braket.local.qubit", + "default.qubit", + "default.qubit.autograd", + "qulacs.simulator", + "lightning.gpu", + "lightning.qubit" + ] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "amazon-braket-pennylane-plugin", - "version": "1.30.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "pennylane", "version": "0.37.0"}, + {"name": "pennylane-lightning", "version": "0.38.0"}, + {"name": "amazon-braket-pennylane-plugin", "version": "1.30.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony": from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 @@ -117,35 +114,35 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this solver - :return: - .. code-block:: python - - return { - "shots": { # number measurements to make on circuit - "values": list(range(10, 500, 30)), - "description": "How many shots do you need?" - }, - "iterations": { # number measurements to make on circuit - "values": [1, 10, 20, 50, 75], - "description": "How many iterations do you need?" - }, - "layers": { - "values": [2, 3, 4], - "description": "How many layers for QAOA do you want?" - }, - "coeff_scale": { - "values": [0.01, 0.1, 1, 10], - "description": "How do you want to scale your coefficients?" - }, - "stepsize": { - "values": [0.0001, 0.001, 0.01, 0.1, 1], - "description": "Which stepsize do you want?" - } - } + :return: Dictionary of configuration settings + .. code-block:: python + + return { + "shots": { # number measurements to make on circuit + "values": list(range(10, 500, 30)), + "description": "How many shots do you need?" + }, + "iterations": { # number measurements to make on circuit + "values": [1, 10, 20, 50, 75], + "description": "How many iterations do you need?" + }, + "layers": { + "values": [2, 3, 4], + "description": "How many layers for QAOA do you want?" + }, + "coeff_scale": { + "values": [0.01, 0.1, 1, 10], + "description": "How do you want to scale your coefficients?" + }, + "stepsize": { + "values": [0.0001, 0.001, 0.01, 0.1, 1], + "description": "Which stepsize do you want?" + } + } """ return { @@ -173,7 +170,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -193,71 +190,59 @@ class Config(TypedDict): stepsize: float @staticmethod - def normalize_data(data: any, scale: float = 1.0) -> any: + def normalize_data(data: Any, scale: float = 1.0) -> Any: """ Not used currently, as I just scale the coefficients in the qaoa_operators_from_ising. - :param data: - :type data: any - :param scale: - :type scale: float + :param data: Data to normalize + :param scale: Scaling factor :return: Normalized data - :rtype: any """ return scale * data / np.max(np.abs(data)) @staticmethod - def qaoa_operators_from_ising(J: any, t: any, scale: float = 1.0) -> (any, any): + def qaoa_operators_from_ising(J: Any, t: Any, scale: float = 1.0) -> Tuple[Any, Any]: """ Generates pennylane cost and mixer hamiltonians from the Ising matrix J and vector t. :param J: J matrix - :type J: any :param t: t vector - :type t: any - :param scale: - :type scale: float - :return: + :param scale: Scaling factor + :return: Cost Hamiltonian and mixer Hamiltonian :rtype: tuple(any, any) """ - # we define the scaling factor as scale * the maximum parameter found in the coefficients + # Define the scaling factor scaling_factor = scale * max(np.max(np.abs(J.flatten())), np.max(np.abs(t))) - # we scale the coefficients + + # Scale the coefficients J /= scaling_factor t /= scaling_factor sigzsigz_arr = [ - qml.PauliZ(i) @ qml.PauliZ(j) for i in range(len(J)) - for j in range(len(J)) - ] + qml.PauliZ(i) @ qml.PauliZ(j) for i in range(len(J)) for j in range(len(J)) + ] sigz_arr = [qml.PauliZ(i) for i in range(len(t))] J_real = np.real(J.flatten()) t_real = np.real(t) h_cost = qml.simplify(qml.Hamiltonian([*t_real, *J_real.flatten()], [*sigz_arr, *sigzsigz_arr])) - # definition of the mixer hamiltonian + # Definition of the mixer hamiltonian h_mixer = -1 * qml.qaoa.mixers.x_mixer(range(len(J))) return h_cost, h_mixer # pylint: disable=R0915 - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> (any, any, float): + def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Any, Any, float]: """ Runs Pennylane QAOA on the Ising problem. :param mapped_problem: Ising - :type mapped_problem: any - :param device_wrapper: - :type device_wrapper: any - :param config: - :type config: Config + :param device_wrapper: Device to run the problem on + :param config: QAOA solver settings :param kwargs: contains store_dir for the plot of the optimization - :type kwargs: any :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - J = mapped_problem['J'] t = mapped_problem['t'] wires = J.shape[0] @@ -351,19 +336,14 @@ def cost_function(params): # Optimization Loop optimizer = qml.GradientDescentOptimizer(stepsize=config['stepsize']) - # optimizer = qml.MomentumOptimizer(stepsize=config['stepsize'], momentum=0.9) - # optimizer = qml.QNSPSAOptimizer(stepsize=config['stepsize']) logging.info(f"Device: {device_wrapper.device}, Optimizer {optimizer}, Differentiation: {diff_method}, " f"Optimization start...") additional_solver_information = {} - min_param = None - min_cost = None - cost_pt = [] - params_list = [] - x = [] + min_param, min_cost, cost_pt, params_list, x= None, None, [], [], [] run_id = round(time()) start = start_time_measurement() + for iteration in range(config['iterations']): t0 = start_time_measurement() # Evaluates the cost, then does a gradient step to new params @@ -375,12 +355,14 @@ def cost_function(params): logging.error(e) logging.error("Run a smaller problem size or select another device.") raise e + # Convert cost_before to a float, so it's easier to handle cost_before = float(cost_before) if iteration == 0: logging.info(f"Initial cost: {cost_before}") else: logging.info(f"Cost at step {iteration}: {cost_before}") + # Log the current loss as a metric logging.info(f"Time to complete iteration {iteration + 1}: {end_time_measurement(t0)}") cost_pt.append(cost_before) @@ -403,7 +385,6 @@ def cost_function(params): plt.clf() params = min_param - logging.info(f"Final params: {params}") logging.info(f"Final costs: {min_cost}") diff --git a/src/modules/solvers/QAOA.py b/src/modules/solvers/QAOA.py index 78bcc0c1..2a65cfa7 100644 --- a/src/modules/solvers/QAOA.py +++ b/src/modules/solvers/QAOA.py @@ -13,13 +13,15 @@ # limitations under the License. from time import sleep -from typing import TypedDict +from typing import TypedDict, List, Dict, Any, Tuple +import logging import numpy as np from braket.circuits import Circuit from scipy.optimize import minimize -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -30,38 +32,36 @@ class QAOA(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() - self.submodule_options = ["LocalSimulator", "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", - "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3"] + self.submodule_options = [ + "LocalSimulator", "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + ] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "scipy", - "version": "1.12.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "amazon-braket-sdk", "version": "1.87.0"}, + {"name": "scipy", "version": "1.12.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony": from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 @@ -81,27 +81,27 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns the configurable settings for this solver - - :return: - .. code-block:: python - - return { - "shots": { # number measurements to make on circuit - "values": list(range(10, 500, 30)), - "description": "How many shots do you need?" - }, - "opt_method": { - "values": ["Powell", "Nelder-Mead"], - "description": "Which optimization method do you want?" - }, - "depth": { - "values": [3], - "description": "Which circuit depth for QAOA do you want?" - } - } + Returns the configurable settings for this solver. + + :return: Dictionary of parameter settings + .. code-block:: python + + return { + "shots": { # number measurements to make on circuit + "values": list(range(10, 500, 30)), + "description": "How many shots do you need?" + }, + "opt_method": { + "values": ["Powell", "Nelder-Mead"], + "description": "Which optimization method do you want?" + }, + "depth": { + "values": [3], + "description": "Which circuit depth for QAOA do you want?" + } + } """ return { @@ -121,7 +121,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -134,22 +134,16 @@ class Config(TypedDict): opt_method: str depth: int - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> (any, float): + def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Any, float]: """ Run QAOA algorithm on Ising. :param mapped_problem: dictionary with the keys 'J' and 't' - :type mapped_problem: any - :param device_wrapper: instance of device - :type device_wrapper: any - :param config: - :type config: Config + :param device_wrapper: Instance of device + :param config: Solver configuration settings :param kwargs: no additionally settings needed - :type kwargs: any :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - j = mapped_problem['J'] if np.any(np.iscomplex(j)): logging.warning("The problem matrix of the QAOA solver contains imaginary numbers." @@ -161,7 +155,7 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs n_qubits = j.shape[0] # User-defined hypers - depth = config['depth'] # circuit depth for QAOA + depth = config['depth'] opt_method = config['opt_method'] # SLSQP, COBYLA, Nelder-Mead, BFGS, Powell, ... # initialize reference solution (simple guess) @@ -194,24 +188,23 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs # kick off training start = start_time_measurement() - # result_energy, result_angle, tracker _, _, tracker = train( - device=device_wrapper.get_device(), options=options, p=depth, ising=j, n_qubits=n_qubits, + device=device_wrapper.get_device(), + options=options, + p=depth, ising=j, + n_qubits=n_qubits, n_shots=config['shots'], - opt_method=opt_method, tracker=tracker, s3_folder=device_wrapper.s3_destination_folder, verbose=True) + opt_method=opt_method, + tracker=tracker, + s3_folder=device_wrapper.s3_destination_folder, + verbose=True + ) time_to_solve = end_time_measurement(start) - # print execution time - # logging.info('Code execution time [sec]: ' + (end - start)) - - # print optimized results + # Log optimized results logging.info(f"Optimal energy: {tracker['optimal_energy']}") logging.info(f"Optimal classical bitstring: {tracker['optimal_bitstring']}") - # visualize the optimization process - # cycles = np.arange(1, tracker['count']) - # optim_classical = tracker['global_energies'] - # TODO maybe save this plot # plt.plot(cycles, optim_classical) # plt.xlabel('optimization cycle') @@ -274,11 +267,10 @@ def cost_circuit(gamma, n_qubits, ising, device): # for Rigetti we decompose ZZ using CNOT gates if device.name in ["Rigetti", "Aspen-9"]: # TODO make this more flexible gate = ZZgate(qubit_pair[0], qubit_pair[1], gamma * int_strength) - circ.add(gate) # classical simulators and IonQ support ZZ gate else: gate = Circuit().zz(qubit_pair[0], qubit_pair[1], angle=2 * gamma * int_strength) - circ.add(gate) + circ.add(gate) return circ @@ -330,15 +322,14 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol if device.name in ["DefaultSimulator", "StateVectorSimulator"]: task = device.run(qaoa_circuit, shots=n_shots) else: - task = device.run( - qaoa_circuit, s3_folder, shots=n_shots, poll_timeout_seconds=3 * 24 * 60 * 60 - ) + task = device.run(qaoa_circuit, s3_folder, shots=n_shots, poll_timeout_seconds=3 * 24 * 60 * 60) # get ID and status of submitted task task_id = task.id status = task.state() logging.info(f"ID of task: {task_id}") logging.info(f"Status of task: {status}") + # wait for job to complete while status != 'COMPLETED': status = task.state() @@ -349,9 +340,6 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol result = task.result() logging.info(result) - # get metadata - # metadata = result.task_metadata - # convert results (0 and 1) to ising (-1 and 1) meas_ising = result.measurements meas_ising[meas_ising == 0] = -1 @@ -368,8 +356,7 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol # store optimal (classical) result/bitstring if energy_min < tracker["optimal_energy"]: - tracker.update({"optimal_energy": energy_min}) - tracker.update({"optimal_bitstring": optimal_string}) + tracker.update({"optimal_energy": energy_min, "optimal_bitstring": optimal_string}) # store global minimum tracker["global_energies"].append(tracker["optimal_energy"]) @@ -397,7 +384,6 @@ def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_ function to run QAOA algorithm for given, fixed circuit depth p """ logging.info("Starting the training.") - logging.info("==================================" * 2) logging.info(f"OPTIMIZATION for circuit depth p={p}") diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index d854665e..d145f717 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import logging -from typing import Tuple, TypedDict +from typing import Tuple, TypedDict, Dict, Any, List import numpy as np @@ -24,7 +25,8 @@ from qiskit_algorithms.optimizers import POWELL, SPSA, COBYLA from qiskit_algorithms.minimum_eigensolvers import VQE, QAOA, NumPyMinimumEigensolver -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class QiskitQAOA(Solver): @@ -34,88 +36,73 @@ class QiskitQAOA(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() - # self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu", "ibm_eagle"] self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu"] self.ry = None @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "qiskit-algorithms", - "version": "0.3.0" - } - + {"name": "qiskit", "version": "1.1.0"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "qiskit-algorithms", "version": "0.3.0"} ] def get_default_submodule(self, option: str) -> Core: - if option == "qasm_simulator": - from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - return HelperClass("qasm_simulator") - elif option == "qasm_simulator_gpu": + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ + if option in ["qasm_simulator", "qasm_simulator_gpu"]: from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - return HelperClass("qasm_simulator_gpu") - # elif option == "ibm_eagle": - # from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - # return HelperClass("ibm_eagle") + return HelperClass(option) else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ Returns the configurable settings for this solver. - :return: - .. code-block:: python + :return: Dictionary of configurable settings + .. code-block:: python - return { - "shots": { # number measurements to make on circuit - "values": list(range(10, 500, 30)), - "description": "How many shots do you need?" - }, - "iterations": { # number measurements to make on circuit - "values": [1, 5, 10, 20, 50, 75], - "description": "How many iterations do you need? Warning: When using\ - the IBM Eagle Device you should only choose a lower number of\ - iterations, since a high number would lead to a waiting time that\ - could take up to mulitple days!" - }, - "depth": { - "values": [2, 3, 4, 5, 10, 20], - "description": "How many layers for QAOA (Parameter: p) do you want?" - }, - "method": { - "values": ["classic", "vqe", "qaoa"], - "description": "Which Qiskit solver should be used?" - }, - "optimizer": { - "values": ["POWELL", "SPSA", "COBYLA"], - "description": "Which Qiskit solver should be used? Warning: When\ - using the IBM Eagle Device you should not use the SPSA optimizer,\ - since it is not suited for only one evaluation!" - } - } + return { + "shots": { # number measurements to make on circuit + "values": list(range(10, 500, 30)), + "description": "How many shots do you need?" + }, + "iterations": { # number measurements to make on circuit + "values": [1, 5, 10, 20, 50, 75], + "description": "How many iterations do you need? Warning: When using\ + the IBM Eagle Device you should only choose a lower number of\ + iterations, since a high number would lead to a waiting time that\ + could take up to mulitple days!" + }, + "depth": { + "values": [2, 3, 4, 5, 10, 20], + "description": "How many layers for QAOA (Parameter: p) do you want?" + }, + "method": { + "values": ["classic", "vqe", "qaoa"], + "description": "Which Qiskit solver should be used?" + }, + "optimizer": { + "values": ["POWELL", "SPSA", "COBYLA"], + "description": "Which Qiskit solver should be used? Warning: When\ + using the IBM Eagle Device you should not use the SPSA optimizer,\ + since it is not suited for only one evaluation!" + } + } """ return { @@ -164,39 +151,31 @@ class Config(TypedDict): method: str @staticmethod - def normalize_data(data: any, scale: float = 1.0) -> any: + def normalize_data(data: Any, scale: float = 1.0) -> Any: """ Not used currently, as I just scale the coefficients in the qaoa_operators_from_ising. - :param data: - :type data: any - :param scale: - :type scale: float - :return: scaled data - :rtype: any + :param data: Data to normalize + :param scale: Scaling factor + :return: Normalized data """ return scale * data / np.max(np.abs(data)) - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> (any, float): + def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Any, float]: """ Run Qiskit QAOA algorithm on Ising. :param mapped_problem: dictionary with the keys 'J' and 't' - :type mapped_problem: any - :param device_wrapper: instance of device - :type device_wrapper: any - :param config: - :type config: Config + :param device_wrapper: Instance of device + :param config: Config object for the solver :param kwargs: no additionally settings needed - :type kwargs: any :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - J = mapped_problem['J'] t = mapped_problem['t'] start = start_time_measurement() ising_op = self._get_pauli_op((t, J)) + if config["method"] == "classic": algorithm = NumPyMinimumEigensolver() else: @@ -222,34 +201,21 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs # run actual optimization algorithm try: result = algorithm.compute_minimum_eigenvalue(ising_op) - print('result',result) except ValueError as e: logging.error(f"The following ValueError occurred in module QiskitQAOA: {e}") logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") from e + best_bitstring = self._get_best_solution(result) return best_bitstring, end_time_measurement(start), {} + + def _get_best_solution(self, result) -> Any: + """ + Gets the best solution from the result. - @staticmethod - def _get_quantum_instance(device_wrapper: any) -> any: - backend = Aer.get_backend("qasm_simulator") - if device_wrapper.device == 'qasm_simulator_gpu': - logging.info("Using GPU simulator") - backend.set_options(device='GPU') - backend.set_options(method='statevector_gpu') - # elif device_wrapper.device == 'ibm_eagle': - # logging.info("Using IBM Eagle") - # ibm_quantum_token = os.environ.get('ibm_quantum_token') - # service = QiskitRuntimeService(channel="ibm_quantum", token=ibm_quantum_token) - # backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - else: - logging.info("Using CPU simulator") - backend.set_options(device='CPU') - backend.set_options(method='statevector') - backend.set_options(max_parallel_threads=48) - return backend - - def _get_best_solution(self, result) -> any: + :param result: Result from the quantum algorithm + :return: Best bitstring solution + """ if self.ry is not None: if hasattr(result, "optimal_point"): para_dict = dict(zip(self.ry.parameters, result.optimal_point)) @@ -262,16 +228,22 @@ def _get_best_solution(self, result) -> any: else: raise AttributeError("The result object does not have 'optimal_point' or 'eigenstate' attributes.") else: - # If self.ry is None if hasattr(result, "eigenstate"): eigvec = result.eigenstate else: raise AttributeError("The result object does not have 'eigenstate'.") + best_bitstring = OptimizationApplication.sample_most_likely(eigvec) return best_bitstring @staticmethod - def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> object: + def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> SparsePauliOp: + """ + Creates a Pauli operator from the given Ising model representation + + :param ising: Tuple with linear and quandratic terms + .return: SparsePauliOp representing the Ising model + """ pauli_list = [] number_qubits = len(ising[0]) @@ -299,20 +271,5 @@ def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> object: pauli_str = "".join(pauli_str_list) pauli_list.append((pauli_str, complex(x))) - # for key, value in ising[0].items(): - # pauli_str = "I"*number_qubits - # pauli_str_list = list(pauli_str) - # pauli_str_list[key] = "Z" - # pauli_str = "".join(pauli_str_list) - # pauli_list.append((pauli_str, value)) - # - # for key, value in ising[1].items(): - # pauli_str = "I"*number_qubits - # pauli_str_list = list(pauli_str) - # pauli_str_list[key[0]] = "Z" - # pauli_str_list[key[1]] = "Z" - # pauli_str = "".join(pauli_str_list) - # pauli_list.append((pauli_str, value)) - isingOp =SparsePauliOp.from_list(pauli_list) return isingOp diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index 9b9050b2..ef4820b6 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Any, Tuple import random -import networkx +import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,70 +28,63 @@ class RandomPVC(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] - - def get_default_submodule(self, option: str) -> any: + return [{"name": "networkx", "version": "3.2.1"}] + + def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: """ Solve the PVC graph in a greedy fashion. - :param mapped_problem: graph representing a PVC problem - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work + # Deep copy since we are modifying the graph. This ensures that the original graph remains unchanges # with a different graph mapped_problem = mapped_problem.copy() start = start_time_measurement() + # We always start at the base node current_node = ((0, 0), 1, 1) idx = 1 @@ -101,9 +95,10 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi # to the same seam while len(mapped_problem.nodes) > 2: # Get the random neighbor edge from the current node - next_node = random.choice([x for x in mapped_problem.edges(current_node[0], data=True) if - x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] - and x[2]['t_start'] == current_node[2]]) + next_node = random.choice([ + x for x in mapped_problem.edges(current_node[0], data=True) + if x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] + ]) next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) # Make the step - add distance to cost, add the best node to tour, @@ -113,6 +108,7 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) + current_node = next_node idx += 1 diff --git a/src/modules/solvers/RandomClassicalSAT.py b/src/modules/solvers/RandomClassicalSAT.py index cc247afe..76c3dba9 100644 --- a/src/modules/solvers/RandomClassicalSAT.py +++ b/src/modules/solvers/RandomClassicalSAT.py @@ -12,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Dict, Tuple, Any import numpy as np from pysat.formula import WCNF +import logging -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -33,22 +35,15 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "python-sat", - "version": "1.8.dev13" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "python-sat", "version": "1.8.dev13"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: @@ -58,40 +53,31 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) -> (list, float): + def run(self, mapped_problem: WCNF, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[List, float]: """ - The given application is a problem instance from the pysat library. This generates a random solution to the - problem. + The given application is a problem instance from the pysat library. + This generates a random solution to the problem. - :param mapped_problem: - :type mapped_problem: WCNF + :param mapped_problem: The WCNF representation of the SAT problem :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - logging.info( f"Got problem with {mapped_problem.nv} variables, {len(mapped_problem.hard)} constraints and" f" {len(mapped_problem.soft)} tests." diff --git a/src/modules/solvers/RandomClassicalTSP.py b/src/modules/solvers/RandomClassicalTSP.py index 0e289e67..f0378d12 100644 --- a/src/modules/solvers/RandomClassicalTSP.py +++ b/src/modules/solvers/RandomClassicalTSP.py @@ -12,40 +12,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, List, Any, Dict, Tuple import random import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class RandomTSP(Solver): """ - Classical Random Solver the TSP + Classical Random Solver the TSP. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: if option == "Local": @@ -54,58 +49,48 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. :return: empty dict - :rtype: dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: """ Solve the TSP graph in a greedy fashion. - :param mapped_problem: graph representing a TSP - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a TSP :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - start = start_time_measurement() source = nx.utils.arbitrary_element(mapped_problem) nodeset = set(mapped_problem) nodeset.remove(source) tour = [source] + while nodeset: next_node = random.choice(list(nodeset)) tour.append(next_node) nodeset.remove(next_node) tour.append(tour[0]) - # We remove the duplicate node as we don't want a cycle + # Remove the duplicate node as we don't want a cycle # https://stackoverflow.com/a/7961390/10456906 tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later - result = {} - for idx, node in enumerate(tour): - result[(node, idx)] = 1 - # Tour needs to look like + result = {(node, idx): 1 for idx, node in enumerate(tour)} + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/ReverseGreedyClassicalPVC.py b/src/modules/solvers/ReverseGreedyClassicalPVC.py index f040353c..1f45abfc 100644 --- a/src/modules/solvers/ReverseGreedyClassicalPVC.py +++ b/src/modules/solvers/ReverseGreedyClassicalPVC.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Any, List, Dict, Tuple -import networkx +import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,12 +28,18 @@ class ReverseGreedyClassicalPVC(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -40,30 +47,21 @@ def get_default_submodule(self, option: str) -> Core: raise NotImplementedError(f"Device Option {option} not implemented") @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] - """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] - - def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + return [{"name": "networkx", "version": "3.2.1"}] - :return: empty dict - :rtype: dict + def get_parameter_options(self) -> Dict: """ - return { + Returns empty dict as this solver has no configurable settings. - } + :return: Empty dict + """ + return {} class Config(TypedDict): """ @@ -71,26 +69,22 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: """ Solve the PVC graph in a greedy fashion. We take the worst choice at each step. :param mapped_problem: graph representing a PVC problem - :type mapped_problem: networkx.Graph :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dicT + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Need to deep copy since we are modifying the graph in this function. + # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() start = start_time_measurement() + # We always start at the base node current_node = ((0, 0), 1, 1) idx = 1 @@ -102,10 +96,15 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi while len(mapped_problem.nodes) > 2: # Get the minimum neighbor edge from the current node # TODO This only works if the artificial high edge weights are exactly 100000 - next_node = max((x for x in mapped_problem.edges(current_node[0], data=True) if - x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] and - x[2]['weight'] != 100000), - key=lambda x: x[2]['weight']) + next_node = max( + ( + x for x in mapped_problem.edges(current_node[0], data=True) + if x[2]['c_start'] == current_node[1] + and x[2]['t_start'] == current_node[2] + and x[2]['weight'] != 100000 + ), + key=lambda x: x[2]['weight'] + ) next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) # Make the step - add distance to cost, add the best node to tour, @@ -115,6 +114,7 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) + current_node = next_node idx += 1 diff --git a/src/modules/solvers/ReverseGreedyClassicalTSP.py b/src/modules/solvers/ReverseGreedyClassicalTSP.py index 8f0eb60e..b40676fe 100644 --- a/src/modules/solvers/ReverseGreedyClassicalTSP.py +++ b/src/modules/solvers/ReverseGreedyClassicalTSP.py @@ -12,59 +12,58 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict +from typing import TypedDict, Dict, List, Any, Tuple -import networkx +import networkx as nx from networkx.algorithms import approximation as approx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class ReverseGreedyClassicalTSP(Solver): """ - Classical Reverse Greedy Solver for the TSP. We take the worst choice at each step. + Classical Reverse Greedy Solver for the TSP. + We take the worst choice at each step. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> dict: + def get_parameter_options(self) -> Dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ @@ -72,28 +71,25 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Config, **kwargs: any) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Any) -> Tuple[Dict, float]: """ Solve the TSP graph in a greedy fashion. - :param mapped_problem: graph representing a TSP - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a TSP :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Need to deep copy since we are modifying the graph in this function. + # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() + # Let's flip the edge weights to take the worst node every time instead of the best for _, _, d in mapped_problem.edges(data=True): d['weight'] = -1.0 * d['weight'] + start = start_time_measurement() tour = approx.greedy_tsp(mapped_problem) @@ -103,8 +99,6 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later - result = {} - for idx, node in enumerate(tour): - result[(node, idx)] = 1 - # Tour needs to look like + result = {(node, idx): 1 for idx, node in enumerate(tour)} + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/Solver.py b/src/modules/solvers/Solver.py index dc08beba..e32bd374 100644 --- a/src/modules/solvers/Solver.py +++ b/src/modules/solvers/Solver.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from modules.Core import * +from typing import List, Dict, Any, Tuple +from abc import ABC, abstractmethod +from modules.Core import Core class Solver(Core, ABC): @@ -22,38 +23,29 @@ class Solver(Core, ABC): defined objective function. """ - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: """ The actual solving process is done here, as we have the device, which got provided by the device submodule, and the problem data provided by the parent module. :param input_data: Data passed to the run function of the solver - :type input_data: any - :param config: solver config - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param config: Solver config + :param kwargs: Optional keyword arguments :return: Output and time needed - :rtype: (any, float) """ output, elapsed_time, additional_metrics = self.run(self.preprocessed_input, input_data, config, **kwargs) self.metrics.add_metric_batch(additional_metrics) return output, elapsed_time @abstractmethod - def run(self, mapped_problem, device_wrapper, config, **kwargs) -> (any, float, dict): + def run(self, mapped_problem, device_wrapper, config, **kwargs) -> Tuple[Any, float, Dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. - :param mapped_problem: a representation of the problem that the solver can solve - :type mapped_problem: any - :param device_wrapper: a device the solver can leverage for the algorithm - :type device_wrapper: any - :param config: settings for the solver such as hyperparameters - :type config: any - :param kwargs: optional additional settings - :type kwargs: any + :param mapped_problem: A representation of the problem that the solver can solve + :param device_wrapper: A device the solver can leverage for the algorithm + :param config: Settings for the solver such as hyperparameters + :param kwargs: Optional additional settings :return: Solution, the time it took to compute it and some optional additional information - :rtype: tuple(any, float, dict) """ pass From d7211495872dee08be312db2434bc08e54421576 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 2 Oct 2024 16:07:29 +0200 Subject: [PATCH 04/40] Refactor code for PEP8 compliance and improved readability --- .../data/data_handler/DiscreteData.py | 4 ++-- .../mappings/CustomQiskitNoisyBackend.py | 8 +------- .../generative_modeling/mappings/Library.py | 3 +-- .../mappings/PresetQiskitNoisyBackend.py | 1 - .../transformations/MinMax.py | 12 +++++------ .../transformations/PIT.py | 1 - .../applications/optimization/ACL/ACL.py | 14 ++++++------- .../optimization/ACL/mappings/ISING.py | 7 ++++--- .../optimization/ACL/mappings/QUBO.py | 3 ++- .../applications/optimization/MIS/__init__.py | 1 - .../optimization/MIS/data/graph_layouts.py | 15 +++++++------- .../applications/optimization/Optimization.py | 2 +- .../applications/optimization/PVC/PVC.py | 20 ++++++++++++------- .../optimization/PVC/mappings/QUBO.py | 7 ++++--- .../applications/optimization/SAT/SAT.py | 4 ++-- .../applications/optimization/SAT/__init__.py | 1 + .../optimization/SAT/mappings/DinneenQUBO.py | 8 ++++---- .../optimization/SCP/data/__init__.py | 2 +- .../optimization/SCP/mappings/qubovertQUBO.py | 3 ++- 19 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py index 20790057..ebcc3ab8 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py @@ -12,17 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Any, List, Dict, Tuple import itertools import logging from pprint import pformat +from typing import TypedDict, List, Dict, Tuple import numpy as np from modules.circuits.CircuitCardinality import CircuitCardinality -from utils import start_time_measurement, end_time_measurement from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import DataHandler from modules.applications.QML.generative_modeling.data.data_handler.MetricsGeneralization import MetricsGeneralization +from utils import start_time_measurement, end_time_measurement class DiscreteData(DataHandler): diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py index 748e8cb3..8d6bee09 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -263,13 +263,7 @@ def select_backend(config: str, n_qubits: int) -> Backend: return backend - def get_execute_circuit( - self, - circuit: QuantumCircuit, - backend: Backend, - config: str, - config_dict: dict - ) -> tuple[any, any]: + def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index 8105da40..2a26ac12 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -91,8 +91,7 @@ def sequence_to_circuit(self, input_data: Dict) -> Dict: @staticmethod @abstractmethod - def get_execute_circuit(circuit: Any, backend: Any, config: str, config_dict: Dict) -> ( - tuple)[any, any]: + def get_execute_circuit(circuit: Any, backend: Any, config: str, config_dict: Dict) -> (tuple)[any, any]: """ This method combines the circuit implementation and the selected backend and returns a function that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py index c3a020f1..e1433e48 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -388,5 +388,4 @@ def get_FakeBackend(self, noise_configuration: str, num_qubits: int) -> Backend: noise_model = NoiseModel.from_backend(backend) logging.info(f'Using {backend_name} with coupling map: {backend.coupling_map}') logging.info(f'Using {backend_name} with noise model: {noise_model}') - return AerSimulator.from_backend(backend) diff --git a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py index a349bfd8..809d65d5 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py @@ -189,9 +189,9 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: :param data: Data to be fitted :return: fitted data """ - self.data_min = data.min() - self.data_max = data.max() - self.data_min - data = (data - self.data_min) / self.data_max + data_min = data.min() + data_max = data.max() - data_min + data = (data - data_min) / data_max return data @@ -202,7 +202,7 @@ def inverse_transform(self, data: np.ndarray) -> np.ndarray: :param data: Data to be fitted :return: data in original space """ - self.data_min = data.min() - self.data_max = data.max() - self.data_min + data_min = data.min() + data_max = data.max() - data_min - return data * self.data_max + self.data_min + return data * data_max + data_min diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/QML/generative_modeling/transformations/PIT.py index f257992f..03ea6f14 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/QML/generative_modeling/transformations/PIT.py @@ -245,5 +245,4 @@ def emp_integral_trans(self, data: np.ndarray) -> np.ndarray: length = data.size ecdf = np.linspace(0, 1, length, dtype=np.float64) ecdf_biject = ecdf[rank] - return ecdf_biject diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index d7352f9a..3521cc3e 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -227,7 +227,7 @@ def _generate_tiny_model(self, df, vehicles): for t in plats_t: prob += pulp.lpSum( weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[t] for v in vecs) <= wt[t] - + self.application = prob def _generate_small_model(self, df, vehicles): @@ -252,7 +252,7 @@ def _generate_small_model(self, df, vehicles): # max. weight on p, if sp is used wsp = [28, 28, 28] - class_list, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) + _, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) # Set of available cars vecs = set(range(len(vehicles))) @@ -348,7 +348,7 @@ def _generate_small_model(self, df, vehicles): for p_t in plats_t: prob += pulp.lpSum( weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) <= wt[p_t] - + self.application = prob def _generate_full_model(self, df, vehicles): @@ -609,9 +609,9 @@ def _generate_full_model(self, df, vehicles): for v in vecs) \ <= hmax_trailer[h] - + self.application = prob - + def _get_vehicle_params(self, df, vehicles): """ Extract vehicle parameters for the problem formulation @@ -631,7 +631,7 @@ def _get_vehicle_params(self, df, vehicles): length_list[i] = int(df_new["Length"].iloc[0]) height_list[i] = int(df_new["Height"].iloc[0]) weight_list[i] = int(df_new["Weight"].iloc[0]) - + return class_list, length_list, height_list, weight_list def validate(self, solution: Any) -> Tuple[bool, float]: @@ -669,7 +669,7 @@ def evaluate(self, solution: Any) -> Tuple[float, float]: variables = solution.get("variables", {}) assignments = [key for key in variables if variables[key] > 0] - + logging.info(f"vehicle-to-platform assignments (platform, vehicle): {assignments}") return objective_value, end_time_measurement(start) diff --git a/src/modules/applications/optimization/ACL/mappings/ISING.py b/src/modules/applications/optimization/ACL/mappings/ISING.py index 91dc449e..74ca774b 100644 --- a/src/modules/applications/optimization/ACL/mappings/ISING.py +++ b/src/modules/applications/optimization/ACL/mappings/ISING.py @@ -21,6 +21,7 @@ from qiskit_optimization.converters import QuadraticProgramToQubo from modules.applications.Mapping import Mapping +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -112,7 +113,7 @@ def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: rhs=-1 * constraint["constant"], name=constraint["name"] ) - + return qp def map(self, problem: dict, config: Config) -> tuple[dict, float]: @@ -144,7 +145,7 @@ def map(self, problem: dict, config: Config) -> tuple[dict, float]: for pauli_op in qubit_op: pauli_str, coeff = pauli_op.primitive.to_list()[0] index_pos_list = list(locate(pauli_str, lambda a: a == 'Z')) - + if len(index_pos_list) == 1: t_matrix[index_pos_list[0]] = coeff elif len(index_pos_list) == 2: @@ -163,7 +164,7 @@ def reverse_map(self, solution: dict) -> Tuple(dict, float): if np.any(solution == "-1"): solution = self._convert_ising_to_qubo(solution) - + result = {"status": [0]} variables = {} objective_value = 0 diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index c96a8b62..c6efe661 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -180,7 +180,8 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra if variable in argument and variable2 in argument and variable > variable2: parameter += argument[0] # this value is already taking into account the factor 2 from quadratic term - # For the variables on the diagonal, if the parameter is zero, we still have to check the sign in + # For the variables on the diagonal, if the parameter is zero + # we still have to check the sign in # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. elif isinstance(argument, str): if variable in argument and variable2 in argument and variable == variable2: diff --git a/src/modules/applications/optimization/MIS/__init__.py b/src/modules/applications/optimization/MIS/__init__.py index ec0e95aa..b7ca66cc 100644 --- a/src/modules/applications/optimization/MIS/__init__.py +++ b/src/modules/applications/optimization/MIS/__init__.py @@ -17,4 +17,3 @@ This module initialize the MIS package """ - diff --git a/src/modules/applications/optimization/MIS/data/graph_layouts.py b/src/modules/applications/optimization/MIS/data/graph_layouts.py index 79de4ac9..86183756 100644 --- a/src/modules/applications/optimization/MIS/data/graph_layouts.py +++ b/src/modules/applications/optimization/MIS/data/graph_layouts.py @@ -39,7 +39,7 @@ def generate_hexagonal_graph( Returns: nx.Graph: networkx Graph representing the hexagonal graph layout. """ - if not (0.0 < filling_fraction <= 1.0): + if not 0.0 < filling_fraction <= 1.0: raise ValueError("The filling fraction must be in the domain of (0.0, 1.0].") # Create a layout large enough to contain the desired number of atoms at @@ -77,19 +77,18 @@ def generate_hexagonal_graph( return hexagonal_graph -def _generate_edges( - node_positions: Dict[int, List[float]], - radius: float = R_rydberg -) -> list[tuple[int, int]]: - """Generate edges between vertices within a given distance 'radius', which +def _generate_edges(node_positions: Dict[int, List[float]], radius: float = R_rydberg) -> list[tuple[int, int]]: + """ + Generate edges between vertices within a given distance 'radius', which defaults to R_rydberg. Parameters: node_positions (dict): A dictionary with the node ids as keys, and the node coordinates as values. radius (float): When the distance between two nodes is smaller than this radius, an edge is generated between them. - + Returns: - list[tuple]: A list of 2-tuples. Each 2-tuple contains two different node ids and represents an edge between those nodes. + list[tuple]: A list of 2-tuples. Each 2-tuple contains two different node ids and + represents an edge between those nodes. """ edges = [] vertex_keys = list(node_positions.keys()) diff --git a/src/modules/applications/optimization/Optimization.py b/src/modules/applications/optimization/Optimization.py index 63c4f663..f38a203a 100644 --- a/src/modules/applications/optimization/Optimization.py +++ b/src/modules/applications/optimization/Optimization.py @@ -120,5 +120,5 @@ def postprocess(self, input_data: Any, config: dict, **kwargs) -> Tuple[Any, flo "time_to_validation": time_to_validation, "time_to_evaluation": time_to_evaluation }) - + return solution_validity, sum(filter(None, [time_to_process_solution, time_to_validation, time_to_evaluation])) diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index 8583d97e..f2330a3b 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -167,12 +167,14 @@ def generate_problem(self, config: Config) -> nx.Graph: all_possible_edges = [(edges[0], edges[1], t_start, t_end, c_start, c_end) for edges in all_possible_edges for c_end in config for c_start in config for t_end in tool for t_start in tool if edges[0] != edges[1]] - + missing_edges = [item for item in all_possible_edges if item not in current_edges] # add these edges with very high values for edge in missing_edges: - graph.add_edge(edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], weight=100000) + graph.add_edge( + edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], weight=100000 + ) logging.info("Created PVC problem with the following attributes:") logging.info(f" - Number of seams: {seams}") @@ -192,7 +194,6 @@ def process_solution(self, solution: dict) -> tuple[List, bool]: start_time = start_time_measurement() nodes = list(self.application.nodes()) start = ((0, 0), 1, 1) - route: list = [None] * int((len(self.application) - 1) / 2 + 1) visited_seams = [] @@ -224,7 +225,11 @@ def process_solution(self, solution: dict) -> tuple[List, bool]: idx = route.index(start) route = route[idx:] + route[:idx] - parsed_route = ' ->\n'.join([f' Node {visit[0][1]} of Seam {visit[0][0]} using config {visit[1]} & tool {visit[2]}' for visit in route]) + parsed_route = ' ->\n'.join( + [ + f' Node {visit[0][1]} of Seam {visit[0][0]} using config {visit[1]} & tool {visit[2]}' for visit in route + ] + ) logging.info(f"Route found:\n{parsed_route}") return route, end_time_measurement(start_time) @@ -241,7 +246,8 @@ def validate(self, solution: list) -> tuple[bool, float]: visited_seams = {seam[0][0] for seam in solution if seam is not None} if len(visited_seams) == len(solution): - logging.info(f"All {len(solution) - 1} seams and the base node got visited (We only need to visit 1 node per seam)") + logging.info(f"All {len(solution) - 1} seams and + the base node got visited (We only need to visit 1 node per seam)") return True, end_time_measurement(start) else: logging.error(f"Only {len(visited_seams) - 1} got visited") @@ -252,7 +258,7 @@ def evaluate(self, solution: list) -> tuple[float, float]: Calculates the tour length for a given valid tour :param solution: List containing the nodes of the solution - :return: Tour length, time it took to calculate the tour lengt + :return: Tour length, time it took to calculate the tour length """ start = start_time_measurement() @@ -261,7 +267,7 @@ def evaluate(self, solution: list) -> tuple[float, float]: for idx, _ in enumerate(solution[:-1]): edge = next( item for item in list(self.application[solution[idx][0]][solution[idx + 1][0]].values()) - if item["c_start"] == solution[idx][1] and item["t_start"] == solution[idx][2] and + if item["c_start"] == solution[idx][1] and item["t_start"] == solution[idx][2] and item["c_end"] == solution[idx + 1][1] and item["t_end"] == solution[idx + 1][2] ) dist = edge['weight'] diff --git a/src/modules/applications/optimization/PVC/mappings/QUBO.py b/src/modules/applications/optimization/PVC/mappings/QUBO.py index c42e499d..7a567978 100644 --- a/src/modules/applications/optimization/PVC/mappings/QUBO.py +++ b/src/modules/applications/optimization/PVC/mappings/QUBO.py @@ -144,7 +144,8 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: ] for other_seam_node in other_seam_nodes: # penalize visiting other node of same seam - q[((node, c_start, t_start, pos_1), (other_seam_node, c_end, t_end, pos_2))] += 2.0 * lagrange + q[((node, c_start, t_start, pos_1), (other_seam_node, c_end, t_end, pos_2))] \ + += 2.0 * lagrange # Constraint to only visit a single node in a single timestep for pos in range(timesteps): @@ -166,8 +167,8 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: for c_end in config: nextpos = (pos + 1) % timesteps edge_u_v = next( - item for item in list(problem[u][v].values()) - if item["c_start"] == c_start and item["t_start"] == t_start and + item for item in list(problem[u][v].values()) + if item["c_start"] == c_start and item["t_start"] == t_start and item["c_end"] == c_end and item["t_end"] == t_end ) # since it is the other direction we switch start and end of tool and config diff --git a/src/modules/applications/optimization/SAT/SAT.py b/src/modules/applications/optimization/SAT/SAT.py index c7309a1b..9fa13371 100644 --- a/src/modules/applications/optimization/SAT/SAT.py +++ b/src/modules/applications/optimization/SAT/SAT.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TypedDict, List, Dict, Tuple, Any import nnf @@ -22,7 +23,6 @@ from modules.applications.Application import Application from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement -import logging class SAT(Optimization): @@ -300,7 +300,7 @@ def save(self, path: str, iter_count: int) -> None: """ with open(f"{path}/constraints_iter_{iter_count}.cnf", "w") as f_cons: dump( - obj=self.application["constraints"], + obj=self.application["constraints"], fp=f_cons, var_labels={str(literal): idx + 1 for idx, literal in enumerate(self.literals)} ) diff --git a/src/modules/applications/optimization/SAT/__init__.py b/src/modules/applications/optimization/SAT/__init__.py index 0ef93529..5e76bc8a 100644 --- a/src/modules/applications/optimization/SAT/__init__.py +++ b/src/modules/applications/optimization/SAT/__init__.py @@ -17,3 +17,4 @@ This module initializes the SAT application """ + diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py index 3a8d203c..c13d282f 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py @@ -97,12 +97,12 @@ def map(self, problem: Tuple[And, List], config: Config) -> Tuple[Dict, float]: lagrange *= len(soft) def _add_clause( - curr_qubo_dict: Dict[Tuple[int, int], float], - clause: Any, - pos: int, + curr_qubo_dict: Dict[Tuple[int, int], float], + clause: Any, + pos: int, weight: float ) -> Dict[Tuple[int, int], float]: - + """ Function that adds the QUBO terms corresponding to the clause and updates the QUBO dictionary accordingly. Additionally, the weight of the clause is taken into account. diff --git a/src/modules/applications/optimization/SCP/data/__init__.py b/src/modules/applications/optimization/SCP/data/__init__.py index 34b3f0c0..5826cf71 100644 --- a/src/modules/applications/optimization/SCP/data/__init__.py +++ b/src/modules/applications/optimization/SCP/data/__init__.py @@ -16,4 +16,4 @@ Module for SCP mappings This module initializes the SCP application -""" \ No newline at end of file +""" diff --git a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py index 1c88e884..164ffca0 100644 --- a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py +++ b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TypedDict, Dict, List, Tuple, Set + from qubovert.problems import SetCover from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement -import logging class QubovertQUBO(Mapping): From af1bbb68eca3bdfdf869d52bf2464f46f9c661b0 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 2 Oct 2024 16:28:19 +0200 Subject: [PATCH 05/40] Refactor code for PEP8 compliance and improved readability --- .../mappings/CustomQiskitNoisyBackend.py | 3 ++- .../QML/generative_modeling/mappings/Library.py | 2 +- src/modules/applications/optimization/PVC/PVC.py | 4 ++-- .../applications/optimization/PVC/mappings/ISING.py | 4 ++-- .../applications/optimization/PVC/mappings/QUBO.py | 9 +++++---- src/modules/applications/optimization/SAT/__init__.py | 1 - .../optimization/SAT/mappings/QubovertQUBO.py | 4 +++- .../applications/optimization/SAT/mappings/__init__.py | 2 +- src/modules/devices/Device.py | 1 + src/modules/solvers/Annealer.py | 2 +- src/modules/solvers/ClassicalSAT.py | 2 +- src/modules/solvers/GreedyClassicalPVC.py | 6 +++--- src/modules/solvers/MIPsolverACL.py | 2 +- src/modules/solvers/NeutralAtomMIS.py | 2 +- src/modules/solvers/PennylaneQAOA.py | 2 +- src/modules/solvers/QiskitQAOA.py | 8 ++++---- src/modules/solvers/RandomClassicalPVC.py | 7 ++++--- src/modules/solvers/RandomClassicalSAT.py | 4 +++- src/modules/solvers/RandomClassicalTSP.py | 2 +- src/modules/solvers/ReverseGreedyClassicalPVC.py | 2 +- src/modules/solvers/ReverseGreedyClassicalTSP.py | 4 ++-- 21 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py index 8d6bee09..cfa61c06 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -263,7 +263,8 @@ def select_backend(config: str, n_qubits: int) -> Backend: return backend - def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict) -> tuple[any, any]: + def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict + ) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index 2a26ac12..fe518d93 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -91,7 +91,7 @@ def sequence_to_circuit(self, input_data: Dict) -> Dict: @staticmethod @abstractmethod - def get_execute_circuit(circuit: Any, backend: Any, config: str, config_dict: Dict) -> (tuple)[any, any]: + def get_execute_circuit(self, circuit: Any, backend: Any, config: str, config_dict: Dict) -> Tuple[Any, Any]: """ This method combines the circuit implementation and the selected backend and returns a function that will be called during training. diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index f2330a3b..12faf91d 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -246,8 +246,8 @@ def validate(self, solution: list) -> tuple[bool, float]: visited_seams = {seam[0][0] for seam in solution if seam is not None} if len(visited_seams) == len(solution): - logging.info(f"All {len(solution) - 1} seams and - the base node got visited (We only need to visit 1 node per seam)") + logging.info(f"All {len(solution) - 1} seams and " + "the base node got visited (We only need to visit 1 node per seam)") return True, end_time_measurement(start) else: logging.error(f"Only {len(visited_seams) - 1} got visited") diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index bd32e82e..88b257e3 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -93,7 +93,7 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: """ start = start_time_measurement() - # Convert the PVC problem to QUBO + # Convert the PVC problem to QUBO qubo_mapping = QUBO() q, _ = qubo_mapping.map(problem, config) @@ -138,7 +138,7 @@ def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: logging.info(f"Key Mapping: {self.key_mapping}") result = {key: 1 if solution[self.key_mapping[key]] == 1 else 0 for key in self.key_mapping} - + return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: diff --git a/src/modules/applications/optimization/PVC/mappings/QUBO.py b/src/modules/applications/optimization/PVC/mappings/QUBO.py index 7a567978..7c6b8ee4 100644 --- a/src/modules/applications/optimization/PVC/mappings/QUBO.py +++ b/src/modules/applications/optimization/PVC/mappings/QUBO.py @@ -144,8 +144,9 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: ] for other_seam_node in other_seam_nodes: # penalize visiting other node of same seam - q[((node, c_start, t_start, pos_1), (other_seam_node, c_end, t_end, pos_2))] \ - += 2.0 * lagrange + q[( + (node, c_start, t_start, pos_1), (other_seam_node, c_end, t_end, pos_2)) + ] += 2.0 * lagrange # Constraint to only visit a single node in a single timestep for pos in range(timesteps): @@ -173,8 +174,8 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: ) # since it is the other direction we switch start and end of tool and config edge_v_u = next( - item for item in list(problem[v][u].values()) - if item["c_start"] == c_end and item["t_start"] == t_end and + item for item in list(problem[v][u].values()) + if item["c_start"] == c_end and item["t_start"] == t_end and item["c_end"] == c_start and item["t_end"] == t_start ) # going from u -> v diff --git a/src/modules/applications/optimization/SAT/__init__.py b/src/modules/applications/optimization/SAT/__init__.py index 5e76bc8a..0ef93529 100644 --- a/src/modules/applications/optimization/SAT/__init__.py +++ b/src/modules/applications/optimization/SAT/__init__.py @@ -17,4 +17,3 @@ This module initializes the SAT application """ - diff --git a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py index ce3d4543..7d48a72f 100644 --- a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TypedDict, List, Dict, Tuple, Any + from qubovert.sat import NOT, OR, AND from nnf import And + from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement -import logging class QubovertQUBO(Mapping): diff --git a/src/modules/applications/optimization/SAT/mappings/__init__.py b/src/modules/applications/optimization/SAT/mappings/__init__.py index cc4501cb..0ef93529 100644 --- a/src/modules/applications/optimization/SAT/mappings/__init__.py +++ b/src/modules/applications/optimization/SAT/mappings/__init__.py @@ -16,4 +16,4 @@ Module for SAT mappings This module initializes the SAT application -""" \ No newline at end of file +""" diff --git a/src/modules/devices/Device.py b/src/modules/devices/Device.py index 50a5f4d4..9c7d9357 100644 --- a/src/modules/devices/Device.py +++ b/src/modules/devices/Device.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from abc import ABC from typing import Dict, Any, Tuple from modules.Core import Core from utils import start_time_measurement, end_time_measurement diff --git a/src/modules/solvers/Annealer.py b/src/modules/solvers/Annealer.py index 064698e1..824f4a39 100644 --- a/src/modules/solvers/Annealer.py +++ b/src/modules/solvers/Annealer.py @@ -99,7 +99,7 @@ def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwarg logging.error("Please select another solver module.") logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") - + response = device.sample_qubo(Q, num_reads=config['number_of_reads']) time_to_solve = end_time_measurement(start) diff --git a/src/modules/solvers/ClassicalSAT.py b/src/modules/solvers/ClassicalSAT.py index d77d907b..dcbe71ec 100644 --- a/src/modules/solvers/ClassicalSAT.py +++ b/src/modules/solvers/ClassicalSAT.py @@ -19,7 +19,7 @@ from pysat.formula import WCNF from modules.solvers.Solver import Solver -from modules.Core import Core +from modules.Core import Core from utils import start_time_measurement, end_time_measurement diff --git a/src/modules/solvers/GreedyClassicalPVC.py b/src/modules/solvers/GreedyClassicalPVC.py index a5cbeab1..839ee542 100644 --- a/src/modules/solvers/GreedyClassicalPVC.py +++ b/src/modules/solvers/GreedyClassicalPVC.py @@ -17,7 +17,7 @@ import networkx as nx from modules.solvers.Solver import Solver -from modules.Core import Core +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -98,7 +98,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Any, **kwar if x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] ), key=lambda x: x[2]['weight']) - + next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) # Make the step - add distance to cost, add the best node to tour @@ -108,7 +108,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Any, **kwar to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) - + current_node = next_node idx += 1 diff --git a/src/modules/solvers/MIPsolverACL.py b/src/modules/solvers/MIPsolverACL.py index c8244917..fc6f3356 100644 --- a/src/modules/solvers/MIPsolverACL.py +++ b/src/modules/solvers/MIPsolverACL.py @@ -105,5 +105,5 @@ def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwarg for v in problem_instance.variables(): variables[v.name] = v.varValue solution_data["variables"] = variables - + return solution_data, end_time_measurement(start), {} diff --git a/src/modules/solvers/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py index d0fd3d0d..60f5fc0d 100644 --- a/src/modules/solvers/NeutralAtomMIS.py +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -96,7 +96,7 @@ def run(self, mapped_problem: Dict, device_wrapper: Any, config: Any, **kwargs: graph = mapped_problem.get('graph') nodes = list(graph.nodes()) edges = list(graph.edges()) - + logging.info(f"Got problem with {len(graph.nodes)} nodes, {len(graph.edges)} edges.") device = device_wrapper.get_device() diff --git a/src/modules/solvers/PennylaneQAOA.py b/src/modules/solvers/PennylaneQAOA.py index 783b3d5e..3fd45a3d 100644 --- a/src/modules/solvers/PennylaneQAOA.py +++ b/src/modules/solvers/PennylaneQAOA.py @@ -355,7 +355,7 @@ def cost_function(params): logging.error(e) logging.error("Run a smaller problem size or select another device.") raise e - + # Convert cost_before to a float, so it's easier to handle cost_before = float(cost_before) if iteration == 0: diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index d145f717..c1ad4aaa 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.py @@ -65,7 +65,7 @@ def get_default_submodule(self, option: str) -> Core: """ if option in ["qasm_simulator", "qasm_simulator_gpu"]: from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - return HelperClass(option) + return HelperClass(option) else: raise NotImplementedError(f"Device Option {option} not implemented") @@ -205,10 +205,10 @@ def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs logging.error(f"The following ValueError occurred in module QiskitQAOA: {e}") logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") from e - + best_bitstring = self._get_best_solution(result) return best_bitstring, end_time_measurement(start), {} - + def _get_best_solution(self, result) -> Any: """ Gets the best solution from the result. @@ -232,7 +232,7 @@ def _get_best_solution(self, result) -> Any: eigvec = result.eigenstate else: raise AttributeError("The result object does not have 'eigenstate'.") - + best_bitstring = OptimizationApplication.sample_most_likely(eigvec) return best_bitstring diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index ef4820b6..788ce1f0 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -96,8 +96,9 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k while len(mapped_problem.nodes) > 2: # Get the random neighbor edge from the current node next_node = random.choice([ - x for x in mapped_problem.edges(current_node[0], data=True) - if x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] + x for x in mapped_problem.edges(current_node[0], data=True) + if x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] and x[2]['t_start'] \ + == current_node[2] ]) next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) @@ -108,7 +109,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) - + current_node = next_node idx += 1 diff --git a/src/modules/solvers/RandomClassicalSAT.py b/src/modules/solvers/RandomClassicalSAT.py index 76c3dba9..1acfa9b0 100644 --- a/src/modules/solvers/RandomClassicalSAT.py +++ b/src/modules/solvers/RandomClassicalSAT.py @@ -13,9 +13,11 @@ # limitations under the License. from typing import TypedDict, List, Dict, Tuple, Any +import logging + import numpy as np + from pysat.formula import WCNF -import logging from modules.solvers.Solver import Solver from modules.Core import Core diff --git a/src/modules/solvers/RandomClassicalTSP.py b/src/modules/solvers/RandomClassicalTSP.py index f0378d12..eeae08a5 100644 --- a/src/modules/solvers/RandomClassicalTSP.py +++ b/src/modules/solvers/RandomClassicalTSP.py @@ -92,5 +92,5 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k # Parse tour so that it can be processed later result = {(node, idx): 1 for idx, node in enumerate(tour)} - + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/ReverseGreedyClassicalPVC.py b/src/modules/solvers/ReverseGreedyClassicalPVC.py index 1f45abfc..1dec4d78 100644 --- a/src/modules/solvers/ReverseGreedyClassicalPVC.py +++ b/src/modules/solvers/ReverseGreedyClassicalPVC.py @@ -99,7 +99,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k next_node = max( ( x for x in mapped_problem.edges(current_node[0], data=True) - if x[2]['c_start'] == current_node[1] + if x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] and x[2]['weight'] != 100000 ), diff --git a/src/modules/solvers/ReverseGreedyClassicalTSP.py b/src/modules/solvers/ReverseGreedyClassicalTSP.py index b40676fe..0b887cb8 100644 --- a/src/modules/solvers/ReverseGreedyClassicalTSP.py +++ b/src/modules/solvers/ReverseGreedyClassicalTSP.py @@ -82,7 +82,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k :return: Solution, the time it took to compute it and optional additional information """ - # Need to deep copy since we are modifying the graph in this function. + # Need to deep copy since we are modifying the graph in this function. # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() @@ -100,5 +100,5 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k # Parse tour so that it can be processed later result = {(node, idx): 1 for idx, node in enumerate(tour)} - + return result, end_time_measurement(start), {} From 71ae87e3e563205293bc77230bb7da12bd0a5042 Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 8 Oct 2024 09:23:32 +0200 Subject: [PATCH 06/40] Refactor code for PEP8 compliance and improved readability --- src/BenchmarkManager.py | 164 ++++++++--------- src/BenchmarkRecord.py | 59 ++---- src/ConfigManager.py | 139 ++++---------- src/Installer.py | 129 ++++--------- src/Metrics.py | 53 ++---- src/Plotter.py | 118 ++++++------ src/demo/instruction_demo.py | 28 ++- src/main.py | 49 +++-- src/modules/Core.py | 42 ++--- .../applications/optimization/ACL/ACL.py | 20 +- .../optimization/ACL/mappings/ISING.py | 24 ++- .../optimization/ACL/mappings/QUBO.py | 53 +++--- .../applications/optimization/MIS/MIS.py | 78 ++++---- .../optimization/MIS/data/graph_layouts.py | 15 +- .../optimization/MIS/mappings/NeutralAtom.py | 13 +- .../applications/optimization/Optimization.py | 31 ++-- .../applications/optimization/PVC/PVC.py | 91 ++++++---- .../PVC/data/createReferenceGraph.py | 5 +- .../optimization/PVC/mappings/ISING.py | 30 +-- .../optimization/PVC/mappings/QUBO.py | 26 +-- .../applications/optimization/SAT/SAT.py | 171 ++++++++++-------- .../optimization/SAT/mappings/ChoiISING.py | 64 ++++--- .../optimization/SAT/mappings/ChoiQUBO.py | 70 +++---- .../optimization/SAT/mappings/DinneenISING.py | 61 ++++--- .../optimization/SAT/mappings/DinneenQUBO.py | 109 +++++------ .../optimization/SAT/mappings/Direct.py | 57 +++--- .../optimization/SAT/mappings/QubovertQUBO.py | 64 +++---- .../applications/optimization/SCP/SCP.py | 69 ++++--- .../optimization/SCP/mappings/qubovertQUBO.py | 63 +++---- .../applications/optimization/TSP/TSP.py | 56 +++--- .../TSP/data/createReferenceGraph.py | 43 +++-- .../optimization/TSP/mappings/ISING.py | 104 +++++------ .../optimization/TSP/mappings/QUBO.py | 44 +++-- src/modules/training/Inference.py | 56 +++--- src/modules/training/QCBM.py | 164 ++++++++--------- src/modules/training/QGAN.py | 164 +++++++---------- src/modules/training/Training.py | 69 +++---- src/quark2_adapter/adapters.py | 156 +++++++--------- .../legacy_classes/Application.py | 96 ++++------ src/quark2_adapter/legacy_classes/Device.py | 32 ++-- src/quark2_adapter/legacy_classes/Mapping.py | 45 ++--- src/quark2_adapter/legacy_classes/Solver.py | 45 ++--- src/utils.py | 58 ++---- src/utils_mpi.py | 52 ++++-- 44 files changed, 1420 insertions(+), 1659 deletions(-) diff --git a/src/BenchmarkManager.py b/src/BenchmarkManager.py index cd543705..1a244f3c 100644 --- a/src/BenchmarkManager.py +++ b/src/BenchmarkManager.py @@ -21,7 +21,7 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import List, Dict, Optional +from typing import Optional import numpy as np @@ -30,7 +30,6 @@ from Plotter import Plotter from modules.Core import Core from utils import get_git_revision - from utils_mpi import get_comm comm = get_comm() @@ -55,10 +54,8 @@ def _prepend_instruction(result: tuple) -> tuple: the first entry of the returned list is an INSTRUCTION with PROCEED as default. - :param result: the list to which the instruction is to be prepended - :type result: tuple - :return: the list with an INSTRUCTION as first entry - :rtype: tuple + :param result: The tuple to which the instruction is to be prepended + :return: The tuple with an INSTRUCTION as first entry """ if isinstance(result[0], Instruction): return result @@ -71,10 +68,8 @@ def postprocess(module_instance: Core, *args, **kwargs) -> tuple: Wraps module_instance.postprocess such that the first entry of the result list is guaranteed to be an Instruction. See _prepend_instruction. - :param module_instance: the QUARK module on which to call postprocess - :type module_instance: Core - :return: the result list of module_instance.postprocess with an Instruction as first entry. - :rtype: tuple + :param module_instance: The QUARK module on which to call postprocess + :return: The result list of module_instance.postprocess with an Instruction as first entry. """ result = module_instance.postprocess(*args, **kwargs) return _prepend_instruction(result) @@ -85,10 +80,8 @@ def preprocess(module_instance: Core, *args, **kwargs) -> tuple: Wraps module_instance.preprocess such that the first entry of the result list is guaranteed to be an Instruction. See _prepend_instruction. - :param module_instance: the QUARK module on which to call preprocess - :type module_instance: Core - :return: the result list of module_instance.preprocess with an Instruction as first entry. - :rtype: tuple + :param module_instance: The QUARK module on which to call preprocess + :return: The result list of module_instance.preprocess with an Instruction as first entry. """ result = module_instance.preprocess(*args, **kwargs) return _prepend_instruction(result) @@ -104,9 +97,9 @@ class BenchmarkManager: def __init__(self, fail_fast: bool = False): """ - Constructor method + Constructor method. + :param fail_fast: Boolean whether a single failed benchmark run causes QUARK to fail - :type fail_fast: bool """ self.fail_fast = fail_fast self.application = None @@ -118,8 +111,9 @@ def __init__(self, fail_fast: bool = False): def load_interrupted_results(self) -> Optional[list]: """ - :return: the content of the results file from the QUARK run to be resumed or None. - :rtype: Optional[list] + Loads the interrupted results if available. + + :return: The content of the results file from the QUARK run to be resumed or None. """ if self.interrupted_results_path is None or not os.path.exists(self.interrupted_results_path): return None @@ -132,11 +126,7 @@ def _create_store_dir(self, store_dir: str = None, tag: str = None) -> None: Creates directory for a benchmark run. :param store_dir: Directory where the new directory should be created - :type store_dir: str - :param tag: prefix of the new directory - :type tag: str - :return: - :rtype: None + :param tag: Prefix of the new directory """ if store_dir is None: store_dir = Path.cwd() @@ -145,12 +135,19 @@ def _create_store_dir(self, store_dir: str = None, tag: str = None) -> None: Path(self.store_dir).mkdir(parents=True, exist_ok=True) self._set_logger() - def _resume_store_dir(self, store_dir) -> None: + def _resume_store_dir(self, store_dir: str) -> None: + """ + Resumes the existing store directory. + + :param store-dir: Directory to be resumed + """ self.store_dir = store_dir self._set_logger() def _set_logger(self) -> None: - # Also store the log file to the benchmark dir + """ + Sets up the logger to also write to a file in the store directory. + """ logger = logging.getLogger() formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") filehandler = logging.FileHandler(f"{self.store_dir}/logging.log") @@ -163,22 +160,18 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod Executes the benchmarks according to the given settings. :param benchmark_config_manager: Instance of BenchmarkConfigManager class, where config is already set. - :type benchmark_config_manager: ConfigManager - :param app_modules: the list of application modules as specified in the application modules configuration. - :type app_modules: list of dict - :param store_dir: target directory to store the results of the benchmark (if you decided to store it) - :type store_dir: str - :param interrupted_results_path: result file from which the information for the interrupted jobs will be read. + :param app_modules: The list of application modules as specified in the application modules configuration. + :param store_dir: Target directory to store the results of the benchmark (if you decided to store it) + :param interrupted_results_path: Result file from which the information for the interrupted jobs will be read. If store_dir is None the parent directory of interrupted_results_path will be used as store_dir. - :type interrupted_results_path: str - :rtype: None """ self.interrupted_results_path = interrupted_results_path if interrupted_results_path and not store_dir: self._resume_store_dir(os.path.dirname(interrupted_results_path)) else: self._create_store_dir(store_dir, tag=benchmark_config_manager.get_config()["application"]["name"].lower()) + benchmark_config_manager.save(self.store_dir) benchmark_config_manager.load_config(app_modules) self.application = benchmark_config_manager.get_app() @@ -187,7 +180,6 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod logging.info(f"Created Benchmark run directory {self.store_dir}") benchmark_backlog = benchmark_config_manager.start_create_benchmark_backlog() - self.run_benchmark(benchmark_backlog, benchmark_config_manager.get_reps()) # Wait until all MPI processes have finished and save results on rank 0 @@ -196,15 +188,12 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod results = self._collect_all_results() self._save_as_json(results) - def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: disable=R0915 + def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # pylint: disable=R0915 """ Goes through the benchmark backlog, which contains all the benchmarks to execute. :param repetitions: Number of repetitions - :type repetitions: int :param benchmark_backlog: List with the benchmark items to run - :type benchmark_backlog: list - :return: """ git_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ) git_revision_number, git_uncommitted_changes = get_git_revision(git_dir) @@ -219,9 +208,11 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di with open(f"{path}/application_config.json", 'w') as filehandler: json.dump(backlog_item["config"], filehandler, indent=2) job_status_count = {} + for i in range(1, repetitions + 1): logging.info(f"Running backlog item {idx_backlog + 1}/{len(benchmark_backlog)}," f" Iteration {i}/{repetitions}:") + # getting information of interrupted jobs job_info_with_meta_data = {} if interrupted_results: @@ -231,6 +222,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di break job_info = job_info_with_meta_data['module'] if job_info_with_meta_data else {} quark_job_status_name = job_info.get("quark_job_status") + if quark_job_status_name in (JobStatus.FINISHED.name, JobStatus.FAILED.name): quark_job_status = JobStatus.FINISHED if quark_job_status_name == JobStatus.FINISHED.name \ else JobStatus.FAILED @@ -241,20 +233,21 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di continue try: - - self.benchmark_record_template = BenchmarkRecord(idx_backlog, - datetime.today().strftime('%Y-%m-%d-%H-%M-%S'), - git_revision_number, git_uncommitted_changes, - i, repetitions) + self.benchmark_record_template = BenchmarkRecord( + idx_backlog, + datetime.today().strftime('%Y-%m-%d-%H-%M-%S'), + git_revision_number, git_uncommitted_changes, + i, repetitions + ) self.application.metrics.set_module_config(backlog_item["config"]) - instruction, problem, preprocessing_time = preprocess(self.application, None, - backlog_item["config"], - store_dir=path, rep_count=i, - previous_job_info=job_info) + instruction, problem, preprocessing_time = preprocess( + self.application, None, backlog_item["config"], + store_dir=path, rep_count=i, previous_job_info=job_info + ) self.application.metrics.set_preprocessing_time(preprocessing_time) self.application.save(path, i) - postprocessing_time = 0. + postprocessing_time = 0.0 benchmark_record = self.benchmark_record_template.copy() if instruction == Instruction.PROCEED: instruction, processed_input, benchmark_record = \ @@ -269,10 +262,11 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di quark_job_status = JobStatus.INTERRUPTED else: quark_job_status = JobStatus.FINISHED - self.application.metrics.add_metric("quark_job_status", quark_job_status.name) + self.application.metrics.add_metric("quark_job_status", quark_job_status.name) self.application.metrics.set_postprocessing_time(postprocessing_time) self.application.metrics.validate() + if benchmark_record is not None: benchmark_record.append_module_record_left(deepcopy(self.application.metrics)) benchmark_records.append(benchmark_record) @@ -339,25 +333,18 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di # pylint: disable=R0917 def traverse_config(self, module: dict, input_data: any, path: str, rep_count: int, previous_job_info: - dict = None) -> (any, BenchmarkRecord): + dict = None) -> tuple[any, BenchmarkRecord]: """ Executes a benchmark by traversing down the initialized config recursively until it reaches the end. Then traverses up again. Once it reaches the root/application, a benchmark run is finished. :param module: Current module - :type module: dict :param input_data: The input data needed to execute the current module. - :type input_data: any :param path: Path in case the modules want to store anything - :type path: str :param rep_count: The iteration count - :type rep_count: int - :param previous_job_info: information about previous job - :type previous_job_info: dict - :return: tuple with the output of this step and the according BenchmarkRecord - :rtype: tuple(any, BenchmarkRecord) + :param previous_job_info: Information about previous job + :return: Tuple with the output of this step and the according BenchmarkRecord """ - # Only the value of the dict is needed (dict has only one key) module = module[next(iter(module))] module_instance: Core = module["instance"] @@ -370,27 +357,29 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i submodule_job_info = previous_job_info['submodule'] module_instance.metrics.set_module_config(module["config"]) - instruction, module_instance.preprocessed_input, preprocessing_time\ - = preprocess(module_instance, input_data, - module["config"], - store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info) + instruction, module_instance.preprocessed_input, preprocessing_time = preprocess( + module_instance, input_data, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info + ) module_instance.metrics.set_preprocessing_time(preprocessing_time) output = None benchmark_record = self.benchmark_record_template.copy() postprocessing_time = 0.0 + if instruction == Instruction.PROCEED: # Check if end of the chain is reached if not module["submodule"]: # If we reach the end of the chain we create the benchmark record, fill it and then pass it up - instruction, module_instance.postprocessed_input, postprocessing_time = \ - postprocess( module_instance, - module_instance.preprocessed_input, - module["config"], store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info) + instruction, module_instance.postprocessed_input, postprocessing_time = postprocess( + module_instance, + module_instance.preprocessed_input, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info + ) output = module_instance.postprocessed_input else: instruction, processed_input, benchmark_record = self.traverse_config(module["submodule"], @@ -398,12 +387,12 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i rep_count, previous_job_info=submodule_job_info) if instruction == Instruction.PROCEED: - instruction, module_instance.postprocessed_input, postprocessing_time = \ - postprocess(module_instance, processed_input, - module["config"], - store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info) + instruction, module_instance.postprocessed_input, postprocessing_time = postprocess( + module_instance, processed_input, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info + ) output = module_instance.postprocessed_input else: output = processed_input @@ -414,12 +403,11 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i return instruction, output, benchmark_record - def _collect_all_results(self) -> List[Dict]: + def _collect_all_results(self) -> list[dict]: """ Collect all results from the multiple results.json. - :return: list of dicts with results - :rtype: List[Dict] + :return: List of dicts with results """ results = [] for filename in glob.glob(f"{self.store_dir}/**/results.json"): @@ -431,6 +419,11 @@ def _collect_all_results(self) -> List[Dict]: return results def _save_as_json(self, results: list) -> None: + """ + Saves benchmark results to a JSON file. + + :param results: Benchmark results to be saved + """ logging.info(f"Saving {len(results)} benchmark records to {self.store_dir}/results.json") with open(f"{self.store_dir}/results.json", 'w') as filehandler: json.dump(results, filehandler, indent=2) @@ -439,9 +432,7 @@ def summarize_results(self, input_dirs: list) -> None: """ Helper function to summarize multiple experiments. - :param input_dirs: list of directories - :type input_dirs: list - :rtype: None + :param input_dirs: List of directories """ self._create_store_dir(tag="summary") logging.info(f"Summarizing {len(input_dirs)} benchmark directories") @@ -454,11 +445,8 @@ def load_results(self, input_dirs: list = None) -> list: Load results from one or more results.json files. :param input_dirs: If you want to load more than 1 results.json (default is just 1, the one from the experiment) - :type input_dirs: list - :return: a list - :rtype: list + :return: A list """ - if input_dirs is None: input_dirs = [self.store_dir] @@ -473,7 +461,7 @@ def load_results(self, input_dirs: list = None) -> list: class NumpyEncoder(json.JSONEncoder): """ - Encoder that is used for json.dump(...) since numpy value items in dictionary might cause problems + Encoder that is used for json.dump(...) since numpy value items in dictionary might cause problems. """ def default(self, o: any): diff --git a/src/BenchmarkRecord.py b/src/BenchmarkRecord.py index 7016977e..0ea8be9c 100644 --- a/src/BenchmarkRecord.py +++ b/src/BenchmarkRecord.py @@ -22,26 +22,22 @@ class BenchmarkRecord: """ - The BenchmarkRecord class contains all the Metric instances and additional general information generated by a single - benchmark run. + The BenchmarkRecord class contains all the Metric instances and additional general information + generated by a single benchmark run. """ # pylint: disable=R0917 def __init__(self, benchmark_backlog_item_number: int, timestamp: str, git_revision_number: str, git_uncommitted_changes: str, repetition: int, total_repetitions: int): """ + Constructor method for BenchmarkRecord. + :param benchmark_backlog_item_number: Number of the item in the benchmark backlog - :type benchmark_backlog_item_number: int :param timestamp: Timestamp of the benchmark run - :type timestamp: str :param git_revision_number: Git revision number during the benchmark run - :type git_revision_number: str :param git_uncommitted_changes: Indication if there were uncommitted changes during the benchmark run - :type git_uncommitted_changes: str :param repetition: Number of current repetitions of the benchmark run - :type repetition: int :param total_repetitions: Number of total repetitions of the benchmark run - :type total_repetitions: int """ self.benchmark_backlog_item_number = benchmark_backlog_item_number self.timestamp = timestamp @@ -54,48 +50,38 @@ def __init__(self, benchmark_backlog_item_number: int, timestamp: str, git_revis self.linked_list_metrics = deque() @final - def append_module_record_right(self, module_record: Metrics): + def append_module_record_right(self, module_record: Metrics) -> None: """ - Adds Metrics instance to the end of the linked list + Adds Metrics instance to the end of the linked list. :param module_record: Metrics instance which should be appended to the end of the linked list - :type: Metrics - :rtype: None """ self.linked_list_metrics.append(module_record) @final def append_module_record_left(self, module_record: Metrics) -> None: """ - Adds Metrics instance to the beginning of the linked list + Adds Metrics instance to the beginning of the linked list. :param module_record: Metrics instance which should be appended to the beginning of the linked list - :type module_record: Metrics - :rtype: None """ self.linked_list_metrics.appendleft(module_record) @final def sum_up_times(self) -> None: """ - Sums up the recording timings - - :rtype: None + Sums up the recording timings. """ - self.total_time = 0.0 - for item in self.linked_list_metrics: - self.total_time += item.total_time + self.total_time = sum(item.total_time for item in self.linked_list_metrics) @final def hash_config(self, llist: deque) -> dict: """ Recursively traverses through linked list and returns a dictionary with the next module's name, config, - and subsequent submodule(s) + and subsequent submodule(s). :param llist: Linked list - :type llist: deque :return: Dictionary with the name and config of the first module in the linked list and subsequent submodule(s) - :rtype: dict """ next_item: Metrics = llist.popleft() return { @@ -111,7 +97,6 @@ def start_hash_config(self) -> int: Then generates hash with it. :return: Hash of the benchmark run config - :rtype: int """ list_copy = deepcopy(self.linked_list_metrics) # Hash assumes that all keys are strings! @@ -120,14 +105,11 @@ def start_hash_config(self) -> int: @final def linked_list_to_dict(self, llist: deque, module_level: int = 0) -> dict: """ - Recursively traverses through linked list and adds the items of the Metrics objects to one single dictionary + Recursively traverses through linked list and adds the items of the Metrics objects to one single dictionary. :param llist: Linked list - :type llist: deque :param module_level: Current level in chain (starts at 0) - :type module_level: int :return: Dictionary with the module, its level, and its submodule(s) - :rtype: dict """ next_item: Metrics = llist.popleft() return { @@ -140,10 +122,9 @@ def linked_list_to_dict(self, llist: deque, module_level: int = 0) -> dict: def start_linked_list_to_dict(self) -> dict: """ Helper function to start linked_list_to_dict function, which merges the various Metrics objects - to one dictionary + to one dictionary. :return: Resulting dictionary of linked_list_to_dict - :rtype: dict """ list_copy = deepcopy(self.linked_list_metrics) return self.linked_list_to_dict(list_copy) @@ -152,10 +133,9 @@ def start_linked_list_to_dict(self) -> dict: def get(self) -> dict: """ Returns a dictionary containing all benchmark information and a nested dictionary, in which each level - contains the metrics of the respective module + contains the metrics of the respective module. :return: Dictionary containing all the records of the benchmark - :rtype: dict """ return { "benchmark_backlog_item_number": self.benchmark_backlog_item_number, @@ -171,11 +151,11 @@ def get(self) -> dict: } @final - def copy(self) -> any: + def copy(self) -> "BenchmarkRecord": """ - Returns a copy of itself + Returns a copy of itself. + :return: Return copy of itself - :rtype: BenchmarkRecord """ return deepcopy(self) @@ -186,10 +166,12 @@ class BenchmarkRecordStored: It is a simple wrapper with the purpose to provide the same interface to the BenchmarkManager as the BenchmarkRecord does. """ + def __init__(self, record: dict): """ + Constructor method for BenchmarkRecordStored. + :param record: the record as dictionary - :type record: dict """ self.record = record @@ -198,14 +180,11 @@ def get(self) -> dict: Simply returns the dictionary as given to the constructor. :return: Dictionary as given to the constructor - :rtype: dict """ return self.record def sum_up_times(self) -> None: """ Dummy implementation which does nothing. - - :rtype: None """ pass diff --git a/src/ConfigManager.py b/src/ConfigManager.py index 4a68f0b7..32d70caa 100644 --- a/src/ConfigManager.py +++ b/src/ConfigManager.py @@ -53,7 +53,6 @@ class ConfigManager: """ A class responsible for generating/loading QUARK benchmark configs. Loading includes instantiation of the various modules specified in the config. - """ def __init__(self): @@ -66,8 +65,6 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: necessary to run the benchmark. :param app_modules: List of application modules as specified in the application modules configuration - :type app_modules: List of dict - :rtype: None """ application_answer = inquirer.prompt([inquirer.List('application', message="What application do you want?", @@ -79,12 +76,10 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: self.application = _get_instance_with_sub_options(app_modules, app_name) application_config = self.application.get_parameter_options() - application_config = ConfigManager._query_for_config( application_config, f"(Option for {application_answer['application']})") submodule_options = self.application.get_available_submodule_options() - submodule_answer = checkbox(key='submodules', message="What submodule do you want?", choices=submodule_options) @@ -102,28 +97,21 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: repetitions_answer = inquirer.prompt( [inquirer.Text('repetitions', message="How many repetitions do you want?", - validate=lambda _, x: re.match("\\d", x), - default=1 - )]) + validate=lambda _, x: re.match("\\d", x),default=1)]) self.config["repetitions"] = int(repetitions_answer["repetitions"]) def query_module(self, module: Core, module_friendly_name: str) -> ConfigModule: """ - Recursive function which queries every module and its submodule until end is reached + Recursive function which queries every module and its submodule until end is reached. :param module: Module instance - :type module: Core :param module_friendly_name: Name of the module - :type module_friendly_name: str :return: Config module with the choices of the user - :rtype: ConfigModule """ - module_config = module.get_parameter_options() module_config = ConfigManager._query_for_config(module_config, f"(Option for {module.__class__.__name__})") available_submodules = module.get_available_submodule_options() - submodule_answer = checkbox(key='submodules', message="What submodule do you want?", choices=available_submodules) @@ -132,7 +120,6 @@ def query_module(self, module: Core, module_friendly_name: str) -> ConfigModule: "config": module_config, "submodules": [self.query_module(module.get_submodule(sm), sm) for sm in submodule_answer["submodules"]] - } def set_config(self, config: BenchmarkConfig) -> None: @@ -140,23 +127,17 @@ def set_config(self, config: BenchmarkConfig) -> None: In case the user provides a config file, this function is used to set the config. :param config: Valid config file - :type config: BenchmarkConfig - :rtype: None """ - if ConfigManager.is_legacy_config(config): config = ConfigManager.translate_legacy_config(config) - self.config = config @staticmethod def is_legacy_config(config: dict) -> bool: """ - Checks if a QUARK 1 config was provided + Checks if a QUARK 1 config was provided. - :param config: Valid config file - :type config: dict - :rtype: bool + :param config: Valid config file :return: True if provided config is QUARK 1 config """ if "mapping" in config.keys(): @@ -168,14 +149,11 @@ def is_legacy_config(config: dict) -> bool: @staticmethod def translate_legacy_config(config: dict) -> BenchmarkConfig: """ - Translates the QUARK 1 config format to QUARK 2 format + Translates the QUARK 1 config format to QUARK 2 format. :param config: QUARK 1 config - :type config: dict :return: Translated Config - :rtype: BenchmarkConfig """ - logging.info("Trying to translate QUARK 1 config to QUARK 2 config format") try: translated_config = {key: config[key] for key in ["application", "repetitions"]} @@ -202,19 +180,13 @@ def translate_legacy_config(config: dict) -> BenchmarkConfig: @staticmethod def translate_legacy_config_helper(config_part: dict, module_key: str) -> list: """ - Helper function for translate_legacy_config, which translates the QUARK 1 config format to QUARK 2 format + Helper function for translate_legacy_config, which translates the QUARK 1 config format to QUARK 2 format. - :param config_part: part of a config - :type config_part: dict + :param config_part: Part of a config :param module_key: Module key: mapping, solver or device - :type module_key: str - :return: translated config_part - :rtype: list + :return: Translated config_part """ - - next_module_key = None - if module_key.lower() == "solver": - next_module_key = "device" + next_module_key = "device" if module_key.lower() == "solver" else None result = [] for item in config_part[module_key]: @@ -229,17 +201,14 @@ def translate_legacy_config_helper(config_part: dict, module_key: str) -> list: return result - def load_config(self, app_modules: list[dict]): + def load_config(self, app_modules: list[dict]) -> None: """ - Uses the config to generate all class instances needed to run the benchmark + Uses the config to generate all class instances needed to run the benchmark. :param app_modules: List of application modules as specified in the application modules configuration - :type app_modules: List of dict - :rtype: None """ self.application = _get_instance_with_sub_options(app_modules, self.config["application"]["name"]) - self.config["application"].update({"instance": self.application, "submodules": [ConfigManager.initialize_module_classes(self.application, c) for c in self.config["application"]["submodules"]]}) @@ -247,70 +216,58 @@ def load_config(self, app_modules: list[dict]): @staticmethod def initialize_module_classes(parent_module: Core, config: ConfigModule) -> ConfigModule: """ - Recursively initializes all instances of the required modules and their submodules for a given config + Recursively initializes all instances of the required modules and their submodules for a given config. :param parent_module: Class of the parent module - :type parent_module: Core :param config: Uninitialized config module - :type config: ConfigModule :return: Config with instances - :rtype: ConfigModule """ - module_instance = parent_module.get_submodule(config["name"]) config.update({"instance": module_instance, - "submodules": [ConfigManager.initialize_module_classes(module_instance, c) for c in - config["submodules"]]} - ) + "submodules": [ConfigManager.initialize_module_classes(module_instance, c) + for c in config["submodules"]]}) return config def get_config(self) -> BenchmarkConfig: """ - Returns the config + Returns the config. :return: Returns the config - :rtype: BenchmarkConfig """ return self.config def get_app(self) -> Application: """ - Returns instance of the application + Returns instance of the application. :return: Instance of the application - :rtype: Application """ return self.config["application"]["instance"] def get_reps(self) -> int: """ - Returns number of repetitions specified in config + Returns number of repetitions specified in config. :return: Number of repetitions - :rtype: int """ return self.config["repetitions"] def start_create_benchmark_backlog(self) -> list: """ - Helper function to kick off the creation of the benchmark backlog + Helper function to kick off the creation of the benchmark backlog. :return: List with all benchmark items - :rtype: list """ return ConfigManager.create_benchmark_backlog(self.config["application"]) @staticmethod def create_benchmark_backlog(module: ConfigModule) -> list: """ - Recursive function which splits up the loaded config into single benchmark runs + Recursive function which splits up the loaded config into single benchmark runs. :param module: ConfigModule - :type module: any :return: List with all benchmark items - :rtype: list """ - items = [] if len(module["config"].items()) > 0: @@ -328,36 +285,30 @@ def create_benchmark_backlog(module: ConfigModule) -> list: "name": module["name"], "instance": module["instance"], "config": single_config, - "submodule": { - submodule["name"]: item - } + "submodule": {submodule["name"]: item} }) else: items.append({ "name": module["name"], "instance": module["instance"], "config": single_config, - "submodule": { - } + "submodule": {} }) return items - def save(self, store_dir: str): + def save(self, store_dir: str) -> None: """ - Saves the config as a YAML file + Saves the config as a YAML file. :param store_dir: Directory in which the file should be stored - :type store_dir: str - :rtype: None """ with open(f"{store_dir}/config.yml", 'w') as filehandler: yaml.dump(self.config, filehandler) def print(self) -> None: """ - Prints the config - :rtype: None + Prints the config. """ print(yaml.dump(self.config)) @@ -365,22 +316,17 @@ def print(self) -> None: def _query_for_config(param_opts: dict, prefix: str = "") -> dict: """ For a given module config, queries users in an interactive mode, which of the options they would like to - include in the final benchmark config + include in the final benchmark config. :param param_opts: Dictionary containing the options for a parameter including a description - :type param_opts: dict :param prefix: Prefix string, which is attached when interacting with the user - :type prefix: str :return: Dictionary containing the decisions of the user on what to include in the benchmark. - :rtype: dict """ config = {} for key, config_answer in param_opts.items(): if config_answer.get("if"): - key_in_cond = config_answer.get("if")["key"] dependency = param_opts.get(key_in_cond) - consistent = False err_msg = None if dependency is None: @@ -400,26 +346,20 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: # When there is only 1 value to choose from skip the user input for now values = config_answer['values'] print(f"{prefix} {config_answer['description']}: {config_answer['values'][0]}") - elif config_answer.get('exclusive', False): answer = inquirer.prompt( - [inquirer.List(key, - message=f"{prefix} {config_answer['description']}", - choices=config_answer['values'] - )]) + [inquirer.List(key, message=f"{prefix} {config_answer['description']}", + choices=config_answer['values'])]) values = (answer[key],) else: - choices = [*config_answer['values'], "Custom Input"] if (config_answer.get("custom_input") and config_answer["custom_input"]) \ else config_answer['values'] - if config_answer.get("allow_ranges") and config_answer["allow_ranges"]: choices.append("Custom Range") - answer = checkbox(key=key, - message=f"{prefix} {config_answer['description']}", - # Add custom_input if it is specified in the parameters - choices=choices) + + # Add custom_input if it is specified in the parameters + answer = checkbox(key=key,message=f"{prefix} {config_answer['description']}", choices=choices) values = answer[key] if "Custom Input" in values: @@ -439,7 +379,6 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: "this input is done!)"), inquirer.Text('step', message=f"What are the steps of your range for {key}? (No validation of " "this input is done!)")]) - values.remove("Custom Range") values.extend(np.arange(float(range_answer["start"]), float(range_answer["stop"]), float(range_answer["step"]))) @@ -451,23 +390,19 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: # with each of the user selected values as argument. # Note that the stored config file will contain the processed values. values = [config_answer["postproc"](v) for v in values] + config[key] = values return config def create_tree_figure(self, store_dir: str) -> None: """ - Visualizes the benchmark as a graph (experimental feature) + Visualizes the benchmark as a graph (experimental feature). :param store_dir: Directory where the file should be stored - :type store_dir: str - :rtype: None """ - graph = nx.DiGraph() - ConfigManager._create_tree_figure_helper(graph, self.config["application"]) - nx.draw(graph, with_labels=True, pos=nx.spectral_layout(graph), node_shape="s") plt.savefig(f"{store_dir}/BenchmarkGraph.png", format="PNG") plt.clf() @@ -475,15 +410,11 @@ def create_tree_figure(self, store_dir: str) -> None: @staticmethod def _create_tree_figure_helper(graph: nx.Graph, config: ConfigModule) -> None: """ - Helper function for create_tree_figure that traverses the config recursively + Helper function for create_tree_figure that traverses the config recursively. - :param graph: networkx Graph - :type graph: networkx.Graph - :param config: benchmark config - :type config: dict - :rtype: None + :param graph: Networkx Graph + :param config: Benchmark config """ - if config: key = config["name"] if "submodules" in config and config["submodules"]: diff --git a/src/Installer.py b/src/Installer.py index f2365d18..4400678f 100644 --- a/src/Installer.py +++ b/src/Installer.py @@ -30,7 +30,7 @@ class Installer: """ Installer class that can be used by the user to install certain QUARK modules and also return the required python - packages for the demanded modules + packages for the demanded modules. """ def __init__(self): @@ -64,14 +64,10 @@ def __init__(self): def configure(self, env_name="default") -> None: """ - Configures a new QUARK environment or overwrites an existing one + Configures a new QUARK environment or overwrites an existing one. :param env_name: Name of the env to configure - :type env_name: str - :return: - :rtype: None """ - configured_envs = self.check_for_configs() if env_name in configured_envs: @@ -88,8 +84,7 @@ def configure(self, env_name="default") -> None: chosen_config_type = inquirer.prompt([ inquirer.List("config", message="Do you want to use the default configuration or a custom environment?", - choices=["Default", "Custom"], - )])["config"] + choices=["Default", "Custom"])])["config"] logging.info(f"You chose {chosen_config_type}") module_db = self.get_module_db() @@ -117,52 +112,44 @@ def configure(self, env_name="default") -> None: activate_answer = inquirer.prompt([ inquirer.List("activate", message="Do you want to activate the QUARK module environment?", - choices=["Yes", "No"], - )])["activate"] + choices=["Yes", "No"])])["activate"] if activate_answer == "Yes": self.set_active_env(env_name) def check_for_configs(self) -> list: """ - Checks if QUARK is already configured and if yes, which environments + Checks if QUARK is already configured and if yes, which environments. :return: Returns the configured QUARK envs in a list - :rtype: list """ return list(p.stem for p in Path(self.envs_dir).glob("*.json")) def set_active_env(self, name: str) -> None: """ - Sets active env to active_env.json + Sets active env to active_env.json. :param name: Name of the env - :type name: str - :return: - :rtype: None """ self._check_if_env_exists(name) with open(f"{self.settings_dir}/active_env.json", "w") as jsonFile: - data = {"name": name} - json.dump(data, jsonFile, indent=2) + json.dump({"name": name}, jsonFile, indent=2) logging.info(f"Set active QUARK module environment to {name}") def check_active_env(self) -> bool: """ - Checks if .settings/active_env.json exists + Checks if .settings/active_env.json exists. :return: True if active_env.json exists - :rtype: bool """ return Path(f"{self.settings_dir}/active_env.json").is_file() def get_active_env(self) -> str: """ - Returns the current active environment + Returns the current active environment. :return: Returns the name of the active env - :rtype: str """ if not self.check_active_env(): logging.warning("No active QUARK module environment found, using default") @@ -176,12 +163,10 @@ def get_active_env(self) -> str: def get_env(self, name: str) -> list[dict]: """ - Loads the env from file and returns it + Loads the env from file and returns it. :param name: Name of the env - :type name: dict :return: Returns the modules of the env - :rtype: list[dict] """ file = f"{self.envs_dir}/{name}.json" self._check_if_env_exists(name) @@ -200,12 +185,10 @@ def get_env(self, name: str) -> list[dict]: def _check_if_env_exists(self, name: str) -> str: """ - Checks if a given env exists, returns the location of the associated JSON file and raises an error otherwise + Checks if a given env exists, returns the location of the associated JSON file and raises an error otherwise. :param name: Name of the env - :type name: str :return: Returns location of the JSON file associated with the env if it exists - :rtype: str """ file = f"{self.envs_dir}/{name}.json" if not Path(file).is_file(): @@ -214,16 +197,11 @@ def _check_if_env_exists(self, name: str) -> str: def save_env(self, env: dict, name: str) -> None: """ - Saves a created env to a file with the name of choice + Saves a created env to a file with the name of choice. :param env: Env which should be saved - :type env: dict :param name: Name of the env - :type name: str - :return: - :rtype: None """ - with open(f"{self.envs_dir}/{name}.json", "w") as jsonFile: json.dump(env, jsonFile, indent=2) @@ -231,14 +209,11 @@ def save_env(self, env: dict, name: str) -> None: def start_query_user(self, module_db: dict) -> dict: """ - Queries the user which applications and submodules to include + Queries the user which applications and submodules to include. :param module_db: module_db file - :type module_db: dict :return: Returns the module_db with selected (sub)modules - :rtype: dict """ - answer_apps = checkbox("apps", "Which application would you like to include?", [m["name"] for m in module_db["modules"]])["apps"] @@ -254,13 +229,8 @@ def query_user(self, submodules: dict, name: str) -> None: Queries the user which submodules to include :param submodules: Submodules for the module - :type submodules: dict :param name: Name of the module - :type name: str - :return: - :rtype: None """ - if submodules["submodules"]: answer_submodules = \ checkbox("submodules", f"Which submodule would you like to include for {name}?", @@ -272,20 +242,16 @@ def query_user(self, submodules: dict, name: str) -> None: def get_module_db(self) -> dict: """ - Returns the module database that contains all module possibilities + Returns the module database that contains all module possibilities. :return: Module Database - :rtype: dict """ with open(f"{self.settings_dir}/module_db.json", "r") as filehandler: return json.load(filehandler) def create_module_db(self) -> None: """ - Creates module database by automatically going through the available submodules for each module - - :return: - :rtype: None + Creates module database by automatically going through the available submodules for each module. """ logging.info("Creating Module Database") @@ -323,16 +289,12 @@ def create_module_db(self) -> None: @staticmethod def _create_module_db_helper(module: Core, name: str) -> dict: """ - Recursive helper function for create_module_db + Recursive helper function for create_module_db. - :param module: module - :type module: Core + :param module: Modulen instance :param name: Name of the module - :type name: str - :return: module dict - :rtype: dict + :return: Module dict """ - return { "name": name, "class": module.__class__.__name__, @@ -350,12 +312,10 @@ def _create_module_db_helper(module: Core, name: str) -> dict: def get_module_db_build_number(self) -> int: """ - Returns the build number of the module_db + Returns the build number of the module_db. :return: Returns the build number of the module_db if it exists, otherwise 0 - :rtype: int """ - if Path(f"{self.settings_dir}/module_db.json").is_file(): module_db = self.get_module_db() return module_db["build_number"] @@ -364,14 +324,11 @@ def get_module_db_build_number(self) -> int: def collect_requirements(self, env: list[dict]) -> dict: """ - Collects requirements of the different modules in the given env file + Collects requirements of the different modules in the given env file. - :param env: env file - :type env: list[dict] + :param env: Environment configuration :return: Collected requirements - :rtype: dict """ - requirements: list[dict] = self.core_requirements for app in env: requirements.extend(Installer._collect_requirements_helper(app)) @@ -402,14 +359,11 @@ def collect_requirements(self, env: list[dict]) -> dict: @staticmethod def _collect_requirements_helper(module: dict) -> list[dict]: """ - Helper function for collect_requirements_helper that recursively checks modules for requirements + Helper function for collect_requirements_helper that recursively checks modules for requirements. - :param module: module dict - :type module: dict + :param module: Module dict :return: List of dicts with the requirements - :rtype: list[dict] """ - requirements = module["requirements"] for submodule in module["submodules"]: requirements.extend(Installer._collect_requirements_helper(submodule)) @@ -418,16 +372,11 @@ def _collect_requirements_helper(module: dict) -> list[dict]: def create_conda_file(self, requirements: dict, name: str, directory: str = None) -> None: """ - Creates conda yaml file based on the requirements + Creates conda yaml file based on the requirements. :param requirements: Collected requirements - :type requirements: dict :param name: Name of the conda env - :type name: str :param directory: Directory where the file should be saved. If None self.envs_dir will be taken - :type directory: str - :return: - :rtype: None """ if directory is None: directory = self.envs_dir @@ -449,16 +398,11 @@ def create_conda_file(self, requirements: dict, name: str, directory: str = None def create_req_file(self, requirements: dict, name: str, directory: str = None) -> None: """ - Creates pip txt file based on the requirements + Creates pip txt file based on the requirements. :param requirements: Collected requirements - :type requirements: dict :param name: Name of the env - :type name: str :param directory: Directory where the file should be saved. If None self.envs_dir will be taken - :type directory: str - :return: - :rtype: None """ if directory is None: directory = self.envs_dir @@ -472,12 +416,8 @@ def create_req_file(self, requirements: dict, name: str, directory: str = None) def list_envs(self) -> None: """ - List all existing envs - - :return: - :rtype: None + List all existing environments. """ - logging.info("Existing environments:") for env in self.check_for_configs(): logging.info(f" - {env}") @@ -485,12 +425,9 @@ def list_envs(self) -> None: @staticmethod def show(env: list[dict]) -> None: """ - Visualize the env + Visualize the env. - :param env: env - :type env: list[dict] - :return: - :rtype: None + :param env: Environment configuration """ space = " " branch = "| " @@ -500,16 +437,12 @@ def show(env: list[dict]) -> None: def tree(modules: list[dict], prefix: str = ""): """ A recursive function that generates a tree from the modules. - This function is based on https://stackoverflow.com/a/59109706, but modified to the needs here + This function is based on https://stackoverflow.com/a/59109706, but modified to the needs here. - :param modules: Modules - :type modules: list[dict] + :param modules: Modules list :param prefix: Prefix for the indentation - :type prefix: str - :return: - :rtype: + :return: Generator yielding formatted lines of the environment tree """ - # Modules in the middle/beginning get a |--, the final leaf >-- pointers = [connector] * (len(modules) - 1) + [leaf] for pointer, module in zip(pointers, modules): diff --git a/src/Metrics.py b/src/Metrics.py index 1d2e3b19..3f93c21e 100644 --- a/src/Metrics.py +++ b/src/Metrics.py @@ -17,17 +17,15 @@ class Metrics: """ - Metrics Module, used by every QUARK module + Metrics Module, used by every QUARK module. """ def __init__(self, module_name: str, module_src: str): """ - Constructor for Metrics class + Constructor for Metrics class. :param module_name: Name of the module this metrics object belongs to - :type module_name: str :param module_src: Source file of the module this metrics object belongs to - :type module_src: str """ self.module_name = module_name self.module_src = module_src @@ -44,83 +42,65 @@ def __init__(self, module_name: str, module_src: str): def validate(self) -> None: """ Validates whether the mandatory metrics got recorded, then sets total time. - - :return: - :rtype: None """ - assert self.preprocessing_time is not None, "preprocessing time must not be None!" - assert self.postprocessing_time is not None, "postprocessing time must not be None!" + assert self.preprocessing_time is not None, ( + "preprocessing time must not be None!" + ) + assert self.postprocessing_time is not None, ( + "postprocessing time must not be None!" + ) self.total_time = self.preprocessing_time + self.postprocessing_time @final def set_preprocessing_time(self, value: float) -> None: """ - Sets the preprocessing time + Sets the preprocessing time. :param value: Time - :type value: float - :return: - :rtype: None """ self.preprocessing_time = value @final def set_module_config(self, config: dict) -> None: """ - Sets the config of the module this metrics object belongs to + Sets the config of the module this metrics object belongs to. :param config: Config of the QUARK module - :type config: dict - :return: - :rtype: None """ self.module_config = config @final def set_postprocessing_time(self, value: float) -> None: """ - Sets the postprocessing time + Sets the postprocessing time. :param value: Time - :type value: float - :return: - :rtype: None """ self.postprocessing_time = value @final def add_metric(self, name: str, value: any) -> None: """ - Adds a single metric + Adds a single metric. :param name: Name of the metric - :type name: str :param value: Value of the metric - :type value: any - :return: - :rtype: None """ self.additional_metrics.update({name: value}) @final def add_metric_batch(self, key_values: dict) -> None: """ - Adds a dictionary containing metrics to the existing metrics + Adds a dictionary containing metrics to the existing metrics. - :param key_values: dict containing metrics - :type key_values: dict - :return: - :rtype: None + :param key_values: Dict containing metrics """ self.additional_metrics.update(key_values) @final def reset(self) -> None: """ - Resets all recorded metrics - - :return: - :rtype: None + Resets all recorded metrics. """ self.preprocessing_time = None self.postprocessing_time = None @@ -129,10 +109,9 @@ def reset(self) -> None: @final def get(self) -> dict: """ - Returns all recorded metrics + Returns all recorded metrics. :return: Metrics as a dict - :rtype: dict """ return { "module_name": self.module_name, diff --git a/src/Plotter.py b/src/Plotter.py index 61ad58c6..4914a424 100644 --- a/src/Plotter.py +++ b/src/Plotter.py @@ -27,7 +27,7 @@ class Plotter: """ - Plotter class which generates some general plots + Plotter class which generates some general plots. """ @staticmethod @@ -35,22 +35,18 @@ def visualize_results(results: List[Dict], store_dir: str) -> None: """ Function to plot the execution times of the benchmark. - :param results: dict containing the results - :type results: list[dict] - :param store_dir: directory where the plots are stored - :type store_dir: str - :return: - :rtype: None + :param results: Dict containing the results + :param store_dir: Directory where the plots are stored """ - if results is None or len(results) == 0: logging.info("Nothing to plot since results are empty.") return processed_results_with_application_score = [] processed_results_rest = [] - required_application_score_keys = ["application_score_value", "application_score_unit", - "application_score_type"] + required_application_score_keys = [ + "application_score_value", "application_score_unit", "application_score_type" + ] application_name = None application_axis = None static_keys, changing_keys = Plotter._get_config_keys(results) @@ -66,17 +62,19 @@ def visualize_results(results: List[Dict], store_dir: str) -> None: application_config = ', '.join( [f"{key}: {value}" for (key, value) in sorted(result["module"]["module_config"].items(), key=lambda key_value_pair: - key_value_pair[0]) if key not in static_keys]) + key_value_pair[0]) if key not in static_keys] + ) if len(static_keys) > 0: # Include the static items in the axis name application_axis += "(" + ', '.join( - [f"{key}: {result['module']['module_config'][key]}" for key in static_keys]) + ")" + [f"{key}: {result['module']['module_config'][key]}" for key in static_keys] + ) + ")" - processed_item = Plotter._extract_columns({"benchmark_backlog_item_number": - result["benchmark_backlog_item_number"], - "total_time": result["total_time"], - "application_config": application_config}, - result["module"]) + processed_item = Plotter._extract_columns({ + "benchmark_backlog_item_number": result["benchmark_backlog_item_number"], + "total_time": result["total_time"], + "application_config": application_config + }, result["module"]) if all(k in result["module"] for k in required_application_score_keys): # Check if all required keys are present to create application score plots @@ -88,12 +86,15 @@ def visualize_results(results: List[Dict], store_dir: str) -> None: if len(processed_results_with_application_score) > 0: logging.info("Found results with an application score, generating according plots.") - Plotter.plot_application_score(application_name, application_axis, - processed_results_with_application_score, store_dir) + Plotter.plot_application_score( + application_name, application_axis, processed_results_with_application_score, store_dir + ) - Plotter.plot_times(application_name, application_axis, [*processed_results_with_application_score, - *processed_results_rest], store_dir, - required_application_score_keys) + Plotter.plot_times( + application_name, application_axis, + [*processed_results_with_application_score, *processed_results_rest], + store_dir, required_application_score_keys + ) logging.info("Finished creating plots.") @@ -103,18 +104,11 @@ def plot_times(application_name: str, application_axis: str, results: list[dict] """ Function to plot execution times of the different modules in a benchmark. - :param application_name: name of the application - :type application_name: str - :param application_axis: name of the application axis - :type application_axis: str - :param results: dict containing the results - :type results: list[dict] - :param store_dir: directory where the plots are stored - :type store_dir: str - :param required_application_score_keys: list of keys which have to be present to calculate an application score - :type required_application_score_keys: list - :return: - :rtype: None + :param application_name: Name of the application + :param application_axis: Name of the application axis + :param results: Dict containing the results + :param store_dir: Directory where the plots are stored + :param required_application_score_keys: List of keys which have to be present to calculate an application score """ df = pd.DataFrame.from_dict(results) @@ -140,7 +134,7 @@ def plot_times(application_name: str, application_axis: str, results: list[dict] # Put the legend out of the figure plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0., title="Modules used") plt.title(application_name) - matplotlib.pyplot.sca(ax) + plt.sca(ax) # If column values are very long and of type string rotate the ticks if (pd.api.types.is_string_dtype(df.application_config.dtype) or pd.api.types.is_object_dtype( df.application_config.dtype)) and df.application_config.str.len().max() > 10: @@ -155,18 +149,11 @@ def plot_application_score(application_name: str, application_axis: str, results """ Funtion to create plots showing the application score. - :param application_name: name of the application - :type application_name: str - :param application_axis: name of the application axis - :type application_axis: str - :param results: dict containing the results - :type results: list[dict] - :param store_dir: directory where the plots are stored - :type store_dir: str - :return: - :rtype: None + :param application_name: Name of the application + :param application_axis: Name of the application axis + :param results: Dict containing the results + :param store_dir: Directory where the plots are stored """ - df = pd.DataFrame.from_dict(results) application_score_units = df["application_score_unit"].unique() count_invalid_rows = pd.isna(df['application_score_value']).sum() @@ -178,18 +165,24 @@ def plot_application_score(application_name: str, application_axis: str, results logging.info(f"{count_invalid_rows} out of {len(df)} benchmark runs have an invalid application score.") if len(application_score_units) != 1: - logging.warning(f"Found more or less than exactly 1 application_score_unit in {application_score_units}." - f" This might lead to incorrect plots!") + logging.warning( + f"Found more or less than exactly 1 application_score_unit in {application_score_units}." + f" This might lead to incorrect plots!" + ) ax = sns.barplot(x="application_config", y="application_score_value", data=df, hue="config_combo") ax.set(xlabel=application_axis, ylabel=application_score_units[0]) # Put the legend out of the figure plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0., title="Modules used") - ax.text(1.03, 0.5, f"{len(df) - count_invalid_rows}/{len(df)} runs have a valid \napplication score", - transform=ax.transAxes, fontsize=12, verticalalignment='top', bbox={"boxstyle": "round", "alpha": 0.15}) + ax.text( + 1.03, 0.5, + f"{len(df) - count_invalid_rows}/{len(df)} runs have a valid \napplication score", + transform=ax.transAxes, fontsize=12, verticalalignment='top', + bbox={"boxstyle": "round", "alpha": 0.15} + ) plt.title(application_name) - matplotlib.pyplot.sca(ax) + plt.sca(ax) # If column values are very long and of type string, rotate the ticks if (pd.api.types.is_string_dtype(df.application_config.dtype) or pd.api.types.is_object_dtype( df.application_config.dtype)) and df.application_config.str.len().max() > 10: @@ -197,18 +190,15 @@ def plot_application_score(application_name: str, application_axis: str, results plt.savefig(f"{store_dir}/application_score.pdf", dpi=300, bbox_inches='tight') logging.info(f"Saved {f'{store_dir}/application_score.pdf'}.") - plt.clf() @staticmethod - def _get_config_keys(results: list[dict]) -> (list, list): + def _get_config_keys(results: list[dict]) -> tuple[list, list]: """ Function that extracts config keys. - :param results: results of a benchmark run - :type results: list[dict] - :return: tuple with list of static keys and list of changing keys - :rtype: (list, list) + :param results: Results of a benchmark run + :return: Tuple with list of static keys and list of changing keys """ static_keys = [] changing_keys = [] @@ -234,14 +224,10 @@ def _extract_columns(config: dict, rest_result: dict) -> dict: Funtion to extract and summarize certain data fields like the time spent in every module from the nested module chain. - :param config: dictionary containing multiple data fields like the config a module - :type config: dict - :param rest_result: rest of the module chain - :type rest_result: dict - :return: extracted data - :rtype: dict + :param config: Dictionary containing multiple data fields like the config a module + :param rest_result: Rest of the module chain + :return: Extracted data """ - if rest_result: module_name = rest_result["module_name"] for key, value in sorted(rest_result["module_config"].items(), @@ -253,8 +239,8 @@ def _extract_columns(config: dict, rest_result: dict) -> dict: { **config, "config_combo": config_combo, - module_name: rest_result["total_time"] if module_name not in config else config[module_name] + - rest_result["total_time"] + module_name: rest_result["total_time"] + if module_name not in config else config[module_name] + rest_result["total_time"] }, rest_result["submodule"] ) diff --git a/src/demo/instruction_demo.py b/src/demo/instruction_demo.py index 6f53a968..24e102aa 100644 --- a/src/demo/instruction_demo.py +++ b/src/demo/instruction_demo.py @@ -17,6 +17,11 @@ def __init__(self, application_name: str = None): def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple: """ Preprocess input data with given configuration and instructions. + + :param input_data: Data to be processed. + :param config: Configuration for processing the data + :param kwargs: Additional keyword arguments + :return: Instruction, processed data, and processing time. """ logging.info("%s", kwargs.keys()) logging.info("previous_job_info: %s", kwargs.get("previous_job_info")) @@ -44,7 +49,7 @@ def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple: def get_parameter_options(self) -> dict: """ - Returns parameter options for the preprocess methios. + Returns parameter options for the preprocess method. """ return { "instruction": { @@ -60,11 +65,20 @@ def get_parameter_options(self) -> dict: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule for the given option. + + :param option: The submodule option + :return: Default submodule """ return Dummy() def save(self, path: str, iter_count: int) -> None: + """ + Saves the current state to the specified path. + + :param path: Path where the state should be saved + :param iter_count: Iteration count. + """ pass @@ -74,7 +88,17 @@ class Dummy(Core): """ def get_parameter_options(self) -> dict: + """ + Returns parameter options for the Dummy module. + + :return: Dictionary containing parameter options + """ return {} def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + + :param option: The submodule option + """ pass diff --git a/src/main.py b/src/main.py index 74098203..47b250d5 100644 --- a/src/main.py +++ b/src/main.py @@ -26,7 +26,7 @@ comm = get_comm() -# add the paths +# Add the paths install_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(install_dir) @@ -39,9 +39,7 @@ def _filter_comments(file: Iterable) -> str: Returns the content of the filehandle, ignoring all lines starting with '#'. :param file: file to be read - :type file: Iterable :return: file content without comment lines - :rtype: str """ lines = [] for line in file: @@ -53,10 +51,7 @@ def _filter_comments(file: Iterable) -> str: def setup_logging() -> None: """ - Sets up the logging - - :return: - :rtype: None + Sets up the logging. """ logging.root.handlers = [] logging.basicConfig( @@ -78,18 +73,15 @@ def setup_logging() -> None: logging.info(" ============================================================ ") logging.info(" A Framework for Quantum Computing Application Benchmarking ") logging.info(" ") - logging.info(" Licensed under the Apache License, Version 2.0 ") + logging.info(" Licensed under the Apache License, Version 2.1 ") logging.info(" ============================================================ ") -def start_benchmark_run(config_file: str = None, store_dir: str = None, fail_fast: bool = False) -> None: +def start_benchmark_run(config_file: str = None, store_dir: str = None, + fail_fast: bool = False) -> None: """ - Starts a benchmark run from the code - - :return: - :rtype: None + Starts a benchmark run from the code. """ - setup_logging() # Helper for Hybrid Jobs @@ -115,7 +107,9 @@ def start_benchmark_run(config_file: str = None, store_dir: str = None, fail_fas # Can be overridden by using the -m|--modules option installer = Installer() app_modules = installer.get_env(installer.get_active_env()) - benchmark_manager.orchestrate_benchmark(config_manager, store_dir=store_dir, app_modules=app_modules) + benchmark_manager.orchestrate_benchmark( + config_manager, store_dir=store_dir, app_modules=app_modules + ) def create_benchmark_parser(parser: argparse.ArgumentParser): @@ -148,12 +142,9 @@ def create_env_parser(parser: argparse.ArgumentParser): def handle_benchmark_run(args: argparse.Namespace) -> None: """ - Handles the different options of a benchmark run + Handles the different options of a benchmark run. :param args: Namespace with the arguments given by the user - :type args: argparse.Namespace - :return: - :rtype: None """ from BenchmarkManager import BenchmarkManager # pylint: disable=C0415 from Plotter import Plotter # pylint: disable=C0415 @@ -172,7 +163,9 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: # + Replaces relative paths by taking them relative to the location of the modules configuration file base_dir = os.path.dirname(args.modules) with open(args.modules) as filehandler: - app_modules = _expand_paths(json.loads(_filter_comments(filehandler)), base_dir) + app_modules = _expand_paths(json.loads( + _filter_comments(filehandler)), base_dir + ) else: # Gets current env here installer = Installer() @@ -199,10 +192,13 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: logging.info("Selected config is:") config_manager.print() else: - interrupted_results_path = None if args.resume_dir is None else os.path.join(args.resume_dir, - "results.json") - benchmark_manager.orchestrate_benchmark(config_manager, app_modules, - interrupted_results_path=interrupted_results_path) + interrupted_results_path = None if args.resume_dir is None else os.path.join( + args.resume_dir, "results.json" + ) + benchmark_manager.orchestrate_benchmark( + config_manager, app_modules, + interrupted_results_path=interrupted_results_path + ) comm.Barrier() if comm.Get_rank() == 0: results = benchmark_manager.load_results() @@ -211,12 +207,9 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: def handler_env_run(args: argparse.Namespace) -> None: """ - Orchestrates the requests to the QUARK module environment + Orchestrates the requests to the QUARK module environment. :param args: Namespace with the arguments by the user - :type args: argparse.Namespace - :return: - :rtype: None """ installer = Installer() if args.createmoduledb: diff --git a/src/modules/Core.py b/src/modules/Core.py index f09b54a1..f4bc5f00 100644 --- a/src/modules/Core.py +++ b/src/modules/Core.py @@ -15,25 +15,24 @@ from __future__ import annotations # Needed if you want to type hint a method with the type of the enclosing class import os -from abc import ABC, abstractmethod +import sys import logging +from abc import ABC, abstractmethod from typing import final -import sys from utils import _get_instance_with_sub_options - from Metrics import Metrics class Core(ABC): """ - Core Module for QUARK used by all other Modules that are part of a benchmark process + Core Module for QUARK, used by all other Modules that are part of a benchmark process. """ def __init__(self, name: str = None): """ - Constructor method + Constructor method. + :param name: name used to identify this QUARK module. If not specified class name will be used as default. - :type name: str """ self.submodule_options = [] self.sub_options = [] @@ -47,7 +46,7 @@ def __init__(self, name: str = None): @abstractmethod def get_parameter_options(self) -> dict: """ - Returns the parameters for a given module + Returns the parameters for a given module. Should always be in this format: @@ -65,19 +64,16 @@ def get_parameter_options(self) -> dict: } :return: Available settings for this application - :rtype: dict """ @final - def get_submodule(self, option: str) -> any: + def get_submodule(self, option: str) -> Core: """ Submodule is instantiated according to the information given in self.sub_options. If self.sub_options is None, get_default_submodule is called as a fallback. :param option: String with the options - :type option: str :return: Instance of a module - :rtype: any """ if self.sub_options is None or not self.sub_options: return self.get_default_submodule(option) @@ -87,60 +83,49 @@ def get_submodule(self, option: str) -> any: @abstractmethod def get_default_submodule(self, option: str) -> Core: """ - Given an option string by the user, this returns a submodule + Given an option string by the user, this returns a submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ raise NotImplementedError("Please don't use the base version of this method. " "Implement your own override instead.") - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - Essential method for the benchmarking process. Is always executed before traversing down to the next module, + Essential method for the benchmarking process. This is always executed before traversing down to the next module, passing the data returned by this function. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ return input_data, 0.0 - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Essential Method for the benchmarking process. Is always executed after the submodule is finished. The data by this method is passed up to the parent module. :param input_data: Input data comes from the submodule if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the postprocessing and the time it took to postprocess - :rtype: (any, float) """ return input_data, 0.0 @final def get_available_submodule_options(self) -> list: """ - Gets list of available options + Gets the list of available options. :return: List of module options - :rtype: list """ if self.sub_options is None or not self.sub_options: return self.submodule_options else: - return [o["name"] for o in self.sub_options] + return [option["name"] for option in self.sub_options] @staticmethod def get_requirements() -> list: @@ -148,6 +133,5 @@ def get_requirements() -> list: Returns the required pip packages for this module. Optionally, version requirements can be added. :return: List of dictionaries - :rtype: list """ return [] diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index 3521cc3e..51319b37 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -28,7 +28,7 @@ import os import logging -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import pandas as pd import numpy as np @@ -55,7 +55,7 @@ class ACL(Optimization): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("ACL") self.submodule_options = ["MIPsolverACL", "QUBO"] @@ -109,7 +109,7 @@ class Config(TypedDict): model_select: str @staticmethod - def intersectset(p1: List, p2: List) -> List: + def intersectset(p1: list, p2: list) -> list: """ Computes the intersection of two lists. @@ -120,7 +120,7 @@ def intersectset(p1: List, p2: List) -> List: return np.intersect1d(p1, p2).tolist() @staticmethod - def diffset(p1: List, p2: List) -> List: + def diffset(p1: list, p2: list) -> list: """ Computes the difference between two lists. @@ -130,7 +130,7 @@ def diffset(p1: List, p2: List) -> List: """ return np.setdiff1d(p1, p2).tolist() - def generate_problem(self, config: Config) -> Dict: # pylint: disable=R0915 + def generate_problem(self, config: Config) -> dict: # pylint: disable=R0915 """ This function includes three models: Full, small and tiny. Full refers to the original model with all of its constraints. Small refers to the simplified model suitable for solving it with QC methods. @@ -616,7 +616,7 @@ def _get_vehicle_params(self, df, vehicles): """ Extract vehicle parameters for the problem formulation - :param df : Dataframe containing vehicle data + :param df: Dataframe containing vehicle data :param vehicles: List of vehicle types to consider :return: Lists containing class, length, height, and weight of vehicles """ @@ -634,9 +634,9 @@ def _get_vehicle_params(self, df, vehicles): return class_list, length_list, height_list, weight_list - def validate(self, solution: Any) -> Tuple[bool, float]: + def validate(self, solution: any) -> tuple[bool, float]: """ - Checks if the solution is a valid solution + Checks if the solution is a valid solution. : :param solution: Proposed solution :return: Tuple containing a boolean indicating if the solution is valid @@ -655,7 +655,7 @@ def get_solution_quality_unit(self) -> str: """ return "Number of loaded vehicles" - def evaluate(self, solution: Any) -> Tuple[float, float]: + def evaluate(self, solution: any) -> tuple[float, float]: """ Checks how good the solution is. @@ -682,5 +682,5 @@ def save(self, path: str, iter_count: int) -> None: """ # Convert our problem instance from Dict to an LP problem and then to json _, problem_instance = pulp.LpProblem.from_dict(self.application) - # Save problem instance to json + # Save problem instance to JSON problem_instance.to_json(f"{path}/ACL_instance.json") diff --git a/src/modules/applications/optimization/ACL/mappings/ISING.py b/src/modules/applications/optimization/ACL/mappings/ISING.py index 74ca774b..f9e072e2 100644 --- a/src/modules/applications/optimization/ACL/mappings/ISING.py +++ b/src/modules/applications/optimization/ACL/mappings/ISING.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import TypedDict, Any, Tuple +from typing import TypedDict import numpy as np from more_itertools import locate @@ -43,7 +43,7 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module """ @@ -57,7 +57,7 @@ def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. - :return: empty dict + :return: Empty dict """ return {} @@ -72,7 +72,7 @@ def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: Maps the problem dict to a quadratic program. :param problem: Problem formulation in dict form - :return: quadratic program in qiskit-optimization format + :return: Quadratic program in qiskit-optimization format """ # Details at: # https://coin-or.github.io/pulp/guides/how_to_export_models.html @@ -120,8 +120,8 @@ def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ Use Ising mapping of qiskit-optimize. - :param config: config with the parameters specified in Config class - :return: dict with the Ising, time it took to map it + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ start = start_time_measurement() @@ -153,12 +153,12 @@ def map(self, problem: dict, config: Config) -> tuple[dict, float]: return {"J": j_matrix, "t": t_matrix}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> Tuple(dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. :param solution: bit_string containing the solution - :return: solution mapped accordingly, time it took to map it + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() @@ -182,7 +182,7 @@ def reverse_map(self, solution: dict) -> Tuple(dict, float): return result, end_time_measurement(start) @staticmethod - def _convert_ising_to_qubo(solution: Any) -> Any: + def _convert_ising_to_qubo(solution: any) -> any: solution = np.array(solution) with np.nditer(solution, op_flags=['readwrite']) as it: for x in it: @@ -191,6 +191,12 @@ def _convert_ising_to_qubo(solution: Any) -> Any: return solution def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + + :param option: The submodule option + :return: Default submodule + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index c6efe661..b293f8c6 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Any, List, Tuple +from typing import TypedDict import re import logging @@ -56,7 +56,7 @@ def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. - :return: empty dictionary + :return: Empty dictionary """ return {} @@ -71,7 +71,7 @@ def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: Maps the problem dict to a quadratic program. :param problem: Problem formulation in dict form - :return: quadratic program in qiskit-optimization format + :return: Quadratic program in qiskit-optimization format """ # Details at: # https://coin-or.github.io/pulp/guides/how_to_export_models.html @@ -117,13 +117,13 @@ def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: return qp - def convert_string_to_arguments(self, input_string: str) -> List[Any]: + def convert_string_to_arguments(self, input_string: str) -> list[any]: """ Converts QUBO in string format to a list of separated arguments, used to construct the QUBO matrix. :param input_string: QUBO in raw string format - :return: list of arguments + :return: List of arguments """ terms = re.findall(r'[+\-]?[^+\-]+', input_string) # Convert the penalty string to a list of lists of the individual arguments in the penalty term @@ -155,8 +155,8 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra """ Creates QUBO matrix Q to solve linear problem of the form x^T * Q + x. - :param penalty: list of lists containing all non-zero elements of the QUBO matrix as strings - :param variables: listing of all variables used in the problem + :param penalty: List of lists containing all non-zero elements of the QUBO matrix as strings + :param variables: Listing of all variables used in the problem :return: QUBO in numpy array format """ # Create empty qubo matrix @@ -168,25 +168,21 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra for row, variable2 in enumerate(variables): # Save the parameters (values in the qubo) parameter = 0 - for argument in penalty: if isinstance(argument, list): # squared variables in diagonals (x^2 == x) - if len(argument) == 2: - if any(isinstance(elem, str) and variable in elem for elem in argument) and col == row: + if len(argument) == 2 and any(isinstance(elem, str) and variable in elem for elem in argument) and col == row: parameter += argument[0] # Multiplication of different variables not on diagonal - if len(argument) == 3: - if variable in argument and variable2 in argument and variable > variable2: + if len(argument) == 3 and variable in argument and variable2 in argument and variable > variable2: parameter += argument[0] # this value is already taking into account the factor 2 from quadratic term # For the variables on the diagonal, if the parameter is zero # we still have to check the sign in # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. - elif isinstance(argument, str): - if variable in argument and variable2 in argument and variable == variable2: - if "-" in argument: - parameter += -1 + elif isinstance(argument, str) and variable in argument and variable2 in argument and variable == variable2: + if "-" in argument: + parameter += -1 qubo[col, row] = parameter @@ -195,12 +191,12 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra return qubo - def map(self, problem: dict, config: Config) -> Tuple[dict, float]: + def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ Converts linear program created with pulp to quadratic program to Ising with qiskit to QUBO matrix. - :param config: config with the parameters specified in Config class - :return: dict with the QUBO, time it took to map it + :param config: Config with the parameters specified in Config class + :return: Dict with the QUBO, time it took to map it """ start = start_time_measurement() @@ -228,12 +224,12 @@ def map(self, problem: dict, config: Config) -> Tuple[dict, float]: return {"Q": qubo_matrix}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> Tuple[dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. :param solution: bit_string containing the solution - :return: solution mapped accordingly, time it took to map it + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() @@ -241,13 +237,12 @@ def reverse_map(self, solution: dict) -> Tuple[dict, float]: objective_value = 0 variables = {} for bit in solution: - if solution[bit] > 0: + if solution[bit] > 0 and "x" in self.global_variables[bit]: # We only care about assignments of vehicles to platforms: # We map the solution to the original variables - if "x" in self.global_variables[bit]: - variables[self.global_variables[bit]] = solution[bit] - result["status"] = 'Optimal' # TODO: I do not think every solution with at least one car is optimal - objective_value += solution[bit] + variables[self.global_variables[bit]] = solution[bit] + result["status"] = 'Optimal' # TODO: I do not think every solution with at least one car is optimal + objective_value += solution[bit] result["variables"] = variables result["obj_value"] = objective_value @@ -255,6 +250,12 @@ def reverse_map(self, solution: dict) -> Tuple[dict, float]: return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + + :param option: The submodule option + :return: Default submodule + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/MIS/MIS.py b/src/modules/applications/optimization/MIS/MIS.py index 4b5c8f00..669341f3 100644 --- a/src/modules/applications/optimization/MIS/MIS.py +++ b/src/modules/applications/optimization/MIS/MIS.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Tuple, Dict import pickle import logging +from typing import TypedDict import networkx as nx @@ -30,12 +30,23 @@ class MIS(Optimization): """ The maximum independent set (MIS) problem is a combinatorial optimization problem that seeks to find the largest - subset of vertices in a graph such that no two vertices are adjacent. + subset of vertices in a graph such that no two vertices are adjacent. MIS has numerous application in computer + science, network design, resource allocation, and even in physics, where finding optimal confiigurations can + solve fundamental problems related to stability and energy minimization. + + In a graph, the maximum independent set represents a set of nodes such that no two nodes share an edge. This + property makes it a key element in various optimization scenarios. Due to the problem's combinatorial nature, + it becomes computationally challenging, especially for large graphs, often requiring heuristic or approximate + solutions. + + In the context of QUARK, we employ quantum-inspired approaches and state-of-the-art classical algorithms to + tackle the problem. The graph is generated based on user-defined parameters such as size, spacing, and + filling fraction, which affect the complexity and properties of the generated instance. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("MIS") self.submodule_options = ["NeutralAtom"] @@ -45,7 +56,7 @@ def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [] @@ -71,28 +82,28 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Mapping Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. - :return: - .. code-block:: python + :return: Configuration dictionary for this application + .. code-block:: python - return { - "size": { - "values": list(range(1, 18)), - "description": "How large should your graph be?" - }, - "spacing": { - "values": [x/10 for x in range(1, 11)], - "description": "How much space do you want between your nodes," - " relative to Rydberg distance?" - }, - "filling_fraction": { - "values": [x/10 for x in range(1, 11)], - "description": "What should the filling fraction be?" - }, - } + return { + "size": { + "values": list(range(1, 18)), + "description": "How large should your graph be?" + }, + "spacing": { + "values": [x/10 for x in range(1, 11)], + "description": "How much space do you want between your nodes," + " relative to Rydberg distance?" + }, + "filling_fraction": { + "values": [x/10 for x in range(1, 11)], + "description": "What should the filling fraction be?" + }, + } """ return { @@ -121,13 +132,12 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Configuration attributes for generating an MIS problem + Configuration attributes for generating an MIS problem. Attributes: size (int): The number of nodes in the graph. spacing (float): The spacing between nodes in the graph. filling_fraction (float): The fraction of available places in the lattice filled with nodes - """ size: int spacing: float @@ -138,15 +148,13 @@ def generate_problem(self, config: Config) -> nx.Graph: Generates a graph to solve the MIS problem for. :param config: Config specifying the size and connectivity for the problem - :return: networkx graph representing the problem + :return: Networkx graph representing the problem """ if config is None: config = {"size": 3, "spacing": 1, "filling_fraction": 0.5} # Ensure config has the necessary information - assert all( - key in config for key in ['size', 'spacing', 'filling_fraction'] - ) + assert all(key in config for key in ['size', 'spacing', 'filling_fraction']) size = config.get('size') spacing = config.get('spacing') * R_rydberg @@ -166,9 +174,9 @@ def generate_problem(self, config: Config) -> nx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: List) -> Tuple[List, float]: + def process_solution(self, solution: list) -> tuple[list, float]: """ - Returns list of visited nodes and the time it took to process the solution + Returns list of visited nodes and the time it took to process the solution. :param solution: Unprocessed solution :return: Processed solution and the time it took to process it @@ -176,9 +184,9 @@ def process_solution(self, solution: List) -> Tuple[List, float]: start_time = start_time_measurement() return solution, end_time_measurement(start_time) - def validate(self, solution: List) -> Tuple[bool, float]: + def validate(self, solution: list) -> tuple[bool, float]: """ - Checks if the solution is an independent set + Checks if the solution is an independent set. :param solution: List containing the nodes of the solution :return: Boolean whether the solution is valid and time it took to validate @@ -218,9 +226,9 @@ def validate(self, solution: List) -> Tuple[bool, float]: return is_valid, end_time_measurement(start) - def evaluate(self, solution: List) -> Tuple[int, float]: + def evaluate(self, solution: list) -> tuple[int, float]: """ - Calculates the size of the solution + Calculates the size of the solution. :param solution: List containing the nodes of the solution :return: Set size, time it took to calculate the set size diff --git a/src/modules/applications/optimization/MIS/data/graph_layouts.py b/src/modules/applications/optimization/MIS/data/graph_layouts.py index 86183756..6708c20b 100644 --- a/src/modules/applications/optimization/MIS/data/graph_layouts.py +++ b/src/modules/applications/optimization/MIS/data/graph_layouts.py @@ -14,7 +14,6 @@ import math import random -from typing import Dict, List, Tuple import networkx as nx import pulser @@ -37,7 +36,7 @@ def generate_hexagonal_graph( lattice to be filled with nodes. (default: 1.0) Returns: - nx.Graph: networkx Graph representing the hexagonal graph layout. + nx.Graph: Networkx Graph representing the hexagonal graph layout """ if not 0.0 < filling_fraction <= 1.0: raise ValueError("The filling fraction must be in the domain of (0.0, 1.0].") @@ -61,7 +60,6 @@ def generate_hexagonal_graph( traps.pop(atom_to_remove) # Rename the atoms - i = 0 node_positions = {i: traps[trap] for i, trap in enumerate(traps.keys())} # pylint: disable=C0206 # Create the graph @@ -77,7 +75,8 @@ def generate_hexagonal_graph( return hexagonal_graph -def _generate_edges(node_positions: Dict[int, List[float]], radius: float = R_rydberg) -> list[tuple[int, int]]: + +def _generate_edges(node_positions: dict[int, list[float]], radius: float = R_rydberg) -> list[tuple[int, int]]: """ Generate edges between vertices within a given distance 'radius', which defaults to R_rydberg. @@ -101,7 +100,8 @@ def _generate_edges(node_positions: Dict[int, List[float]], radius: float = R_ry edges.append((vertex_key, neighbor_key)) return edges -def _vertex_distance(v0: Tuple[float, ...], v1: Tuple[float, ...]) -> float: + +def _vertex_distance(v0: tuple[float, ...], v1: tuple[float, ...]) -> float: """ Calculates distance between two n-dimensional vertices. For 2 dimensions: distance = sqrt((x0 - x1)**2 + (y0 - y1)**2) @@ -113,5 +113,8 @@ def _vertex_distance(v0: Tuple[float, ...], v1: Tuple[float, ...]) -> float: Returns: float: Distance between the vertices. """ - squared_difference = sum((coordinate0 - coordinate1) ** 2 for coordinate0, coordinate1 in zip(v0, v1)) + squared_difference = sum( + (coordinate0 - coordinate1) ** 2 for coordinate0, coordinate1 in zip(v0, v1) + ) + return math.sqrt(squared_difference) diff --git a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py index b2009a8d..27c69fc5 100644 --- a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py +++ b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Dict, Tuple +from typing import TypedDict import networkx as nx import pulser @@ -28,7 +28,7 @@ class NeutralAtom(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["NeutralAtomMIS"] @@ -38,7 +38,7 @@ def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of requirements of this module + :return: List of requirements of this module """ return [{"name": "pulser", "version": "0.19.0"}] @@ -52,16 +52,16 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Configuration options for Neutral Atom MIS mapping + Configuration options for Neutral Atom MIS mapping. """ pass - def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to a neutral atom MIS problem. :param problem: Networkx graph representing the MIS problem - :param config: config with the parameters specified in Config class + :param config: Config with the parameters specified in Config class :return: Tuple containing a dictionary with the neutral MIS and time it took to map it """ start = start_time_measurement() @@ -73,6 +73,7 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: 'graph': problem, 'register': register } + return neutral_atom_problem, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: diff --git a/src/modules/applications/optimization/Optimization.py b/src/modules/applications/optimization/Optimization.py index f38a203a..cffbfa39 100644 --- a/src/modules/applications/optimization/Optimization.py +++ b/src/modules/applications/optimization/Optimization.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. from abc import ABC, abstractmethod -from typing import List, Dict, Tuple, Any import logging from modules.applications.Application import Application @@ -25,55 +24,55 @@ class Optimization(Application, ABC): """ @abstractmethod - def validate(self, solution: Any) -> Tuple[bool, float]: + def validate(self, solution: any) -> tuple[bool, float]: """ - Checks if the solution is a valid solution + Checks if the solution is a valid solution. :param solution: Proposed solution - :return: bool value if solution is valid and the time it took to validate the solution + :return: Bool value if solution is valid and the time it took to validate the solution """ pass @abstractmethod def get_solution_quality_unit(self) -> str: """ - Returns the unit of the evaluation + Returns the unit of the evaluation. :return: String with the unit """ pass @abstractmethod - def evaluate(self, solution: Any) -> Tuple[float, float]: + def evaluate(self, solution: any) -> tuple[float, float]: """ - Checks how good the solution is + Checks how good the solution is. :param solution: Provided solution - :return: Evaluation and the time it took to create it + :return: Tuple witht the evaluation and the time it took to create it """ pass @abstractmethod - def generate_problem(self, config: Dict) -> Any: + def generate_problem(self, config: dict) -> any: """ - Creates a concrete problem and returns it + Creates a concrete problem and returns it. :param config: Configuration for problem creation :return: Generated problem """ pass - def process_solution(self, solution: Any) -> Tuple[Any, float]: + def process_solution(self, solution: any) -> tuple[any, float]: """ Most of the time the solution has to be processed before it can be validated and evaluated. This might not be necessary in all cases, so the default is to return the original solution. :param solution: Proposed solution - :return: Processed solution and the execution time to process it + :return: Tuple with processed solution and the execution time to process it """ return solution, 0.0 - def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ For optimization problems, we generate the actual problem instance in the preprocess function. @@ -86,7 +85,7 @@ def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, floa output = self.generate_problem(config) return output, end_time_measurement(start) - def postprocess(self, input_data: Any, config: dict, **kwargs) -> Tuple[Any, float]: + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ For optimization problems, we process the solution here, then validate and evaluate it. @@ -121,4 +120,6 @@ def postprocess(self, input_data: Any, config: dict, **kwargs) -> Tuple[Any, flo "time_to_evaluation": time_to_evaluation }) - return solution_validity, sum(filter(None, [time_to_process_solution, time_to_validation, time_to_evaluation])) + return solution_validity, sum(filter(None, [ + time_to_process_solution, time_to_validation, time_to_evaluation + ])) diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index 12faf91d..df1b500e 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -13,7 +13,7 @@ # limitations under the License. import itertools -from typing import TypedDict, List, Tuple +from typing import TypedDict import pickle import logging import os @@ -28,32 +28,36 @@ class PVC(Optimization): """ - In modern vehicle manufacturing, robots - take on a significant workload, including performing welding - jobs, sealing welding joints, or applying paint to the car body. - While the robot’s tasks vary widely, the objective remains - the same: Perform a job with the highest possible quality in the - shortest amount of time. For instance, to protect a car’s underbody - from corrosion, exposed welding seams are sealed by applying - a polyvinyl chloride layer (PVC). The welding seams need to be - traversed by a robot to apply the material. - It is related to TSP, but different and even more complex in some - aspects. + In modern vehicle manufacturing, robots take on a significant workload, including performing welding + jobs, sealing welding joints, or applying paint to the car body. While the robot’s tasks vary widely, + the objective remains the same: Perform a job with the highest possible quality in the shortest amount + of time, optimizing efficiency and productivity on the manufacturing line. + + For instance, to protect a car’s underbody from corrosion, exposed welding seams are sealed + by applying a polyvinyl chloride layer (PVC). The welding seams need to be traversed by a robot to + apply the material. It is related to TSP, but different and even more complex in some aspects. + + The problem of determining the optimal route for robots to traverse all seams shares similarities + with Traveling Salesman Problem (TSP), as it involves finding the shortest possible route to + visit multiple locations. However, it introduces additional complexities, such as different tool + and configuration requirements for each seam, making it an even more challenging problem to solve. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("PVC") - self.submodule_options = ["Ising", "QUBO", "GreedyClassicalPVC", "ReverseGreedyClassicalPVC", "RandomPVC"] + self.submodule_options = [ + "Ising", "QUBO", "GreedyClassicalPVC", "ReverseGreedyClassicalPVC", "RandomPVC" + ] @staticmethod - def get_requirements() -> List[dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "networkx", "version": "3.2.1"}, @@ -61,6 +65,11 @@ def get_requirements() -> List[dict]: ] def get_solution_quality_unit(self) -> str: + """ + Returns the unit of measure for solution quality. + + :return: Unit of measure for solution quality + """ return "Tour cost" def get_default_submodule(self, option: str) -> Core: @@ -94,15 +103,14 @@ def get_parameter_options(self) -> dict: Returns the configurable settings for this application. :return: Dictionary containing parameter options - .. code-block:: python - - return { - "seams": { - "values": list(range(1, 18)), - "description": "How many seams does your graph need?" - } - } - + .. code-block:: python + + return { + "seams": { + "values": list(range(1, 18)), + "description": "How many seams does your graph need?" + } + } """ return { "seams": { @@ -126,7 +134,7 @@ def generate_problem(self, config: Config) -> nx.Graph: Uses the reference graph to generate a problem for a given config. :param config: Config specifying the number of seams for the problem - :return: networkx graph representing the problem + :return: Networkx graph representing the problem """ if config is None: config = {"seams": 3} @@ -139,7 +147,7 @@ def generate_problem(self, config: Config) -> nx.Graph: # Get number of seam in graph seams_in_graph = list({x[0] for x in graph.nodes}) seams_in_graph.sort() - seams_in_graph.remove(0) # always need the base node 0 (which is not a seam) + seams_in_graph.remove(0) # Always need the base node 0 (which is not a seam) if len(seams_in_graph) < seams: logging.info("Too many seams! The original graph has less seams than that!") @@ -161,12 +169,19 @@ def generate_problem(self, config: Config) -> nx.Graph: tool = list(set(tool + [x[2]['t_end'] for x in graph.edges(data=True)])) # fill the rest of the missing edges with high values - current_edges = [(edge[0], edge[1], edge[2]['t_start'], edge[2]['t_end'], edge[2]['c_start'], edge[2]['c_end']) - for edge in graph.edges(data=True)] + current_edges = [ + (edge[0], edge[1], edge[2]['t_start'], edge[2]['t_end'], edge[2]['c_start'], edge[2]['c_end']) + for edge in graph.edges(data=True) + ] all_possible_edges = list(itertools.product(list(graph.nodes), repeat=2)) - all_possible_edges = [(edges[0], edges[1], t_start, t_end, c_start, c_end) for edges in all_possible_edges for - c_end in config for c_start in config for t_end in tool - for t_start in tool if edges[0] != edges[1]] + all_possible_edges = [ + (edges[0], edges[1], t_start, t_end, c_start, c_end) + for edges in all_possible_edges + for c_end in config + for c_start in config + for t_end in tool + for t_start in tool if edges[0] != edges[1] + ] missing_edges = [item for item in all_possible_edges if item not in current_edges] @@ -184,7 +199,7 @@ def generate_problem(self, config: Config) -> nx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: dict) -> tuple[List, bool]: + def process_solution(self, solution: dict) -> tuple[list, bool]: """ Converts solution dictionary to list of visited seams. @@ -200,7 +215,7 @@ def process_solution(self, solution: dict) -> tuple[List, bool]: if sum(value == 1 for value in solution.values()) > len(route): logging.warning("Result is longer than route! This might be problematic!") - # NOTE: Prevent duplicate node entries by enforcing only one occurrence per node along route + # Prevent duplicate node entries by enforcing only one occurrence per node along route for (node, config, tool, timestep), val in solution.items(): if val and (node[0] not in visited_seams): if route[timestep] is not None: @@ -255,14 +270,14 @@ def validate(self, solution: list) -> tuple[bool, float]: def evaluate(self, solution: list) -> tuple[float, float]: """ - Calculates the tour length for a given valid tour + Calculates the tour length for a given valid tour. :param solution: List containing the nodes of the solution :return: Tour length, time it took to calculate the tour length """ start = start_time_measurement() - # get the total distance + # Get the total distance total_dist = 0 for idx, _ in enumerate(solution[:-1]): edge = next( @@ -274,7 +289,7 @@ def evaluate(self, solution: list) -> tuple[float, float]: total_dist += dist logging.info(f"Total distance (without return): {total_dist}") - # add distance between start and end point to complete cycle + # Add distance between start and end point to complete cycle return_edge = next( item for item in list(self.application[solution[0][0]][solution[-1][0]].values()) if item["c_start"] == solution[0][1] and item["t_start"] == solution[0][2] and @@ -283,7 +298,7 @@ def evaluate(self, solution: list) -> tuple[float, float]: return_distance = return_edge['weight'] logging.info(f"Distance between start and end: {return_distance}") - # get distance for full cycle + # Get distance for full cycle distance = total_dist + return_distance logging.info(f"Total distance (including return): {distance}") diff --git a/src/modules/applications/optimization/PVC/data/createReferenceGraph.py b/src/modules/applications/optimization/PVC/data/createReferenceGraph.py index 83a1ea5c..148fe444 100644 --- a/src/modules/applications/optimization/PVC/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/PVC/data/createReferenceGraph.py @@ -21,10 +21,7 @@ with open("reference_data.txt") as infile: for line in infile: line_elements = line.split() - - # Print line elements for debugging purposes - print(line_elements) - + # Extract start and end attributes from line elements r_start, s_start, n_start, c_start, t_start, l_start = map(int, line_elements[1:7]) r_end, s_end, n_end, c_end, t_end, l_end = map(int, line_elements[8:14]) diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index 88b257e3..e41faa1e 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Dict, Tuple +from typing import TypedDict import logging import networkx as nx @@ -52,20 +52,19 @@ def get_requirements() -> list[Dict]: *QUBO.get_requirements() ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. :return: Dictionary containing parameter options. - .. code-block:: python - - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" - } - } - + .. code-block:: python + + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your lagrange?" + } + } """ return { "lagrange_factor": { @@ -83,9 +82,9 @@ class Config(TypedDict): """ lagrange_factor: float - def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ - Uses the PVC QUBO formulation and converts it to an Ising + Uses the PVC QUBO formulation and converts it to an Ising representation. :param problem: Networkx graph representing the PVC problem :param config: Config dictionary with the mapping configuration @@ -103,6 +102,7 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: # Extract unique configuration and tool attributes from the graph config = [x[2]['c_start'] for x in problem.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in problem.edges(data=True)])) + tool = [x[2]['t_start'] for x in problem.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in problem.edges(data=True)])) @@ -127,7 +127,7 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: return {"J": j_matrix, "t": np.array(list(t.values()))}, end_time_measurement(start) - def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the PVC class for validation/evaluation. diff --git a/src/modules/applications/optimization/PVC/mappings/QUBO.py b/src/modules/applications/optimization/PVC/mappings/QUBO.py index 7c6b8ee4..df60355d 100644 --- a/src/modules/applications/optimization/PVC/mappings/QUBO.py +++ b/src/modules/applications/optimization/PVC/mappings/QUBO.py @@ -14,7 +14,7 @@ import itertools from collections import defaultdict -from typing import TypedDict, Dict, Tuple +from typing import TypedDict import logging import networkx as nx @@ -40,23 +40,23 @@ def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dictionaries with requirements of this module + :return: List of dictionaries with requirements of this module """ return [{"name": "networkx", "version": "3.2.1"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. :return: Dictionary containing parameter options - .. code-block:: python + .. code-block:: python - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" - } - } + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your lagrange?" + } + } """ return { @@ -76,7 +76,7 @@ class Config(TypedDict): """ lagrange_factor: float - def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to a QUBO formulation. @@ -92,7 +92,7 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: n = problem.number_of_nodes() timesteps = int((n - 1) / 2 + 1) - # Let`s get the number of different configs and tools + # Get the number of different configs and tools config = [x[2]['c_start'] for x in problem.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in problem.edges(data=True)])) diff --git a/src/modules/applications/optimization/SAT/SAT.py b/src/modules/applications/optimization/SAT/SAT.py index 9fa13371..d4d4c41d 100644 --- a/src/modules/applications/optimization/SAT/SAT.py +++ b/src/modules/applications/optimization/SAT/SAT.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import nnf import numpy as np @@ -27,16 +27,33 @@ class SAT(Optimization): """ - Before a new vehicle model can be deployed for production, several tests have to be carried out on pre-series - vehicles to ensure the feasibility and gauge the functionality of specific configurations of components. - Naturally, the manufacturer wants to save resources and produce as few pre-series vehicles as possible while - still performing all desired tests. Further, not all feature configurations can realistically be implemented in - all vehicles, leading to constraints that the produced vehicles must satisfy. This can be modeled as a SAT problem. + The SAT (Satisfiability) problem plays a crucial role in the field of computational optimization. In the context + of vehicle manufacturing, it is essential to test various pre-series vehicle configurations to ensure they meet + specific requirements before production begins. This testing involves making sure that each vehicle configuration + complies with several hard constraints related to safety, performance, and buildability while also fulfilling + soft constraints such as feature combinations or specific requirements for testing. The SAT problem models these + constraints in a way that enables a systematic approach to determine feasible vehicle configurations and minimize + the need for excessive physical prototypes. + + This problem is modeled as a Max-SAT problem, where the aim is to find a configuration that satisfies as many + constraints as possible while balancing between the number of satisfied hard and soft constraints. The formulation + uses a conjunctive normal form (CNF) representation of logical expressions to model the dependencies and + incompatibilities between various features and components in vehicle assembly. By leveraging optimization + algorithms, the SAT module aims to produce a minimal but sufficient set of configurations, ensuring that all + necessary tests are performed while minimizing resource usage. This approach helps in creating a robust testing + framework and reducing the overall cost of vehicle development. + + To solve the SAT problem, various approaches are employed, including translating the CNF representation into + different quantum and classical optimization mappings such as QUBO (Quadratic Unconstrained Binary Optimization) + or Ising formulations. These mappings make the SAT problem suitable for solving on quantum computers and + classical annealers. The SAT problem in this module is implemented with a flexible interface, allowing integration + with a range of solvers that can exploit different computational paradigms, making it adaptable for a variety of + hardware and optimization backends. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("SAT") self.submodule_options = [ @@ -48,11 +65,11 @@ def __init__(self): self.num_variables = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "nnf", "version": "0.4.1"}, @@ -63,7 +80,13 @@ def get_solution_quality_unit(self) -> str: return "Evaluation" def get_default_submodule(self, option: str) -> Application: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "QubovertQUBO": from modules.applications.optimization.SAT.mappings.QubovertQUBO import \ QubovertQUBO # pylint: disable=C0415 @@ -87,45 +110,44 @@ def get_default_submodule(self, option: str) -> Application: else: raise NotImplementedError(f"Mapping Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application - - :return: Dict with cnfigurable settings - .. code-block:: python - - return { - "variables": { - "values": list(range(10, 151, 10)), - "custom_input": True, - "allow_ranges": True, - "postproc": int, - "description": "How many variables do you need?" - }, - "clvar_ratio_cons": { - "values": [2, 3, 4, 4.2, 5], - "custom_input": True, - "allow_ranges": True, - "postproc": int, - "description": "What clause:variable ratio do you want for the (hard) constraints?" - }, - "clvar_ratio_test": { - "values": [2, 3, 4, 4.2, 5], - "custom_input": True, - "allow_ranges": True, - "postproc": int, - "description": "What clause:variable ratio do you want for the tests (soft con.)?" - }, - "problem_set": { - "values": list(range(10)), - "description": "Which problem set do you want to use?" - }, - "max_tries": { - "values": [100], - "description": "Maximum number of tries to create problem" - } - } + Returns the configurable settings for this application. + + :return: Dictionary with cnfigurable settings + .. code-block:: python + return { + "variables": { + "values": list(range(10, 151, 10)), + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "How many variables do you need?" + }, + "clvar_ratio_cons": { + "values": [2, 3, 4, 4.2, 5], + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "What clause:variable ratio do you want for the (hard) constraints?" + }, + "clvar_ratio_test": { + "values": [2, 3, 4, 4.2, 5], + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "What clause:variable ratio do you want for the tests (soft con.)?" + }, + "problem_set": { + "values": list(range(10)), + "description": "Which problem set do you want to use?" + }, + "max_tries": { + "values": [100], + "description": "Maximum number of tries to create problem" + } + } """ return { "variables": { @@ -161,7 +183,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -178,11 +200,12 @@ class Config(TypedDict): problem_set: int max_tries: int - def generate_problem(self, config: Config) -> Tuple[nnf.And, list]: + def generate_problem(self, config: Config) -> tuple[nnf.And, list]: """ - Generates a vehicle configuration problem out of a given config. Returns buildability constraints (hard - constraints) and tests (soft constraints), the successful evaluation of which we try to maximize. Both - are given in nnf form, which we then convert accordingly. + Generates a vehicle configuration problem out of a given config. + Returns buildability constraints (hard constraints) and tests (soft + constraints), the successful evaluation of which we try to maximize. + Both are given in nnf form, which we then convert accordingly. :param config: Configuration parameters for problem generation :return: A tuple containing the problem, number of variables, and other details @@ -191,10 +214,8 @@ def generate_problem(self, config: Config) -> Tuple[nnf.And, list]: self.num_variables = config["variables"] num_constraints = round(config["clvar_ratio_cons"] * self.num_variables) num_tests = round(config["clvar_ratio_test"] * self.num_variables) - max_tries = config["max_tries"] self.literals = [Var(f"L{i}") for i in range(self.num_variables)] - self.application = {} def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): @@ -211,40 +232,41 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): # Select three (non-repeated) literals and negate them randomly -- together constituting a clause chosen_literals = rng.choice(lit_vars, 3, replace=False) negate_literals = rng.choice([True, False], 3, replace=True) - clause = [] # Perform the random negations and append to clause: - for lit, neg in zip(chosen_literals, negate_literals): - if neg: - clause.append(lit.negate()) - else: - clause.append(lit) + clause = [ + lit.negate() if neg else lit + for lit, neg in zip(chosen_literals, negate_literals) + ] # Append the generated clause to the total container clause_list.append(Or(clause)) prob = And(clause_list) - if not satisfiable or prob.satisfiable(): return clause_list - # loop ran out of tries + # Loop ran out of tries logging.error("Unable to generate valid solutions. Consider increasing max_tries or decreasing " "the clause:variable ratio.") raise ValueError("Unable to generate valid solution.") - # we choose a random seed -- since we try at most max_tries times to generate a solvable instance, + # Choose a random seed -- since we try at most max_tries times to generate a solvable instance, # Space the initial random seeds by 2 * max_tries (because we need both hard and soft constraints). random_seed = 2 * config["problem_set"] * max_tries - # generate hard & soft constraints. Make both satisfiable, but this can in principle be tuned. - hard = And(_generate_3sat_clauses(num_constraints, self.num_variables, - satisfiable=True, rseed=random_seed, nr_tries=max_tries)) - # the random_seed + 1 ensures that a different set of seeds is sampled compared to the hard constraints. - soft = _generate_3sat_clauses(num_tests, self.num_variables, satisfiable=True, rseed=random_seed + 1, - nr_tries=config["max_tries"]) + # Generate hard & soft constraints. Make both satisfiable, but this can in principle be tuned. + hard = And(_generate_3sat_clauses( + num_constraints, self.num_variables, satisfiable=True, + rseed=random_seed, nr_tries=max_tries + )) + # The random_seed + 1 ensures that a different set of seeds is sampled compared to the hard constraints. + soft = _generate_3sat_clauses( + num_tests, self.num_variables, satisfiable=True, + rseed=random_seed + 1, nr_tries=config["max_tries"] + ) if (hard is None) or (soft is None): raise ValueError("Unable to generate satisfiable") - # saving constraints and tests + # Saving constraints and tests self.application["constraints"] = hard self.application["tests"] = soft - # and their cardinalities: + # And their cardinalities: self.num_constraints = len(hard) self.num_tests = len(soft) @@ -253,7 +275,7 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): f" and {self.num_tests} tests") return hard, soft - def validate(self, solution: Dict) -> Tuple[bool, float]: + def validate(self, solution: dict) -> tuple[bool, float]: """ Validate a given solution against the constraints. @@ -268,22 +290,21 @@ def validate(self, solution: Dict) -> Tuple[bool, float]: )) ratio = nr_satisfied_hardcons / self.num_constraints is_valid = ratio == 1.0 - # prints the ratio of satisfied constraints and prints if all constraints are satisfied logging.info(f"Ratio of satisfied constraints: {ratio}\nSuccess:{['no', 'yes'][int(is_valid)]}") return is_valid, end_time_measurement(start) - def evaluate(self, solution: Dict) -> Tuple[float, float]: + def evaluate(self, solution: dict) -> tuple[float, float]: """ Calculates the quality of the solution. - :param solution: dictionary containing the solution + :param solution: Dictionary containing the solution :return: Tour length, time it took to calculate the tour length """ start = start_time_measurement() logging.info("Checking the quality of the solution:") - # count the number of satisfied clauses + # Count the number of satisfied clauses nr_satisfied_tests = len(*np.where([test.satisfied_by(solution) for test in self.application["tests"]])) ratio_satisfied = nr_satisfied_tests / self.num_tests diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py index 2bdbde94..976f2d29 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Any, Dict, Tuple +from typing import TypedDict import numpy as np from dimod import qubo_to_ising @@ -24,12 +24,12 @@ class ChoiIsing(Mapping): """ - Ising formulation for SAT problem using QUBO by Choi (1004.2226) + Ising formulation for SAT problem using QUBO by Choi (1004.2226). """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA"] @@ -39,10 +39,9 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ {"name": "numpy", "version": "1.26.4"}, @@ -52,23 +51,22 @@ def get_requirements() -> list[dict]: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. :return: Dictionary with parameter options - .. code-block:: python - - return { - "hard_reward": { - "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" - }, - "soft_reward": { - "values": [0.1, 1, 2], - "description": "What Bh/Bs ratio do you want? This value is multiplied with the " - "number of tests." - } - } + .. code-block:: python + return { + "hard_reward": { + "values": [0.1, 0.5, 0.9, 0.99], + "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + }, + "soft_reward": { + "values": [0.1, 1, 2], + "description": "What Bh/Bs ratio do you want? This value is multiplied with the " + "number of tests." + } + } """ return { "hard_reward": { @@ -83,7 +81,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -94,16 +92,17 @@ class Config(TypedDict): hard_reward: float soft_reward: float - def map(self, problem: Any, config: Config) -> Tuple[dict, float]: + def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Uses the ChoiQUBO formulation and converts it to an Ising. :param problem: the SAT problem - :param config: dictionary with the mapping config - :return: dict with the ising, time it took to map it + :param config: Dictionary with the mapping config + :return: Dict with the ising, time it took to map it """ start = start_time_measurement() self.problem = problem + # call mapping function self.qubo_mapping = ChoiQUBO() q, _ = self.qubo_mapping.map(problem, config) @@ -122,19 +121,17 @@ def map(self, problem: Any, config: Config) -> Tuple[dict, float]: return {"J": j_matrix, "t": t_vector}, end_time_measurement(start) - def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() # convert raw solution into the right format to use reverse_map() of ChoiQUBO.py - solution_dict = {} - for i, el in enumerate(solution): - solution_dict[i] = el + solution_dict = {i: el for i, el in enumerate(solution)} # reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) @@ -143,10 +140,11 @@ def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Return the default submodule based on the given option. + Returns the default submodule for the given option. - :param option: the submodule option - :return: the default submodule + :param option: The submodule option + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py index d918d531..3ca89768 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py @@ -13,7 +13,7 @@ # limitations under the License. from itertools import combinations, product -from typing import TypedDict, List, Dict, Tuple +from typing import TypedDict import logging from nnf import Var, And @@ -29,7 +29,7 @@ class ChoiQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -37,35 +37,32 @@ def __init__(self): self.reverse_dict = None @staticmethod - def get_requirements() -> list[Dict]: + def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ - return [ - {"name": "nnf", "version": "0.4.1"} - ] + return [{"name": "nnf", "version": "0.4.1"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. :return: Dictionary with parameter options - .. code-block:: python - - return { - "hard_reward": { - "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" - }, - "soft_reward": { - "values": [0.1, 1, 2], - "description": "What Bh/Bs ratio do you want? This value is multiplied with the " - "number of tests." - } - } + .. code-block:: python + return { + "hard_reward": { + "values": [0.1, 0.5, 0.9, 0.99], + "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + }, + "soft_reward": { + "values": [0.1, 1, 2], + "description": "What Bh/Bs ratio do you want? This value is multiplied with the " + "number of tests." + } + } """ return { "hard_reward": { @@ -86,7 +83,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -97,14 +94,14 @@ class Config(TypedDict): hard_reward: float soft_reward: float - def map(self, problem: Tuple[And, List], config: Config) -> Tuple[Dict, float]: + def map(self, problem: tuple[And, list], config: Config) -> tuple[dict, float]: """ - Converts a MaxSAT instance with hard and soft constraints into a graph problem -- solving MaxSAT then - corresponds to solving an instance of the Maximal Independent Set problem. See Andrew Lucas (2014), - or the original publication by Choi (1004.2226). + Converts a MaxSAT instance with hard and soft constraints into a graph problem -- + solving MaxSAT then corresponds to solving an instance of the Maximal Independent Set problem. + See Andrew Lucas (2014), or the original publication by Choi (1004.2226). :param problem: A tuple conatining hard and soft constraints - :param config: config with the parameters specified in Config class + :param config: Config with the parameters specified in Config class :return: Dictionary containing the QUBO representation and the time taken """ start = start_time_measurement() @@ -188,12 +185,12 @@ def _remap_pair(pair): f" Bs={Bs}.") return {'Q': Q}, end_time_measurement(start) - def reverse_map(self, solution: Dict) -> Tuple[dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() # we define the literals list, so that we can check the self-consistency of the solution. That is, we save all @@ -202,6 +199,7 @@ def reverse_map(self, solution: Dict) -> Tuple[dict, float]: literals = [] # assignments saves the actual solution assignments = [] + for node, tf in solution.items(): # Check if node is included in the set (i.e. if tf is True (1)) if tf: @@ -220,6 +218,7 @@ def reverse_map(self, solution: Dict) -> Tuple[dict, float]: lit = Var(lit_str) assignments.append(Var(lit_str.split('-')[0])) literals.append(lit) + # Check for self-consistency of solution; Check that the assignments of all literals are consistent: if not And(set(literals)).satisfiable(): logging.error('Generated solution is not self-consistent!') @@ -238,10 +237,11 @@ def reverse_map(self, solution: Dict) -> Tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Return the default submodule based on the given option. + Returns the default submodule for the given option. - :param option: the submodule option - :return: the default submodule + :param option: The submodule option + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py index d5deed04..089225df 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import numpy as np from dimod import qubo_to_ising @@ -26,12 +26,11 @@ class DinneenIsing(Mapping): """ Ising formulation for SAT using Dinneen QUBO. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA"] @@ -39,11 +38,11 @@ def __init__(self): self.qubo_mapping = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "nnf", "version": "0.4.1"}, @@ -52,21 +51,20 @@ def get_requirements() -> List[Dict]: *DinneenQUBO.get_requirements() ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: dict with parameter options - .. code-block:: python + Returns the configurable settings for this mapping. - return { - "lagrange": { - "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) " - "constraints?" - } - } + :return: Dictionary with parameter options + .. code-block:: python + return { + "lagrange": { + "values": [0.1, 1, 2], + "description": "What lagrange parameter to multiply with the number of (hard) " + "constraints?" + } + } """ return { "lagrange": { @@ -77,7 +75,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -86,16 +84,17 @@ class Config(TypedDict): """ lagrange: float - def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: + def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Uses the DinneenQUBO formulation and converts it to an Ising. :param problem: the SAT problem - :param config: dictionary with the mapping config - :return: dict with the ising, time it took to map it + :param config: Dictionary with the mapping config + :return: Dict with the ising, time it took to map it """ start = start_time_measurement() self.problem = problem + # call mapping function self.qubo_mapping = DinneenQUBO() q, _ = self.qubo_mapping.map(problem, config) @@ -114,28 +113,30 @@ def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: return {"J": j_matrix, "t": t_vector}, end_time_measurement(start) - def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() - # convert raw solution into the right format to use reverse_map() of ChoiQUBO.py + + # Convert raw solution into the right format to use reverse_map() of ChoiQUBO.py solution_dict = {i: el for i, el in enumerate(solution) } - # reverse map + # Reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: """ - Return the default submodule based on the given option. + Returns the default submodule for the given option. - :param option: the submodule option - :return: the default submodule + :param option: The submodule option + :return: The default submodule for the given option + :return NotImplementedError: If the submodule option is not implemented """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py index c13d282f..89bf8863 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py @@ -13,7 +13,7 @@ # limitations under the License. from itertools import combinations -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import logging from nnf import And @@ -29,37 +29,34 @@ class DinneenQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] self.nr_vars = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ - return [ - {"name": "nnf", "version": "0.4.1"} - ] + return [{"name": "nnf", "version": "0.4.1"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. - :return: dict with parameter options - .. code-block:: python - - return { - "lagrange": { - "values": [0.1, 1, 2], - "description": "What lagrange param. to multiply with the number of (hard) constr.?" - } - } + :return: Dictionary with parameter options + .. code-block:: python + return { + "lagrange": { + "values": [0.1, 1, 2], + "description": "What lagrange param. to multiply with the number of (hard) constr.?" + } + } """ return { "lagrange": { @@ -70,7 +67,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -79,50 +76,49 @@ class Config(TypedDict): """ lagrange: float - def map(self, problem: Tuple[And, List], config: Config) -> Tuple[Dict, float]: + def map(self, problem: tuple[And, list], config: Config) -> tuple[dict, float]: """ Performs the mapping into a QUBO formulation, as given by Dinneen. See also the QUARK paper. - + :param problem: SAT problem - :param config: config with the parameters specified in Config class - :return: tuple with the QUBO, time it took to map it + :param config: Config with the parameters specified in Config class + :return: Tuple with the QUBO, time it took to map it """"" start = start_time_measurement() - # extract hard and soft constraints from the generated problem + + # Extract hard and soft constraints from the generated problem hard, soft = problem - # count the variables + + # Count the variables self.nr_vars = len(hard.vars().union(And(soft).vars())) lagrange = config['lagrange'] - # lagrange parameter is a factor of the number of soft constraints. + # Lagrange parameter is a factor of the number of soft constraints. lagrange *= len(soft) - def _add_clause( - curr_qubo_dict: Dict[Tuple[int, int], float], - clause: Any, - pos: int, - weight: float - ) -> Dict[Tuple[int, int], float]: - + def _add_clause(curr_qubo_dict: dict[tuple[int, int], float], + clause: any, + pos: int, + weight: float) -> dict[tuple[int, int], float]: """ Function that adds the QUBO terms corresponding to the clause and updates the QUBO dictionary accordingly. Additionally, the weight of the clause is taken into account. - :param curr_qubo_dict: current QUBO dictionary - :param clause: clause to be added - :param pos: position of the auxiliary variable - :param weight: weight of the clause - :return: updated QUBO dictionary + :param curr_qubo_dict: Current QUBO dictionary + :param clause: Clause to be added + :param pos: Position of the auxiliary variable + :param weight: Weight of the clause + :return: Updated QUBO dictionary """ - def _check_and_add(dictionary: Dict, key: Tuple[int, int], value: float) -> Dict: + def _check_and_add(dictionary: dict, key: tuple[int, int], value: float) -> dict: """ Helper function that checks if key is present or not in dictionary and adds a value, adding the key if missing. - :param dictionary: dictionary to be updated - :param key: key to check in the dictionary - :param value: value to add to the key - :return: updated dictionary + :param dictionary: Dictionary to be updated + :param key: Key to check in the dictionary + :param value: Value to add to the key + :return: Updated dictionary """ key = tuple(sorted(key)) if key not in dictionary.keys(): @@ -134,20 +130,20 @@ def _check_and_add(dictionary: Dict, key: Tuple[int, int], value: float) -> Dict cl_dict = {} for variable in clause.children: for variable_name in variable.vars(): - # transforms the negations (0,1) into signs (-1, 1) + # Transforms the negations (0,1) into signs (-1, 1) cl_dict[int(variable_name[1:])] = (int(variable.true) - 1 / 2) * 2 - # add the linear term of the auxiliary variable w + # Add the linear term of the auxiliary variable w curr_qubo_dict = _check_and_add(curr_qubo_dict, (pos, pos), 2 * weight) - # add x linear terms and xw terms. + # Add x linear terms and xw terms. for qvar, val in cl_dict.items(): # qvar is the name of the var, val is the sign corresponding to whether the variable is negated or not. # linear x term: curr_qubo_dict = _check_and_add(curr_qubo_dict, (qvar, qvar), -weight * val) # x * w (aux. var.) term curr_qubo_dict = _check_and_add(curr_qubo_dict, (qvar, pos), -weight * val) - # add combinations + # Add combinations for q1, q2 in combinations(cl_dict.keys(), 2): curr_qubo_dict = _check_and_add(curr_qubo_dict, (q1, q2), weight * cl_dict[q1] * cl_dict[q2]) @@ -157,20 +153,24 @@ def _check_and_add(dictionary: Dict, key: Tuple[int, int], value: float) -> Dict # Add the hard constraints and add the lagrange parameter as weight for clause_ind, hard_clause in enumerate(hard): qubo_dict = _add_clause(qubo_dict, hard_clause, self.nr_vars + clause_ind, lagrange) + # Add the soft constraints and start the enumeration at the final index corresponding to hard cons. for clause_ind, soft_clause in enumerate(soft): qubo_dict = _add_clause(qubo_dict, soft_clause, self.nr_vars + clause_ind + len(hard), 1) - logging.info(f"Generate Dinneen QUBO with {self.nr_vars + len(hard) + len(soft)} binary variables." - f" Lagrange parameter used was: {config['lagrange']}.") + logging.info( + f"Generate Dinneen QUBO with {self.nr_vars + len(hard) + len(soft)} binary variables." + f" Lagrange parameter used was: {config['lagrange']}." + ) + return {"Q": qubo_dict}, end_time_measurement(start) - def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Reverse mapping of the solution obtained from the Dinneen QUBO. - :param solution: dictionary containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() mapped_sol = {} @@ -180,14 +180,15 @@ def reverse_map(self, solution: Dict) -> Tuple[Dict, float]: mapped_sol[f'L{i}'] = True else: mapped_sol[f'L{i}'] = bool(solution[i]) + return mapped_sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: """ Return the default submodule based on the given option. - :param option: the submodule option - :return: the default submodule + :param option: The submodule option + :return: The default submodule """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/Direct.py b/src/modules/applications/optimization/SAT/mappings/Direct.py index 4e098cc3..dee6f11c 100644 --- a/src/modules/applications/optimization/SAT/mappings/Direct.py +++ b/src/modules/applications/optimization/SAT/mappings/Direct.py @@ -13,7 +13,7 @@ # limitations under the License. import io -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import logging from nnf import And @@ -31,7 +31,7 @@ class Direct(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["ClassicalSAT", "RandomSAT"] @@ -39,20 +39,20 @@ def __init__(self): @staticmethod def get_requirements() -> List[Dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "nnf", "version": "0.4.1"}, {"name": "python-sat", "version": "1.8.dev13"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. - :return: empty dict + :return: Empty dict """ return {} @@ -62,40 +62,52 @@ class Config(TypedDict): """ pass - def map(self, problem: Tuple[And, List], config: Config) -> Tuple[WCNF, float]: + def map(self, problem: tuple[And, list], config: Config) -> tuple[WCNF, float]: """ - We map from the nnf library into the python-sat library. + Map from the nnf library into the python-sat library. :param problem: SAT problem - :param config: config with the parameters specified in Config class - :return: mapped problem and the time it took to map it + :param config: Config with the parameters specified in Config class + :return: Mapped problem and the time it took to map it """ start = start_time_measurement() hard_constraints, soft_constraints = problem - # get number of vars. The union is required in case not all vars are present in either tests/constraints. + + # Get number of vars. The union is required in case not all vars are present in either tests/constraints. nr_vars = len(hard_constraints.vars().union(And(soft_constraints).vars())) - # create a var_labels dictionary that will be used when mapping to pysat + + # Create a var_labels dictionary that will be used when mapping to pysat litdic = {f'L{i - 1}': i for i in range(1, nr_vars + 1)} + # The most convenient way to map between nnf and pysat was to use the native nnf dump function, which exports # the problem as a string, which we can then quickly reload from a buffer. - # create buffers for dumping: + + # Create buffers for dumping: hard_buffer = io.StringIO() soft_buffer = io.StringIO() - # dump constraints and tests to their respective buffers + + # Dump constraints and tests to their respective buffers dump(hard_constraints, hard_buffer, var_labels=litdic, mode='cnf') # tests have to be conjoined, since we will add them as soft constraints. dump(And(soft_constraints), soft_buffer, var_labels=litdic, mode='cnf') - # load the cnfs from the buffers: + # Load the cnfs from the buffers: hard_cnf = CNF(from_string=hard_buffer.getvalue()) soft_cnf = CNF(from_string=soft_buffer.getvalue()) - # create wcnf instance. + + # Create wcnf instance. total_wcnf = WCNF() - # add hard constraints: + + # Add hard constraints: total_wcnf.extend(hard_cnf) - # add soft constraints, with weights. + + # Add soft constraints, with weights. total_wcnf.extend(soft_cnf, weights=[1] * len(soft_cnf.clauses)) - logging.info(f'Generated pysat wcnf with {len(total_wcnf.hard)} constraints and {len(total_wcnf.soft)} tests.') + + logging.info( + f'Generated pysat wcnf with {len(total_wcnf.hard)} constraints and {len(total_wcnf.soft)} tests.' + ) + return total_wcnf, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: @@ -115,14 +127,13 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Solver Option {option} not implemented") - def reverse_map(self, solution: List) -> Tuple[dict, float]: + def reverse_map(self, solution: list) -> tuple[dict, float]: """ Maps the solution returned by the pysat solver into the reference format. - :param solution: list containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: List containing the solution + :return: Solution mapped accordingly, time it took to map it """ - start = start_time_measurement() # converts from (3 / -3) -> (L2 : True / L2: False) mapped_sol = {f'L{abs(lit) - 1}': (lit > 0) for lit in solution} diff --git a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py index 7d48a72f..745e3dee 100644 --- a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict from qubovert.sat import NOT, OR, AND from nnf import And @@ -29,7 +29,7 @@ class QubovertQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -37,31 +37,30 @@ def __init__(self): self.nr_vars = None @staticmethod - def get_requirements() -> list[Dict]: + def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "nnf", "version": "0.4.1"}, {"name": "qubovert", "version": "1.2.5"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. - :return: dict with configurable settings - .. code-block:: python - - return { - "lagrange": { - "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], - "description": "What lagrange for the qubo mapping? 1 the number of tests." - } - } + :return: Dict with configurable settings + .. code-block:: python + return { + "lagrange": { + "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], + "description": "What lagrange for the qubo mapping? 1 the number of tests." + } + } """ return { "lagrange": { @@ -72,7 +71,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -86,8 +85,8 @@ def _constraints2qubovert(constraints: dict) -> AND: """ Converts the constraints nnf to a pubo in the qubovert library. - :param constraints: constraints in nnf format - :return: constraints in qubovert format + :param constraints: Constraints in nnf format + :return: Constraints in qubovert format """ clauses = [] for c in constraints.children: @@ -100,8 +99,8 @@ def _tests2qubovert(test_clauses: dict) -> sum: """ Converts the list of test clauses in the nnf format to a pubo. - :param test_clauses: test clauses in nnf format - :return: sum of mapped test clauses + :param test_clauses: Test clauses in nnf format + :return: Sum of mapped test clauses """ mapped_tests = [] @@ -110,20 +109,20 @@ def _tests2qubovert(test_clauses: dict) -> sum: return sum(mapped_tests) - def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: + def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Converts the problem to a Qubo in dictionary format. Problem is a CNF formula from the nnf library. :param problem: SAT problem - :param config: config with the parameters specified in Config class - :return: dict with the QUBO, time it took to map it + :param config: Config with the parameters specified in Config class + :return: Dict with the QUBO, time it took to map it """ start = start_time_measurement() lagrange = config['lagrange'] constraints, test_clauses = problem - # find number of the variables that appear in the tests and constraints, to verify the reverse mapping. + # Find number of the variables that appear in the tests and constraints, to verify the reverse mapping. self.nr_vars = len(constraints.vars().union(And(test_clauses).vars())) # Convert the constraints to qubovert: @@ -134,9 +133,10 @@ def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: logging.info(f'{tests_pubo.to_qubo().num_terms} number of terms in tests qubo') lagrange *= len(test_clauses) - # define the total pubo problem: + # Define the total pubo problem: self.pubo_problem = -(tests_pubo + lagrange * constraints_pubo) - # convert to qubo: + + # Convert to qubo: qubo_problem = self.pubo_problem.to_qubo() qubo_problem.normalize() logging.info(f"Converted to QUBO with {qubo_problem.num_binary_variables} Variables." @@ -161,20 +161,20 @@ def map(self, problem: Any, config: Config) -> Tuple[Dict, float]: return {"Q": q_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> Tuple[Dict, float]: + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() pubo_sol = self.pubo_problem.convert_solution(solution) - # Let's check if all variables appear in the solution. + # Check if all variables appear in the solution. missing_vars = {f'L{i}' for i in range(self.nr_vars)} - set(pubo_sol.keys()) - # add values for the missing variables -- if they do not appear, then their assignment does not matter. + # Add values for the missing variables -- if they do not appear, then their assignment does not matter. for missing_var in missing_vars: pubo_sol[missing_var] = True diff --git a/src/modules/applications/optimization/SCP/SCP.py b/src/modules/applications/optimization/SCP/SCP.py index 1541e795..04c6f337 100644 --- a/src/modules/applications/optimization/SCP/SCP.py +++ b/src/modules/applications/optimization/SCP/SCP.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Tuple, Set +from typing import TypedDict import pickle import os @@ -23,15 +23,29 @@ class SCP(Optimization): """ - The set cover problem (SCP) is an optimization problem where the goal is to find the smallest subset of a given set - of elements that covers all the elements. This can be formulated as selecting the minimum number of sets from a - collection of sets, such that the union of the selected sets contains all the elements of the problem instance. - This problem has applications in areas like sensor positioning, resource allocation, and network design. + The set cover problem (SCP) is a classical combinatorial optimization problem where the objective is to find the + smallest subset of given elements that covers all required elements in a collection. This can be formulated as + selecting the minimum number of sets from a collection such that the union of the selected sets contains all + elements from the universe of the problem instance. SCP is known for being computationally challenging due to its + NP-hard nature, which means finding an optimal solution becomes exponentially more difficult as the size of the + problem grows. + + SCP has widespread applications in various fields, including sensor positioning, resource allocation, and network + design. For example, in sensor positioning, SCP can help determine the fewest number of sensors required to cover + a given area. Similarly, in resource allocation, SCP helps to allocate resources in an optimal way, ensuring + coverage of all demand points while minimizing costs. Network design also uses SCP principles to efficiently place + routers or gateways in a network to ensure full coverage with minimal redundancy. + + This implementation of SCP provides configurable problem instances of different sizes, such as "Tiny," "Small," + and "Large," allowing the user to explore solutions with varying complexities. We employ various quantum-inspired + methods to solve SCP, including a mapping to the QUBO (Quadratic Unconstrained Binary Optimization) formulation + using Qubovert. These approaches allow us to explore how different optimization algorithms and frameworks perform + when applied to this challenging problem, offering insights into both classical and emerging quantum methods. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("SCP") self.submodule_options = ["qubovertQUBO"] @@ -40,6 +54,13 @@ def get_solution_quality_unit(self) -> str: return "Number of selected subsets" def get_default_submodule(self, option: str) -> Application: + """ + Returns the default submodule based on the provided option. + + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "qubovertQUBO": from modules.applications.optimization.SCP.mappings.qubovertQUBO import QubovertQUBO # pylint: disable=C0415 return QubovertQUBO() @@ -50,16 +71,16 @@ def get_parameter_options(self): """ Returns the configurable settings for this application - :return: - .. code-block:: python + :return: Dictionary containing parameter options + .. code-block:: python - return { - "model_select": { - "values": list(["Tiny", "Small", "Large"]), - "description": "Please select the problem size(s). Tiny: 4 elements, 3 subsets. Small: - 15 elements, 8 subsets. Large: 100 elements, 100 subsets" - } - } + return { + "model_select": { + "values": list(["Tiny", "Small", "Large"]), + "description": "Please select the problem size(s). Tiny: 4 elements, 3 subsets. Small: + 15 elements, 8 subsets. Large: 100 elements, 100 subsets" + } + } """ return { "model_select": { @@ -72,12 +93,12 @@ def get_parameter_options(self): class Config(TypedDict): model_select: str - def generate_problem(self, config: Config) -> Tuple[set, List]: + def generate_problem(self, config: Config) -> tuple[set, list]: """ Generates predefined instances of the SCP. :param config: Config specifying the selected problem instances - :return: the union of all elements of an instance and a set of subsets, each covering a part of the union + :return: The union of all elements of an instance and a set of subsets, each covering a part of the union """ model_select = config['model_select'] self.application = {} @@ -85,7 +106,6 @@ def generate_problem(self, config: Config) -> Tuple[set, List]: if model_select == "Tiny": self.application["elements_to_cover"] = set(range(1, 4)) self.application["subsets"] = [{1, 2}, {1, 3}, {3, 4}] - elif model_select == "Small": self.application["elements_to_cover"] = set(range(1, 15)) self.application["subsets"] = [ @@ -93,7 +113,7 @@ def generate_problem(self, config: Config) -> Tuple[set, List]: {3, 10, 12, 14}, {7, 8, 14, 15}, {1, 2, 6, 11}, {1, 2, 4, 6, 8, 12} ] - else: + elif model_select == "Large": self.application["elements_to_cover"] = set(range(1, 100)) self.application["subsets"] = [] path = os.path.join(os.path.dirname(__file__)) @@ -105,9 +125,12 @@ def generate_problem(self, config: Config) -> Tuple[set, List]: new_set = set(new_set) self.application["subsets"].append(new_set) + else: + raise ValueError(f"Unknown model_select value: {model_select}") + return self.application["elements_to_cover"], self.application["subsets"] - def process_solution(self, solution: List) -> Tuple[List, float]: + def process_solution(self, solution: list) -> tuple[list, float]: """ Returns list of selected subsets and the time it took to process the solution. @@ -118,11 +141,11 @@ def process_solution(self, solution: List) -> Tuple[List, float]: selected_subsets = [list(self.application["subsets"][i]) for i in solution] return selected_subsets, end_time_measurement(start_time) - def validate(self, solution: List) -> Tuple[bool, float]: + def validate(self, solution: list) -> tuple[bool, float]: """ Checks if the elements of the subsets that are part of the solution cover every element of the instance. - :param solution: list containing all subsets that are part of the solution + :param solution: List containing all subsets that are part of the solution :return: Boolean whether the solution is valid and time it took to validate """ start = start_time_measurement() @@ -130,7 +153,7 @@ def validate(self, solution: List) -> Tuple[bool, float]: return covered == self.application["elements_to_cover"], end_time_measurement(start) - def evaluate(self, solution: List) -> Tuple[int, float]: + def evaluate(self, solution: list) -> tuple[int, float]: """ Calculates the number of subsets that are of the solution. diff --git a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py index 164ffca0..d6e0e312 100644 --- a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py +++ b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import TypedDict, Dict, List, Tuple, Set +from typing import TypedDict from qubovert.problems import SetCover from modules.applications.Mapping import Mapping, Core @@ -27,40 +27,37 @@ class QubovertQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ - return [ - {"name": "qubovert", "version": "1.2.5"} - ] + return [{"name": "qubovert", "version": "1.2.5"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return { - "penalty_weight": { - "values": [2, 5, 10, 25, 50, 100], - "custom_input": True, - "custom_range": True, - "postproc": float, - "description": "Please choose the weight of the penalties in the QUBO representation of - the problem" - } - } + Returns the configurable settings for this mapping. + :return: Dictionary containing configurable settings + .. code-block:: python + + return { + "penalty_weight": { + "values": [2, 5, 10, 25, 50, 100], + "custom_input": True, + "custom_range": True, + "postproc": float, + "description": "Please choose the weight of the penalties in the QUBO representation of + the problem" + } + } """ return { "penalty_weight": { @@ -74,7 +71,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -83,14 +80,14 @@ class Config(TypedDict): """ penalty_weight: float - def map(self, problem: Tuple, config: Config) -> Tuple[Dict, float]: + def map(self, problem: tuple, config: Config) -> tuple[dict, float]: """ Maps the SCP to a QUBO matrix. - :param problem: tuple containing the set of all elements of an instance and a list of subsets each covering some - of these elements - :param config: config with the parameters specified in Config class - :return: dict with QUBO matrix, time it took to map it + :param problem: Tuple containing the set of all elements of an instance and a list of subsets, + each covering some of these elements + :param config: Config with the parameters specified in Config class + :return: Dict with QUBO matrix, time it took to map it """ start = start_time_measurement() penalty_weight = config['penalty_weight'] @@ -106,13 +103,13 @@ def map(self, problem: Tuple, config: Config) -> Tuple[Dict, float]: q_dict = {} for key, val in self.SCP_qubo.items(): - # interaction (quadratic) terms + # Interaction (quadratic) terms if len(key) == 2: if (key[0], key[1]) not in q_dict: q_dict[(key[0], key[1])] = float(val) else: q_dict[(key[0], key[1])] += float(val) - # local (linear) fields + # Local (linear) fields elif len(key) == 1: if (key[0], key[0]) not in q_dict: q_dict[(key[0], key[0])] = float(val) @@ -121,12 +118,12 @@ def map(self, problem: Tuple, config: Config) -> Tuple[Dict, float]: return {"Q": q_dict}, end_time_measurement(start) - def reverse_map(self, solution: Dict) -> Tuple[set, float]: + def reverse_map(self, solution: dict) -> tuple[set, float]: """ Maps the solution of the QUBO to a set of subsets included in the solution. :param solution: QUBO matrix in dict form - :return: tuple with set of subsets that are part of the solution and the time it took to map it + :return: Tuple with set of subsets that are part of the solution and the time it took to map it """ start = start_time_measurement() sol = self.SCP_problem.convert_solution(solution) diff --git a/src/modules/applications/optimization/TSP/TSP.py b/src/modules/applications/optimization/TSP/TSP.py index d1b62052..f104a26f 100644 --- a/src/modules/applications/optimization/TSP/TSP.py +++ b/src/modules/applications/optimization/TSP/TSP.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import pickle import logging import os @@ -43,17 +43,19 @@ class TSP(Optimization): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("TSP") - self.submodule_options = ["Ising", "QUBO", "GreedyClassicalTSP", "ReverseGreedyClassicalTSP", "RandomTSP"] + self.submodule_options = [ + "Ising", "QUBO", "GreedyClassicalTSP", "ReverseGreedyClassicalTSP", "RandomTSP" + ] @staticmethod - def get_requirements() -> List: + def get_requirements() -> list: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "networkx", "version": "3.2.1"}, @@ -93,22 +95,21 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Mapping Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this application :return: Dictionary with configurable settings. - .. code-block:: python - - return { - "nodes": { - "values": list([3, 4, 6, 8, 10, 14, 16]), - "allow_ranges": True, - "description": "How many nodes does your graph need?", - "postproc": int - } - } + .. code-block:: python + return { + "nodes": { + "values": list([3, 4, 6, 8, 10, 14, 16]), + "allow_ranges": True, + "description": "How many nodes does your graph need?", + "postproc": int + } + } """ return { "nodes": { @@ -121,7 +122,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -166,7 +167,6 @@ def generate_problem(self, config: Config) -> nx.Graph: graph = pickle.load(file) # Remove seams until the target number of seams is reached - # Get number of seam in graph nodes_in_graph = list(graph.nodes) nodes_in_graph.sort() @@ -184,7 +184,7 @@ def generate_problem(self, config: Config) -> nx.Graph: logging.error("Graph is not connected!") raise ValueError("Graph is not connected!") - # normalize graph + # Normalize graph cost_matrix = self._get_tsp_matrix(graph) graph = nx.from_numpy_array(cost_matrix) @@ -192,12 +192,12 @@ def generate_problem(self, config: Config) -> nx.Graph: return graph - def process_solution(self, solution: Dict) -> Tuple[List, float]: + def process_solution(self, solution: dict) -> tuple[list, float]: """ Convert dict to list of visited nodes. :param solution: Dictionary with solution - :return: processed solution and the time it took to process it + :return: Processed solution and the time it took to process it """ start_time = start_time_measurement() nodes = self.application.nodes() @@ -254,11 +254,11 @@ def process_solution(self, solution: Dict) -> Tuple[List, float]: return route, end_time_measurement(start_time) - def validate(self, solution: List) -> Tuple[bool, float]: + def validate(self, solution: list) -> tuple[bool, float]: """ Checks if it is a valid TSP tour. - :param solution: list containing the nodes of the solution + :param solution: List containing the nodes of the solution :return: Boolean whether the solution is valid, time it took to validate """ start = start_time_measurement() @@ -273,7 +273,7 @@ def validate(self, solution: List) -> Tuple[bool, float]: logging.error(f"{len([node for node in list(nodes) if node not in solution])} nodes were NOT visited") return False, end_time_measurement(start) - def evaluate(self, solution: List) -> Tuple[float, float]: + def evaluate(self, solution: list) -> tuple[float, float]: """ Find distance for given route e.g. [0, 4, 3, 1, 2] and original data. @@ -281,7 +281,7 @@ def evaluate(self, solution: List) -> Tuple[float, float]: :return: Tour cost and the time it took to calculate it """ start = start_time_measurement() - # get the total distance without return + # Get the total distance without return total_dist = 0 for idx, _ in enumerate(solution[:-1]): dist = self.application[solution[idx + 1]][solution[idx]] @@ -289,10 +289,10 @@ def evaluate(self, solution: List) -> Tuple[float, float]: logging.info(f"Total distance (without return): {total_dist}") - # add distance between start and end point to complete cycle + # Add distance between start and end point to complete cycle return_distance = self.application[solution[0]][solution[-1]]['weight'] - # get distance for full cycle + # Get distance for full cycle distance_with_return = total_dist + return_distance logging.info(f"Total distance (including return): {distance_with_return}") diff --git a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py index ca3d442f..9049dae8 100644 --- a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py @@ -18,20 +18,29 @@ # Source http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/tsp/ filename = "dsj1000.tsp" -print(f"Loading {filename}") -# Load the problem from .tsp file -problem = tsplib95.load(filename) -graph = problem.get_graph() - -# We don't needed edges from e.g. node0 -> node0 -for edge in graph.edges: - if edge[0] == edge[1]: - graph.remove_edge(edge[0], edge[1]) - -print("Loaded graph:") -print(nx.info(graph)) - -with open("reference_graph.gpickle", "wb") as file: - pickle.dump(graph, file, pickle.HIGHEST_PROTOCOL) - -print("Saved graph as reference_graph.gpickle") + +def main(): + """ + Load a TSP problem, remove unnecessary edges, and save the reference graph. + """ + print(f"Loading {filename}") + + # Load the problem from .tsp file + problem = tsplib95.load(filename) + graph = problem.get_graph() + + # We don't needed edges from e.g. node0 -> node0 + for edge in graph.edges: + if edge[0] == edge[1]: + graph.remove_edge(edge[0], edge[1]) + + print("Loaded graph:") + print(nx.info(graph)) + + with open("reference_graph.gpickle", "wb") as file: + pickle.dump(graph, file, pickle.HIGHEST_PROTOCOL) + + print("Saved graph as reference_graph.gpickle") + +if __name__ == '__main__': + main() diff --git a/src/modules/applications/optimization/TSP/mappings/ISING.py b/src/modules/applications/optimization/TSP/mappings/ISING.py index d90e4824..29760960 100644 --- a/src/modules/applications/optimization/TSP/mappings/ISING.py +++ b/src/modules/applications/optimization/TSP/mappings/ISING.py @@ -13,7 +13,7 @@ # limitations under the License. import re -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import logging import networkx as nx @@ -23,7 +23,6 @@ from pyqubo import Array, Placeholder, Constraint from qiskit_optimization.applications import Tsp from qiskit_optimization.converters import QuadraticProgramToQubo -from qiskit.quantum_info import SparsePauliOp from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.TSP.mappings.QUBO import QUBO @@ -37,7 +36,7 @@ class Ising(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA", "QiskitQAOA"] @@ -46,11 +45,11 @@ def __init__(self): self.config = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "networkx", "version": "3.2.1"}, @@ -62,24 +61,23 @@ def get_requirements() -> List[Dict]: *QUBO.get_requirements() ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" - }, - "mapping": { - "values": ["ocean", "qiskit", "pyqubo"], - "description": "Which Ising formulation of the TSP problem should be used?" - } - } + Returns the configurable settings for this mapping. + :return: Dictionary containing parameter options. + .. code-block:: python + + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your lagrange?" + }, + "mapping": { + "values": ["ocean", "qiskit", "pyqubo"], + "description": "Which Ising formulation of the TSP problem should be used?" + } + } """ return { "lagrange_factor": { @@ -94,7 +92,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -105,17 +103,17 @@ class Config(TypedDict): lagrange_factor: float mapping: str - def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to an Ising formulation. - :param problem: networkx graph - :param config: config with the parameters specified in Config class - :return: dict with Ising, time it took to map it + :param problem: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with Ising, time it took to map it """ self.graph = problem self.config = config - # call mapping function defined in configuration + # Call mapping function defined in configuration mapping = self.config["mapping"] if mapping == "ocean": return self._map_ocean(problem, config) @@ -128,12 +126,12 @@ def map(self, problem: nx.Graph, config: Config) -> Tuple[Dict, float]: raise ValueError(f"Unknown mapping {mapping}.") @staticmethod - def _create_pyqubo_model(cost_matrix: List) -> Any: + def _create_pyqubo_model(cost_matrix: list) -> any: """ This PyQubo formulation of the TSP was kindly provided by AWS. - :param cost_matrix: cost matrix of the TSP - :return: compiled PyQubo model + :param cost_matrix: Cost matrix of the TSP + :return: Compiled PyQubo model """ n = len(cost_matrix) x = Array.create('c', (n, n), 'BINARY') @@ -167,7 +165,7 @@ def _create_pyqubo_model(cost_matrix: List) -> Any: return model @staticmethod - def _get_matrix_index(ising_index_string: Any, number_nodes: Any) -> Any: + def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: """ Converts dictionary index (e.g. 'c[0][2]') in PyQubo to matrix index. @@ -192,13 +190,13 @@ def _get_matrix_index(ising_index_string: Any, number_nodes: Any) -> Any: return idx - def _map_pyqubo(self, graph: nx.Graph, config: Config) -> Tuple[Dict, float]: + def _map_pyqubo(self, graph: nx.Graph, config: Config) -> tuple[dict, float]: """ Use Qubo / Ising model defined in PyQubo. - :param graph: networkx graph - :param config: config with the parameters specified in Config class - :return: dict with the Ising, time it took to map it + :param graph: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ start = start_time_measurement() cost_matrix = np.array(nx.to_numpy_array(graph, weight="weight")) @@ -227,16 +225,15 @@ def _map_pyqubo(self, graph: nx.Graph, config: Config) -> Tuple[Dict, float]: return {"J": j_matrix, "J_dict": quad, "t_dict": linear, "t": t_matrix}, end_time_measurement(start) - def _map_ocean(self, graph: nx.Graph, config: Config) -> Tuple[Dict, float]: + def _map_ocean(self, graph: nx.Graph, config: Config) -> tuple[dict, float]: """ - Use D-Wave/Ocean TSP QUBO/Ising model: + Use D-Wave/Ocean TSP QUBO/Ising model. https://docs.ocean.dwavesys.com/en/stable/docs_dnx/reference/algorithms/generated/dwave_networkx.algorithms.tsp.traveling_salesperson_qubo.html#dwave_networkx.algorithms.tsp.traveling_salesperson_qubo - :param graph: networkx graph - :param config: config with the parameters specified in Config class - :return: dict with the Ising, time it took to map it + :param graph: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ - start = start_time_measurement() qubo_mapping = QUBO() q, _ = qubo_mapping.map(graph, config) @@ -261,21 +258,18 @@ def _map_ocean(self, graph: nx.Graph, config: Config) -> Tuple[Dict, float]: v = self.key_mapping[key[1]] j_matrix[u][v] = value - logging.info(j_matrix) - logging.info(j_matrix.shape) - return {"J": j_matrix, "t": np.array(list(t.values())), "J_dict": j}, end_time_measurement(start) @staticmethod - def _map_qiskit(graph: nx.Graph, config: Config) -> Tuple[Dict, float]: + def _map_qiskit(graph: nx.Graph, config: Config) -> tuple[dict, float]: """ Use Ising Mapping of Qiskit Optimize: TSP class: https://qiskit.org/documentation/optimization/stubs/qiskit_optimization.applications.Tsp.html Example notebook: https://qiskit.org/documentation/tutorials/optimization/6_examples_max_cut_and_tsp.html - :param graph: networkx graph - :param config: config with the parameters specified in Config class - :return: dict with the Ising, time it took to map it + :param graph: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ start = start_time_measurement() tsp = Tsp(graph) @@ -285,29 +279,27 @@ def _map_qiskit(graph: nx.Graph, config: Config) -> Tuple[Dict, float]: qubo = qp2qubo.convert(qp) qubitOp, _ = qubo.to_ising() - # reverse generate J and t out of qubit PauliSumOperator from qiskit + # Reverse generate J and t out of qubit PauliSumOperator from qiskit t_matrix = np.zeros(qubitOp.num_qubits, dtype=complex) j_matrix = np.zeros((qubitOp.num_qubits, qubitOp.num_qubits), dtype=complex) pauli_list = qubitOp.to_list() for pauli_str, coeff in pauli_list: - logging.info((pauli_str, coeff)) pauli_str_list = list(pauli_str) index_pos_list = list(locate(pauli_str_list, lambda a: a == 'Z')) if len(index_pos_list) == 1: - # update t t_matrix[index_pos_list[0]] = coeff elif len(index_pos_list) == 2: j_matrix[index_pos_list[0]][index_pos_list[1]] = coeff return {"J": j_matrix, "t": t_matrix}, end_time_measurement(start) - def reverse_map(self, solution: Any) -> Tuple[Dict, float]: + def reverse_map(self, solution: any) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the TSP class for validation/evaluation. - :param solution: list or array containing the solution - :return: solution mapped accordingly, time it took to map it + :param solution: List or array containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() if -1 in solution: # ising model output from Braket QAOA @@ -335,7 +327,7 @@ def reverse_map(self, solution: Any) -> Tuple[Dict, float]: return result, end_time_measurement(start) @staticmethod - def _flip_bits_in_bitstring(solution: Any) -> Any: + def _flip_bits_in_bitstring(solution: any) -> any: """ Flip bits in the solution bitstring to unify different mappings. @@ -350,7 +342,7 @@ def _flip_bits_in_bitstring(solution: Any) -> Any: return solution @staticmethod - def _convert_ising_to_qubo(solution: Any) -> Any: + def _convert_ising_to_qubo(solution: any) -> any: """ Convert Ising model output to QUBO format. diff --git a/src/modules/applications/optimization/TSP/mappings/QUBO.py b/src/modules/applications/optimization/TSP/mappings/QUBO.py index c531800d..8215ba94 100644 --- a/src/modules/applications/optimization/TSP/mappings/QUBO.py +++ b/src/modules/applications/optimization/TSP/mappings/QUBO.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Tuple +from typing import TypedDict import logging import dwave_networkx as dnx @@ -25,45 +25,43 @@ class QUBO(Mapping): """ QUBO formulation for the TSP. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "networkx", "version": "3.2.1"}, {"name": "dwave_networkx", "version": "0.8.15"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. :return: Dictionary with configurable settings - .. code-block:: python - - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your " - "lagrange?", - "custom_input": True, - "postproc": float - } - } + .. code-block:: python + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your " + "lagrange?", + "custom_input": True, + "postproc": float + } + } """ return { "lagrange_factor": { @@ -77,7 +75,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -86,13 +84,13 @@ class Config(TypedDict): """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> Tuple[Dict, float]: + def map(self, problem: networkx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to a QUBO formulation. - :param problem: networkx graph - :param config: config with the parameters specified in Config class - :return: dict with QUBO, time it took to map it + :param problem: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with QUBO, time it took to map it """ start = start_time_measurement() lagrange = None diff --git a/src/modules/training/Inference.py b/src/modules/training/Inference.py index 22f6fa00..e4a934bc 100644 --- a/src/modules/training/Inference.py +++ b/src/modules/training/Inference.py @@ -13,7 +13,8 @@ # limitations under the License. from typing import TypedDict -from modules.training.Training import * +import numpy as np +from modules.training.Training import Training, Core class Inference(Training): @@ -23,7 +24,7 @@ class Inference(Training): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("Inference") @@ -33,33 +34,28 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module + :return: list of dict with requirements of this module. :rtype: list[dict] """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this circuit - - :return: - .. code-block:: python - - return { - "pretrained": { - "values": [False], - "custom_input": True, - "postproc": str, - "description": "Please provide the parameters of a pretrained model." - } - } + Returns the configurable settings for this circuit. + + :return: Configuration settings for the pretrained model. + .. code-block:: python + + return { + "pretrained": { + "values": [False], + "custom_input": True, + "postproc": str, + "description": "Please provide the parameters of a pretrained model." + } + } """ return { "pretrained": { @@ -86,16 +82,12 @@ def get_default_submodule(self, option: str) -> Core: def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> dict: """ - Method that uses a pretrained model for inference + Method that uses a pretrained model for inference. :param input_data: Dictionary with information needed for inference - :type input_data: dict :param config: Inference settings - :type config: Config :param kwargs: Optional additional arguments - :type kwargs: dict :return: Dictionary including the information of previous modules as well as of this module - :rtype: dict """ self.n_states_range = range(2 ** input_data['n_qubits']) self.target = np.asarray(input_data["histogram_train"]) @@ -105,9 +97,11 @@ def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> di pmfs, samples = execute_circuit([parameters.get() if GPU else parameters]) pmfs = np.asarray(pmfs) - samples = self.sample_from_pmf( - pmf=pmfs[0], - n_shots=input_data["n_shots"]) if samples is None else samples[0] + samples = ( + self.sample_from_pmf(pmf=pmfs[0], n_shots=input_data["n_shots"]) + if samples is None + else samples[0] + ) loss = self.kl_divergence(pmfs.reshape([-1, 1]), self.target) diff --git a/src/modules/training/QCBM.py b/src/modules/training/QCBM.py index d8bc49a7..46cb7f77 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/training/QCBM.py @@ -14,12 +14,13 @@ from typing import TypedDict import logging +import numpy as np from cma import CMAEvolutionStrategy from tensorboardX import SummaryWriter from matplotlib import figure, axes import matplotlib.pyplot as plt -from modules.training.Training import * +from modules.training.Training import Training, Core from utils_mpi import is_running_mpi, get_comm MPI = is_running_mpi() @@ -34,12 +35,12 @@ class QCBM(Training): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("QCBM") self.n_states_range: list - self.target: np.array + self.target: np.ndarray self.study_generalization: bool self.generalization_metrics: dict self.writer: SummaryWriter @@ -50,70 +51,54 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } + {"name": "numpy", "version": "1.26.4"}, + {"name": "cma", "version": "4.0.0"}, + {"name": "matplotlib", "version": "3.7.5"}, + {"name": "tensorboard", "version": "2.17.0"}, + {"name": "tensorboardX", "version": "2.6.2.2"} ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for the quantum circuit born machine - :return: - .. code-block:: python - - return { - - "population_size": { - "values": [5, 10, 100, 200, 10000], - "description": "What population size do you want?" - }, - - "max_evaluations": { - "values": [100, 1000, 20000, 100000], - "description": "What should be the maximum number of evaluations?" - }, - - "sigma": { - "values": [0.01, 0.5, 1, 2], - "description": "Which sigma would you like to use?" - }, - - "pretrained": { - "values": [False], - "custom_input": True, - "postproc": str, - "description": "Please provide the parameters of a pretrained model." - }, - - "loss": { - "values": ["KL", "NLL"], - "description": "Which loss function do you want to use?" - } + :return: Configuration settings for QCBM + .. code-block:: python + + return { + + "population_size": { + "values": [5, 10, 100, 200, 10000], + "description": "What population size do you want?" + }, + + "max_evaluations": { + "values": [100, 1000, 20000, 100000], + "description": "What should be the maximum number of evaluations?" + }, + + "sigma": { + "values": [0.01, 0.5, 1, 2], + "description": "Which sigma would you like to use?" + }, + + "pretrained": { + "values": [False], + "custom_input": True, + "postproc": str, + "description": "Please provide the parameters of a pretrained model." + }, + + "loss": { + "values": ["KL", "NLL"], + "description": "Which loss function do you want to use?" } + } """ return { "population_size": { @@ -146,7 +131,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -170,17 +155,16 @@ def setup_training(self, input_data: dict, config: Config) -> tuple[float, dict] """ Method to configure the training setup including CMA-ES and tensorboard. - :param input_data: a representation of the quantum machine learning model that will be trained - :type input_data: dict + :param input_data: A representation of the quantum machine learning model that will be trained :param config: Config specifying the parameters of the training - :type config: dict - :return: random initial parameter and options for CMA-ES - :rtype: tuple[float, dict] + :return: Random initial parameter and options for CMA-ES """ logging.info( - f"Running config: [backend={input_data['backend']}] [n_qubits={input_data['n_qubits']}] "\ - f"[population_size={config['population_size']}]") + f"Running config: [backend={input_data['backend']}] " + f"[n_qubits={input_data['n_qubits']}] " + f"[population_size={config['population_size']}]" + ) self.study_generalization = "generalization_metrics" in list(input_data.keys()) if self.study_generalization: @@ -220,33 +204,33 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict This function finds the best parameters of the circuit on a transformed problem instance and returns a solution. :param input_data: A representation of the quantum machine learning model that will be trained - :type input_data: dict :param config: Config specifying the parameters of the training - :type config: dict - :param kwargs: optional additional settings - :type kwargs: dict + :param kwargs: Optional additional settings :return: Dictionary including the information of previous modules as well as of the training - :rtype: dict """ size = None input_data['MPI_size'] = size input_data["store_dir_iter"] += f"_{input_data['dataset_name']}_qubits{input_data['n_qubits']}" x0, options = self.setup_training(input_data, config) - - if comm.Get_rank() == 0: + + is_master = comm.Get_rank() == 0 + if is_master: self.target = np.asarray(input_data["histogram_train"]) self.target[self.target == 0] = 1e-8 + self.n_states_range = range(2 ** input_data['n_qubits']) execute_circuit = input_data["execute_circuit"] timing = self.Timing() es = CMAEvolutionStrategy(x0.get() if GPU else x0, config['sigma'], options) + for parameter in ["best_parameters", "time_circuit", "time_loss", "KL", "best_sample"]: input_data[parameter] = [] best_loss = float("inf") self.fig, self.ax = plt.subplots() + while not es.stop(): solutions = es.ask() epoch = es.result[4] @@ -258,10 +242,11 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict time_circ = timing.stop_recording() timing.start_recording() - if comm.Get_rank() == 0: + if is_master: loss_epoch = self.loss_func(pmfs_model.reshape([config['population_size'], -1]), self.target) else: loss_epoch = np.empty(config["population_size"]) + comm.Bcast(loss_epoch, root=0) comm.Barrier() @@ -277,11 +262,12 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict input_data["KL"].append(float(es.result[1])) logging.info( - f"[Iteration {es.result[4]}] " - f"[{config['loss']}: {es.result[1]:.5f}] "\ - f"[Circuit processing: {(time_circ):.3f} ms] "\ - f"[{config['loss']} processing: {(time_loss):.3f} ms] "\ - f"[sigma: {sigma:.5f}]") + f"[Iteration {epoch}] " + f"[{config['loss']}: {es.result[1]:.5f}] " + f"[Circuit processing: {(time_circ):.3f} ms] " + f"[{config['loss']} processing: {(time_loss):.3f} ms] " + f"[sigma: {sigma:.5f}]" + ) plt.close() self.writer.flush() @@ -295,10 +281,19 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict return input_data def data_visualization(self, loss_epoch, pmfs_model, samples, epoch): + """ + Visualizes the data and metrics for training. + + :param loss_epoch: Loss for the current epoch + :param pmfs_model: The probability mass functions from the model + :param samples: The samples from the model + :param epoch: The current epoch number + :return: Best probability mass function for visualization + """ index = loss_epoch.argmin() best_pmf = pmfs_model[index] / pmfs_model[index].sum() - if self.study_generalization: + if self.study_generalization: if samples is None: counts = self.sample_from_pmf( pmf=best_pmf.get() if GPU else best_pmf, @@ -307,23 +302,24 @@ def data_visualization(self, loss_epoch, pmfs_model, samples, epoch): counts = samples[int(index)] metrics = self.generalization_metrics.get_metrics(counts if GPU else counts) - for (key, value) in metrics.items(): + for key, value in metrics.items(): self.writer.add_scalar(f"metrics/{key}", value, epoch) nll = self.nll(best_pmf.reshape([1, -1]), self.target) kl = self.kl_divergence(best_pmf.reshape([1, -1]), self.target) mmd = self.mmd(best_pmf.reshape([1, -1]), self.target) + self.writer.add_scalar("metrics/NLL", nll.get() if GPU else nll, epoch) self.writer.add_scalar("metrics/KL", kl.get() if GPU else kl, epoch) self.writer.add_scalar("metrics/MMD", mmd.get() if GPU else mmd, epoch) self.ax.clear() self.ax.imshow( - best_pmf.reshape(int(np.sqrt(best_pmf.size)), int(np.sqrt(best_pmf.size))).get() if GPU - else best_pmf.reshape(int(np.sqrt(best_pmf.size)), - int(np.sqrt(best_pmf.size))), + best_pmf.reshape(int(np.sqrt(best_pmf.size)), int(np.sqrt(best_pmf.size))).get() + if GPU else best_pmf.reshape(int(np.sqrt(best_pmf.size)), int(np.sqrt(best_pmf.size))), cmap='binary', - interpolation='none') + interpolation='none' + ) self.ax.set_title(f'Iteration {epoch}') self.writer.add_figure('grid_figure', self.fig, global_step=epoch) diff --git a/src/modules/training/QGAN.py b/src/modules/training/QGAN.py index bb6161a1..543eb19a 100644 --- a/src/modules/training/QGAN.py +++ b/src/modules/training/QGAN.py @@ -23,13 +23,14 @@ import numpy as np import matplotlib.pyplot as plt -from modules.training.Training import * -from modules.applications.QML.generative_modeling.transformations.Transformation import * - +from modules.training.Training import Training, Core +from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation from utils_mpi import is_running_mpi, get_comm + MPI = is_running_mpi() comm = get_comm() + class QGAN(Training): # pylint: disable=R0902 """ Class for QGAN @@ -37,7 +38,7 @@ class QGAN(Training): # pylint: disable=R0902 def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("QGAN") @@ -79,69 +80,53 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "numpy", - "version": "1.26.4" + {"name": "numpy", "version": "1.26.4"}, + {"name": "torch", "version": "2.2.0"}, + {"name": "matplotlib", "version": "3.7.5"}, + {"name": "tensorboard", "version": "2.17.0"}, + {"name": "tensorboardX", "version": "2.6.2.2"} + ] + + def get_parameter_options(self) -> dict: + """ + Returns the configurable settings for this circuit. + + :return: Configuration settings for QGAN + .. code-block:: python + return { + "epochs": { + "values": [2, 100, 200, 10000], + "description": "How many epochs do you want?" }, - { - "name": "torch", - "version": "2.2.0" + "batch_size": { + "values": [10, 20, 100, 2000], + "description": "What batch size do you want?" + }, + "learning_rate_generator": { + "values": [0.1, 0.2], + "description": "What learning rate do you want to set for the generator?" }, - { - "name": "matplotlib", - "version": "3.7.5" + "learning_rate_discriminator": { + "values": [0.1, 0.05], + "description": "What learning rate do you want to set for the discriminator?" }, - { - "name": "tensorboard", - "version": "2.17.0" + "device": { + "values": ["cpu", "gpu"], + "description": "Where do you want to run the discriminator?" }, - { - "name": "tensorboardX", - "version": "2.6.2.2" + "pretrained": { + "values": [True, False], + "description": "Do you want to use parameters of a pretrained model?" + }, + "loss": { + "values": ["KL", "NLL"], + "description": "Which loss function do you want to use?" } - ] - - def get_parameter_options(self) -> dict: - """ - Returns the configurable settings for this circuit - - :return: - .. code-block:: python - return { - "epochs": { - "values": [2, 100, 200, 10000], - "description": "How many epochs do you want?" - }, - "batch_size": { - "values": [10, 20, 100, 2000], - "description": "What batch size do you want?" - }, - "learning_rate_generator": { - "values": [0.1, 0.2], - "description": "What learning rate do you want to set for the generator?" - }, - "learning_rate_discriminator": { - "values": [0.1, 0.05], - "description": "What learning rate do you want to set for the discriminator?" - }, - "device": { - "values": ["cpu", "gpu"], - "description": "Where do you want to run the discriminator?" - }, - "pretrained": { - "values": [True, False], - "description": "Do you want to use parameters of a pretrained model?" - }, - "loss": { - "values": ["KL", "NLL"], - "description": "Which loss function do you want to use?" - } } """ return { @@ -203,12 +188,11 @@ def get_default_submodule(self, option: str) -> Core: def setup_training(self, input_data: dict, config: dict) -> None: """ + Sets up the training configuration. + :param input_data: dictionary with the variables from the circuit needed to start the training - :type input_data: dict - :param config: - :type config: dict + :param config: Configurations for the QGAN training. """ - self.beta_1 = 0.5 self.real_label = 1. self.fake_label = 0. @@ -235,6 +219,7 @@ def setup_training(self, input_data: dict, config: dict) -> None: if input_data["dataset_name"] == "Cardinality_Constraint": new_size = 1000 self.bins_train = np.repeat(self.bins_train,new_size,axis=0) + self.study_generalization = "generalization_metrics" in list(input_data.keys()) if self.study_generalization: self.generalization_metrics = input_data["generalization_metrics"] @@ -248,15 +233,15 @@ def setup_training(self, input_data: dict, config: dict) -> None: self.discriminator.apply(Discriminator.weights_init) self.params = np.random.rand(self.n_params) * np.pi - self.generator = QuantumGenerator(self.n_qubits, self.execute_circuit, self.batch_size) - self.accuracy = [] + self.accuracy = [] self.criterion = torch.nn.BCELoss() self.optimizer_discriminator = torch.optim.Adam( self.discriminator.parameters(), lr=config["learning_rate_discriminator"], - betas=(self.beta_1, 0.999)) + betas=(self.beta_1, 0.999) + ) self.real_labels = torch.full((self.batch_size,), 1.0, dtype=torch.float, device=self.device) self.fake_labels = torch.full((self.batch_size,), 0.0, dtype=torch.float, device=self.device) @@ -272,16 +257,12 @@ def setup_training(self, input_data: dict, config: dict) -> None: def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict: # pylint: disable=R0915 """ - This function starts the training of the QGAN + This function starts the training of the QGAN. - :param input_data: dictionary with the variables from the circuit needed to start the training - :type input_data: dict - :param config: annealing settings - :type config: dict - :param kwargs: optional additional arguments - :type kwargs: dict - :return: dictionary including the solution - :rtype: dict + :param input_data: Dictionary with the variables from the circuit needed to start the training + :param config: Annealing settings + :param kwargs: Optional additional arguments + :return: Dictionary including the solution """ self.setup_training(input_data, config) generator_losses = [] @@ -297,7 +278,6 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict for batch, data in enumerate(self.dataloader): # Training the discriminator # Data from real distribution for training the discriminator - real_data = data.float().to(self.device) self.discriminator.zero_grad() outD_real = self.discriminator(real_data).view(-1) @@ -307,7 +287,6 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict # Use Quantum Variational Circuit to generate fake samples fake_data, _ = self.generator.execute(self.params, self.batch_size) fake_data = fake_data.float().to(self.device) - outD_fake = self.discriminator(fake_data).view(-1) errD_fake = self.criterion(outD_fake, self.fake_labels) errD_fake.backward() @@ -323,13 +302,13 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict self.discriminator, self.criterion, self.real_labels, - self.device) + self.device + ) updated_params = self.params - self.learning_rate_generator * gradients self.params = updated_params self.discriminator_weights = self.discriminator.state_dict() - generator_losses.append(errG.item()) discriminator_losses.append(errD.item()) @@ -399,7 +378,6 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict input_data["best_parameter"] = best_generator_params input_data["best_sample"] = best_sample - input_data["KL"] = self.accuracy input_data["generator_loss"] = generator_losses input_data["discriminator_loss"] = discriminator_losses @@ -409,8 +387,9 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict class Discriminator(nn.Module): """ - This class defines the discriminator of the QGAN + This class defines the discriminator of the QGAN. """ + def __init__(self, input_length: int): super().__init__() self.dense1 = nn.Linear(int(input_length), 2 * int(input_length)) @@ -418,7 +397,7 @@ def __init__(self, input_length: int): def forward(self, x: torch.Tensor) -> float: """ - This function initialized the weight tensor of the linear + InitializeS the weight tensor of the linear layers with values using a Xavier uniform distribution. :param x: Input of the discriminator @@ -433,14 +412,11 @@ def forward(self, x: torch.Tensor) -> float: @staticmethod def weights_init(m: nn.Linear) -> None: """ - This function initialized the weight tensor of the linear + Initializes the weight tensor of the linear layers with values using a Xavier uniform distribution. :param m: Neural network layer - :type m: nn.Linear """ - print(type(m)) - print(m) if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight.data, gain=10) nn.init.constant_(m.bias.data, 1) @@ -448,8 +424,9 @@ def weights_init(m: nn.Linear) -> None: class QuantumGenerator: """ - This class defines the generator of the QGAN + This class defines the generator of the QGAN. """ + def __init__(self, n_qubits, execute_circuit, batch_size): self.n_qubits = n_qubits self.execute_circuit = execute_circuit @@ -457,14 +434,11 @@ def __init__(self, n_qubits, execute_circuit, batch_size): def execute(self, params: np.ndarray, n_shots: int) -> tuple[torch.Tensor, np.ndarray]: """ - This function defines the forward pass of the generator + Forward pass of the generator. :param params: Parameters of the quantum circuit - :type params: np.ndarray :param n_shots: Number of shots - :type n_shots: int :return: samples and the probability distribution generated by the quantum circuit - :rtype: tuple[torch.Tensor, np.ndarray] """ # Call the quantum circuit and obtain probability distributions @@ -487,20 +461,14 @@ def execute(self, params: np.ndarray, n_shots: int) -> tuple[torch.Tensor, np.nd def compute_gradient(self, params: np.ndarray, discriminator: torch.nn.Module, criterion: callable, label: torch.Tensor, device: str) -> np.ndarray: """ - This function defines the forward pass of the generator + This function defines the forward pass of the generator. :param params: Parameters of the quantum circuit - :type params: np.ndarray :param discriminator: Discriminator of the QGAN - :type discriminator: torch.nn.Module :param criterion: Loss function - :type criterion: callable :param label: Label indicating of sample is true or fake - :type label: torch.Tensor :param device: torch device (e.g. CPU or CUDA) - :type device: str :return: samples and the probability distribution generated by the quantum circuit - :rtype: np.ndarray """ shift = 0.5 * np.pi gradients = np.zeros(len(params)) # Initialize gradients as an array of zeros diff --git a/src/modules/training/Training.py b/src/modules/training/Training.py index e483892b..2d9dbe54 100644 --- a/src/modules/training/Training.py +++ b/src/modules/training/Training.py @@ -31,12 +31,14 @@ class Training(Core, ABC): """ - The Training module is the base class fot both finding (QCBM) and executing trained models (Inference) + The Training module is the base class fot both finding (QCBM) and executing trained models (Inference). """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. + + :param name: Name of the training instance """ self.name = name super().__init__() @@ -45,29 +47,20 @@ def __init__(self, name): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def postprocess(self, input_data: dict, config: dict, **kwargs): """ - Here, the actual training of the machine learning model is done + Perform the actual training of the machine learning model. :param input_data: Collected information of the benchmarking process - :type input_data: dict :param config: Training settings - :type config: dict :param kwargs: Optional additional arguments - :type kwargs: dict - :return: + :return: Training results and the postprocessing time. :rtype: """ start = start_time_measurement() @@ -89,26 +82,19 @@ def start_training(self, input_data: dict, config: any, **kwargs: dict) -> dict: This function starts the training of QML model or deploys a pretrained model. :param input_data: A representation of the quantum machine learning model that will be trained - :type input_data: dict :param config: Config specifying the parameters of the training (dict-like Config type defined in children) - :type config: any :param kwargs: optional additional settings - :type kwargs: dict :return: Solution, the time it took to compute it and some optional additional information - :rtype: dict """ pass def sample_from_pmf(self, pmf: np.ndarray, n_shots: int) -> np.ndarray: """ - Function to sample from the probability mass function generated by the quantum circuit + Function to sample from the probability mass function generated by the quantum circuit. :param pmf: Probability mass function generated by the quantum circuit - :type pmf: np.ndarray :param n_shots: Number of shots - :type n_shots: int :return: number of counts in the 2**n_qubits bins - :rtype: np.ndarray """ samples = np.random.choice(self.n_states_range, size=n_shots, p=pmf) counts = np.bincount(samples, minlength=len(self.n_states_range)) @@ -116,42 +102,33 @@ def sample_from_pmf(self, pmf: np.ndarray, n_shots: int) -> np.ndarray: def kl_divergence(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: """ - Kullback-Leibler divergence, that is used as a loss function + Kullback-Leibler divergence, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit - :type pmf_model: np.ndarray :param pmf_target: Probability mass function of the target distribution - :type pmf_target: np.ndarray :return: Kullback-Leibler divergence - :rtype: float """ pmf_model[pmf_model == 0] = 1e-8 return np.sum(pmf_target * np.log(pmf_target / pmf_model), axis=1) def nll(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: """ - Negative log likelihood, that is used as a loss function + Negative log likelihood, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit - :type pmf_model: np.ndarray :param pmf_target: Probability mass function of the target distribution - :type pmf_target: np.ndarray :return: Negative log likelihood - :rtype: float """ pmf_model[pmf_model == 0] = 1e-8 return -np.sum(pmf_target * np.log(pmf_model), axis=1) def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: """ - Maximum mean discrepancy, that is used as a loss function + Maximum mean discrepancy, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit - :type pmf_model: np.ndarray :param pmf_target: Probability mass function of the target distribution - :type pmf_target: np.ndarray :return: Maximum mean discrepancy - :rtype: float """ pmf_model[pmf_model == 0] = 1e-8 sigma = 1/pmf_model.shape[1] @@ -161,12 +138,12 @@ def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: class Timing: """ - This module is an abstraction of time measurement for both CPU and GPU processes + This module is an abstraction of time measurement for both CPU and GPU processes. """ def __init__(self): """ - Constructor method + Constructor method. """ if GPU: @@ -180,27 +157,31 @@ def __init__(self): def start_recording_cpu(self): """ - Function to start time measurement on the CPU + Function to start time measurement on the CPU. """ self.start_cpu = start_time_measurement() - def stop_recording_cpu(self): + def stop_recording_cpu(self) -> float: """ - Function to stop time measurement on the CPU + Function to stop time measurement on the CPU. + + .return: Elapsed time in milliseconds """ return end_time_measurement(self.start_cpu) def start_recording_gpu(self): """ - Function to start time measurement on the GPU + Function to start time measurement on the GPU. """ self.start_gpu = np.cuda.Event() self.end_gpu = np.cuda.Event() self.start_gpu.record() - def stop_recording_gpu(self): + def stop_recording_gpu(self) -> float: """ - Function to stop time measurement on the GPU + Function to stop time measurement on the GPU. + + :return: Elapsed time in milliseconds """ self.end_gpu.record() self.end_gpu.synchronize() diff --git a/src/quark2_adapter/adapters.py b/src/quark2_adapter/adapters.py index 5135a101..e01bbf5d 100644 --- a/src/quark2_adapter/adapters.py +++ b/src/quark2_adapter/adapters.py @@ -15,8 +15,8 @@ from abc import ABC import json -from time import time import logging +from time import time from modules.Core import Core from modules.applications.Application import Application as Application_NEW @@ -48,9 +48,9 @@ class ApplicationAdapter(Application_NEW, Application_OLD, ABC): to get your Application running with QUARK2. """ - def __init__(self, application_name, *args, **kwargs): + def __init__(self, application_name: str, *args, **kwargs): """ - Constructor method + Constructor method. """ logging.warning(WARNING_MSG, self.__class__.__name__) Application_NEW.__init__(self, application_name) @@ -62,8 +62,10 @@ def __init__(self, application_name, *args, **kwargs): self.problems = {} @property - def submodule_options(self): - """Maps the old attribute mapping_options to the new attribute submodule_options.""" + def submodule_options(self) -> list[str]: + """ + Maps the old attribute mapping_options to the new attribute submodule_options. + """ return self.mapping_options @submodule_options.setter @@ -71,7 +73,7 @@ def submodule_options(self, options: list[str]): """ Maps the old attribute mapping_options to the new attribute submodule_options. - :param options: list[str] + :param options: List of submodule options """ self.mapping_options = options @@ -80,24 +82,18 @@ def get_default_submodule(self, option: str) -> Core: Maps the old method get_mapping to the new get_default_submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ return self.get_mapping(option) - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Application_NEW.preprocess using the Application_OLD interface. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ start = time() logging.warning(WARNING_MSG, self.__class__.__name__) @@ -117,39 +113,34 @@ def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): self.problem, creation_time = self.problems[problem_key] else: start = time() - logging.info("generate new problem instance") + logging.info("Generating new problem instance") self.problem = self.generate_problem(config, rep_count) - creation_time = (time() - start)*1000 + creation_time = (time() - start) * 1000 self.problems[problem_key] = (self.problem, creation_time) + return self.problem, creation_time - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Application_NEW.postprocess using the Application_OLD interface. :param input_data: Input data comes from the submodule if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the postprocessing and the time it took to postprocess - :rtype: (any, float) """ - processed_solution, time_processing = self.process_solution(input_data) solution_validity, time_validate = self.validate(processed_solution) - if solution_validity: - solution_quality, time_evaluate = self.evaluate(processed_solution) - else: - solution_quality, time_evaluate = None, 0.0 + solution_quality, time_evaluate = (self.evaluate(processed_solution) + if solution_validity else (None, 0.0)) self.metrics.add_metric("time_to_validation", time_validate) self.metrics.add_metric("time_to_validation_unit", "ms") self.metrics.add_metric("solution_validity", solution_validity) self.metrics.add_metric("solution_quality", solution_quality) self.metrics.add_metric("solution_quality_unit", self.get_solution_quality_unit()) - return (solution_validity, solution_quality), time_validate+time_evaluate+time_processing + + return (solution_validity, solution_quality), time_validate + time_evaluate + time_processing class MappingAdapter(Mapping_NEW, Mapping_OLD, ABC): @@ -168,24 +159,26 @@ class MappingAdapter(Mapping_NEW, Mapping_OLD, ABC): to get your Mapping running with QUARK2. """ - def __init__(self,*args, **kwargs): + def __init__(self, *args, **kwargs): """ - Constructor method + Constructor method. """ Mapping_NEW.__init__(self) Mapping_OLD.__init__(self) @property - def submodule_options(self): - """Maps the old attribute solver_options to the new attribute submodule_options.""" + def submodule_options(self) -> list[str]: + """ + Maps the old attribute solver_options to the new attribute submodule_options. + """ return self.solver_options @submodule_options.setter - def submodule_options(self, options): + def submodule_options(self, options: list[str]): """ Maps the old attribute solver_options to the new attribute submodule_options. - :param options: list[str] + :param options: List of solver options """ self.solver_options = options @@ -194,47 +187,39 @@ def get_default_submodule(self, option: str) -> Core: Maps the old method get_solver to the new get_default_submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ return self.get_solver(option) - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Mapping_NEW.preprocess using the Mapping_OLD interface. """ logging.warning(WARNING_MSG, self.__class__.__name__) return self.map(input_data, config=config) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Mapping_NEW.postprocess using the Mapping_OLD interface. """ logging.info("Calling %s.reverse_map", __class__.__name__) processed_solution, postprocessing_time = self.reverse_map(input_data) - - # self.metrics.add_metric("processed_solution", ["%s: %s" % ( - # sol.__class__.__name__, sol) for sol in processed_solution]) return processed_solution, postprocessing_time def recursive_replace_dict_keys(obj: any)-> any: """ - Replace values used as dict-keys by its str(), to make the object json compatible. + Replace values used as dictionary keys by their string representation + to make the object JSON-compatible. - :param obj: the object - :type obj: any + :param obj: The object to convert + .return: The object with all dictionary keys converted to strings """ obj_new = None if isinstance(obj, dict): - obj_new = {} - for key in obj: - obj_new[str(key)] = recursive_replace_dict_keys(obj[key]) + obj_new = {str(key): recursive_replace_dict_keys(value) for key, value in obj.items()} elif isinstance(obj, list): - obj_new = [] - for element in obj: - obj_new.append(recursive_replace_dict_keys(element)) + obj_new = [recursive_replace_dict_keys(element) for element in obj] elif isinstance(obj, tuple): obj_new = tuple(recursive_replace_dict_keys(element) for element in obj) else: @@ -259,24 +244,26 @@ class SolverAdapter(Solver_NEW, Solver_OLD, ABC): to get your Solver running with QUARK2. """ - def __init__(self,*args, **kwargs): + def __init__(self, *args, **kwargs): """ - Constructor method + Constructor method. """ Solver_NEW.__init__(self) Solver_OLD.__init__(self) @property - def submodule_options(self): - """Maps the old attribute device_options to the new attribute submodule_options.""" + def submodule_options(self) -> list[str]: + """ + Maps the old attribute device_options to the new attribute submodule_options. + """ return self.device_options @submodule_options.setter - def submodule_options(self, options): + def submodule_options(self, options: list[str]): """ Maps the old attribute device_options to the new attribute submodule_options. - :param options: list[str] + :param options: List of device options """ self.device_options = options @@ -285,49 +272,49 @@ def get_default_submodule(self, option: str) -> Core: Maps the old method get_device to the new get_default_submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ return self.get_device(option) - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Solver_NEW.preprocess using the Solver_OLD interface. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ logging.warning(WARNING_MSG, self.__class__.__name__) return input_data, 0.0 - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Solver_NEW.postprocess using the Solver_OLD interface. :param input_data: Data passed to the run function of the solver - :type input_data: any - :param config: solver config - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param config: Solver config + :param kwargs: Optional keyword arguments :return: Output and time needed - :rtype: (any, float) """ run_kwargs = { - "store_dir": kwargs["store_dir"], "repetition": kwargs["rep_count"]} - raw_solution, runtime, additional_solver_information = self.run(input_data["mapped_problem"], - device_wrapper=input_data["device"], - config=config, **run_kwargs) - self.metrics.add_metric("additional_solver_information", dict( - additional_solver_information)) - self.metrics.add_metric("solution_raw", self.raw_solution_to_json(raw_solution)) + "store_dir": kwargs["store_dir"], + "repetition": kwargs["rep_count"] + } + raw_solution, runtime, additional_solver_information = self.run( + input_data["mapped_problem"], + device_wrapper=input_data["device"], + config=config, + **run_kwargs + ) + + self.metrics.add_metric( + "additional_solver_information", dict(additional_solver_information) + ) + self.metrics.add_metric( + "solution_raw", self.raw_solution_to_json(raw_solution) + ) + return raw_solution, runtime def raw_solution_to_json(self, raw_solution: any) -> any: @@ -337,9 +324,8 @@ def raw_solution_to_json(self, raw_solution: any) -> any: to json. Note that using 'recursive_replace_dict_keys' provided by this module might help. - :param raw_solution: the raw solution - :type raw_solution: any - :rtype: any + :param raw_solution: The raw solution + :return: JSON-compatible representation of the raw solution """ return raw_solution @@ -360,9 +346,9 @@ class DeviceAdapter(Device_NEW, Device_OLD): to get your Device running with QUARK2. """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. """ Device_NEW.__init__(self, name) Device_OLD.__init__(self, name) @@ -374,24 +360,18 @@ def get_default_submodule(self, option: str) -> Core: could not have submodules. :param option: String with the chosen submodule - :type option: str :return: None - :rtype: Core """ return None - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Device_NEW.preprocess using the Device_OLD interface. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the device - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ logging.warning(WARNING_MSG, self.__class__.__name__) self.set_config(config) @@ -416,7 +396,7 @@ class LocalAdapter(DeviceAdapter): def __init__(self): """ - Constructor method + Constructor method. """ DeviceAdapter.__init__(self, name="local") self.device = None diff --git a/src/quark2_adapter/legacy_classes/Application.py b/src/quark2_adapter/legacy_classes/Application.py index 71b2417f..ab5f3321 100644 --- a/src/quark2_adapter/legacy_classes/Application.py +++ b/src/quark2_adapter/legacy_classes/Application.py @@ -20,31 +20,32 @@ class Application(ABC): """ - The application component defines the workload, comprising a dataset of increasing complexity, a validation, and an - evaluation function. + The application component defines the workload, comprising a dataset of increasing complexity, + a validation, and an evaluation function. """ - def __init__(self, application_name): + def __init__(self, application_name: str): """ - Constructor method + Constructor method. + + :param application_name: Name of the application """ self.application_name = application_name self.application = None - self.mapping_options = [] - self.sub_options = [] + self.mapping_options: list[str] = [] + self.sub_options: list[dict]= [] self.problem = None - self.problems = {} + self.problems: dict = {} self.conf_idx = None super().__init__() def get_application(self) -> any: """ - Getter that returns the application + Getter that returns the application. - :return: self.application - :rtype: any + :return: The application instance """ return self.application @@ -54,7 +55,6 @@ def get_solution_quality_unit(self) -> str: Method to return the unit of the evaluation which is used to make the plots nicer. :return: String with the unit - :rtype: str """ @abstractmethod @@ -78,23 +78,22 @@ def get_parameter_options(self) -> dict: } :return: Available application settings for this application - :rtype: dict """ pass def regenerate_on_iteration(self, config: dict) -> bool: - """Overwrite this to return True if the problem should be newly generated + """ + Overwrite this to return True if the problem should be newly generated on every iteration. Typically, this will be the case if the problem is taken from a statistical ensemble e.g. an erdos-renyi graph. - :param config: the application configuration - :type config: dict - :return: whether the problem should be recreated on every iteration. Returns False if not overwritten. - :rtype: bool + + :param config: The application configuration + :return: Whether the problem should be recreated on every iteration. Returns False if not overwritten. """ return False @final - def init_problem(self, config, conf_idx: int, iter_count: int, path): + def init_problem(self, config: dict, conf_idx: int, iter_count: int, path: str) ->any: """ This method is called on every iteration and calls generate_problem if necessary. conf_idx identifies the application configuration. @@ -106,17 +105,11 @@ def init_problem(self, config, conf_idx: int, iter_count: int, path): over the different application configurations (conf_idx). :param config: the application configuration - :type config: dict :param conf_idx: the index of the application configuration - :conf_idx: int :param iter_count: the repetition count (starting with 1) - :type iter_count: int :param path: the path used to save each newly generated problem instance - :type path: str :return: the current problem instance - :rtype: any """ - if conf_idx != self.conf_idx: self.problems = {} self.conf_idx = conf_idx @@ -135,65 +128,49 @@ def generate_problem(self, config: dict, iter_count: int) -> any: """ Depending on the config this method creates a concrete problem and returns it. - :param config: - :type config: dict - :param iter_count: the iteration count - :type iter_count: int - :return: - :rtype: any + :param config: The application configuration + :param iter_count: The iteration count + :return: The generated problem instance """ pass - def process_solution(self, solution) -> (any, float): + def process_solution(self, solution) -> tuple[any, float]: """ Most of the time the solution has to be processed before it can be validated and evaluated This might not be necessary in all cases, so the default is to return the original solution. - :param solution: - :type solution: any + :param solution: The solution to be processed :return: Processed solution and the execution time to process it - :rtype: tuple(any, float) - """ return solution, 0 @abstractmethod - def validate(self, solution) -> (bool, float): + def validate(self, solution) -> Tuple[bool, float]: """ - Check if the solution is a valid solution. - - :return: bool and the time it took to create it - :param solution: - :type solution: any - :rtype: tuple(bool, float) + Check if the solution is valid. + :param solution: The solution to validate + :return: Boolean indicating if the solution is valid and the time it took to create it """ pass @abstractmethod - def evaluate(self, solution: any) -> (float, float): + def evaluate(self, solution: any) -> tuple[float, float]: """ Checks how good the solution is to allow comparison to other solutions. - :param solution: - :type solution: any + :param solution: The solution to evaluate :return: Evaluation and the time it took to create it - :rtype: tuple(any, float) - """ pass @abstractmethod def save(self, path: str, iter_count: int) -> None: """ - Function to save the concrete problem. + Save the concrete problem. - :param path: path of the experiment directory for this run - :type path: str + :param path: Path of the experiment directory for this run :param iter_count: the iteration count - :type iter_count: int - :return: - :rtype: None """ pass @@ -202,10 +179,8 @@ def get_submodule(self, mapping_option: str) -> any: If self.sub_options is not None, a mapping is instantiated according to the information given in self.sub_options. Otherwise, get_mapping is called as fall back. - :param mapping_option: String with the option - :type mapping_option: str + :param mapping_option: The option for the mapping :return: instance of a mapping class - :rtype: any """ if self.sub_options is None: return self.get_mapping(mapping_option) @@ -219,20 +194,17 @@ def get_mapping(self, mapping_option: str) -> any: self.sub_options is None. See get_submodule. :param mapping_option: String with the option - :rtype: str - :return: instance of a mapping class - :rtype: any + :return: Instance of a mapping class """ pass def get_available_mapping_options(self) -> list: """ - Get list of available mapping options. + Gets the list of available mapping options. :return: list of mapping options - :rtype: list """ if self.sub_options is None: return self.mapping_options else: - return [o["name"] for o in self.sub_options] + return [option["name"] for option in self.sub_options] diff --git a/src/quark2_adapter/legacy_classes/Device.py b/src/quark2_adapter/legacy_classes/Device.py index dec7d6a0..f5cf74da 100644 --- a/src/quark2_adapter/legacy_classes/Device.py +++ b/src/quark2_adapter/legacy_classes/Device.py @@ -17,16 +17,19 @@ class Device(ABC): """ - The device class abstracts away details of the physical device, such as submitting a task to the quantum system. + The device class abstracts away details of the physical device, + such as submitting a task to the quantum system. """ def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: Name of the device """ - self.device = None - self.device_name = device_name - self.config = None + self.device: any = None + self.device_name: str = device_name + self.config: dict = None def get_parameter_options(self) -> dict: """ @@ -44,28 +47,29 @@ def get_parameter_options(self) -> dict: } :return: Available device settings for this device - :rtype: dict """ - return { - } + return {} + + def set_config(self, config: dict) -> None: + """ + Sets the device configuration. - def set_config(self, config): + :param config: Configuration dictionary + """ self.config = config def get_device(self) -> any: """ - Returns Device. + Returns the device instance. - :return: Instance of the device class - :rtype: any + :return: Instance of the device """ return self.device def get_device_name(self) -> str: """ - Returns Device name. + Returns the name of the Device. :return: Name of the device - :rtype: str """ return self.device_name diff --git a/src/quark2_adapter/legacy_classes/Mapping.py b/src/quark2_adapter/legacy_classes/Mapping.py index 971f5af2..456c6e91 100644 --- a/src/quark2_adapter/legacy_classes/Mapping.py +++ b/src/quark2_adapter/legacy_classes/Mapping.py @@ -13,47 +13,42 @@ # limitations under the License. from abc import ABC, abstractmethod -from time import time from utils import _get_instance_with_sub_options class Mapping(ABC): """ - The task of the mapping module is to translate the application’s data and problem specification into a mathematical - formulation suitable for a solver. + The task of the mapping module is to translate the application’s data and problem specification + into a mathematical formulation suitable for a solver. """ def __init__(self): """ - Constructor method + Constructor method. """ - self.solver_options = [] - self.sub_options = None + self.solver_options: list[str] = [] + self.sub_options: list[dict] = None super().__init__() @abstractmethod - def map(self, problem, config) -> (any, float): + def map(self, problem: any, config: dict) -> tuple[any, float]: """ Maps the given problem into a specific format a solver can work with. E.g. graph to QUBO. - :param config: instance of class Config specifying the mapping settings - :param problem: problem instance which should be mapped to the target representation - :return: Must always return the mapped problem and the time it took to create the mapping - :rtype: tuple(any, float) + :param problem: Problem instance which should be mapped to the target representation + :param config: Instance of class Config specifying the mapping settings + :return: The mapped problem and the time it took to create the mapping """ pass - def reverse_map(self, solution) -> (any, float): + def reverse_map(self, solution: any) -> tuple[any, float]: """ Maps the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. - :param solution: - :type solution: any + :param solution: Solution to be reversed back to its original representation :return: Mapped solution and the time it took to create it - :rtype: tuple(any, float) - """ return solution, 0 @@ -74,7 +69,6 @@ def get_parameter_options(self) -> dict: } :return: Returns the available parameter options of this mapping - :rtype: dict """ pass @@ -83,10 +77,8 @@ def get_submodule(self, solver_option: str) -> any: If self.sub_options is not None, a solver is instantiated according to the information given in sub_options. Otherwise, get_solver is called as fall back. - :param solver_option: String with the option - :type solver_option: str - :return: instance of a solver class - :rtype: any + :param solver_option: The option for the solver + :return: Instance of a solver class """ if self.sub_options is None: return self.get_solver(solver_option) @@ -99,10 +91,8 @@ def get_solver(self, solver_option: str) -> any: Returns the default solver for a given string. This applies only if self.sub_options is None. See get_submodule. - :param solver_option: desired solver - :type solver_option: str - :return: instance of solver class - :rtype: any + :param solver_option: desired solver option + :return: Instance of solver class """ pass @@ -110,10 +100,9 @@ def get_available_solver_options(self) -> list: """ Returns all available solvers. - :return: list of solvers - :rtype: list + :return: List of solvers """ if self.sub_options is None: return self.solver_options else: - return [o["name"] for o in self.sub_options] + return [option["name"] for option in self.sub_options] diff --git a/src/quark2_adapter/legacy_classes/Solver.py b/src/quark2_adapter/legacy_classes/Solver.py index b103e9fb..650ec4f4 100644 --- a/src/quark2_adapter/legacy_classes/Solver.py +++ b/src/quark2_adapter/legacy_classes/Solver.py @@ -13,40 +13,33 @@ # limitations under the License. from abc import ABC, abstractmethod -from time import time from utils import _get_instance_with_sub_options - class Solver(ABC): """ - The solver is responsible for finding feasible and high-quality solutions of the formulated problem, i.e., of the - defined objective function. + The solver is responsible for finding feasible and high-quality solutions + of the formulated problem, i.e., of the defined objective function. """ def __init__(self): """ - Constructor method + Constructor method. """ - self.device_options = [] - self.sub_options = None + self.device_options: list[str] = [] + self.sub_options: list[dict] = None super().__init__() @abstractmethod - def run(self, mapped_problem, device, config, **kwargs) -> (any, float, dict): + def run(self, mapped_problem: any, device: any , config: dict, **kwargs) -> tuple[any, float, dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. - :param mapped_problem: a representation of the problem that the solver can solve - :type mapped_problem: any - :param device: a device the solver can leverage for the algorithm - :type device: any - :param config: settings for the solver such as hyperparameters - :type config: any - :param kwargs: optional additional settings - :type kwargs: any + :param mapped_problem: A representation of the problem that the solver can solve + :param device: A device the solver can leverage for the algorithm + :param config: Settings for the solver such as hyperparameters + :param kwargs: Optional additional settings :return: Solution, the time it took to compute it and some optional additional information - :rtype: tuple(any, float, dict) """ pass @@ -67,7 +60,6 @@ def get_parameter_options(self) -> dict: } :return: Available solver settings for this solver - :rtype: dict """ pass @@ -76,10 +68,8 @@ def get_submodule(self, device_option: str) -> any: If self.sub_options is not None, a device is instantiated according to the information given in self.sub_options. Otherwise, get_device is called as fall back. - :param device_option: String with the option - :type device_option: str - :return: instance of the device class - :rtype: any + :param device_option: The option for the device + :return: Instance of the device class """ if self.sub_options is None: return self.get_device(device_option) @@ -92,19 +82,16 @@ def get_device(self, device_option: str) -> any: Returns the default device based on string. This applies only if self.sub_options is None. See get_submodule. - :param device_option: - :type device_option: str - :return: instance of the device class - :rtype: any + :param device_option: Desired device option + :return: Instance of the device class """ pass def get_available_device_options(self) -> list: """ - Returns list of devices. + Returns the list of available devices. - :return: list of devices - :rtype: list + :return: List of devices """ if self.sub_options is None: return self.device_options diff --git a/src/utils.py b/src/utils.py index 0c9c5992..637f7050 100644 --- a/src/utils.py +++ b/src/utils.py @@ -25,23 +25,18 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: """ - Creates an instance of the QUARK module identified by class_name + Creates an instance of the QUARK module identified by class_name. - :param options: Section of the QUARK module configuration including the submodules' information. - :type options: list of dict - :param name: name of the QUARK component to be initialized - :type name: str + :param options: Section of the QUARK module configuration including the submodules' information + :param name: Name of the QUARK component to be initialized :return: New instance of the QUARK module - :rtype: any """ for opt in options: if name != opt["name"]: continue class_name = opt.get("class", name) clazz = _import_class(opt["module"], class_name, opt.get("dir")) - sub_options = None - if "submodules" in opt: - sub_options = opt["submodules"] + sub_options = opt.get("submodules", None) # In case the class requires some arguments in its constructor they can be defined in the "args" dict if "args" in opt and opt["args"]: @@ -62,7 +57,8 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: instance.metrics.add_metric_batch({ "module_git_revision_number": git_revision_number, - "module_git_uncommitted_changes": git_uncommitted_changes}) + "module_git_uncommitted_changes": git_uncommitted_changes + }) # sub_options inherits 'dir' if sub_options and "dir" in opt: @@ -72,6 +68,7 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: instance.sub_options = sub_options return instance + logging.error(f"{name} not found in {options}") raise ValueError(f"{name} not found in {options}") @@ -84,11 +81,8 @@ def _import_class(module_path: str, class_name: str, base_dir: str = None) -> ty unless it's already contained in it. :param module_path: Python module path of the module containing the class to be imported - :type module_path: str :param class_name: Name of the class to be imported - :type class_name: str :return: Imported class object - :rtype: type """ # Make sure that base_dir is in the search path. @@ -103,18 +97,13 @@ def _import_class(module_path: str, class_name: str, base_dir: str = None) -> ty def checkbox(key: str, message: str, choices: list) -> dict: """ - Wrapper method to avoid empty responses in checkbox + Wrapper method to avoid empty responses in checkbox. :param key: Key for response dict - :type key: str :param message: Message for the user - :type message: str :param choices: Choices for the user - :type choices: list :return: Dict with the response from the user - :rtype: dict """ - if len(choices) > 1: answer = inquirer.prompt([inquirer.Checkbox(key, message=message, choices=choices)]) else: @@ -129,15 +118,13 @@ def checkbox(key: str, message: str, choices: list) -> dict: return answer -def get_git_revision(git_dir: str) -> (str, str): +def get_git_revision(git_dir: str) -> tuple[str, str]: """ Collects git revision number and checks if there are uncommitted changes to allow user to analyze which - codebase was used + codebase was used. :param git_dir: Directory of the git repository - :type git_dir: str :return: Tuple with git_revision_number, git_uncommitted_changes - :rtype: (str, str) """ try: # '-C', git_dir ensures that the following commands also work when QUARK is started from other working @@ -150,7 +137,8 @@ def get_git_revision(git_dir: str) -> (str, str): logging.info( f"Codebase is based on revision {git_revision_number} and has " - f"{'some' if git_uncommitted_changes else 'no'} uncommitted changes") + f"{'some' if git_uncommitted_changes else 'no'} uncommitted changes" + ) except Exception as e: logging.warning(f"Logging of git revision number not possible because of: {e}") git_revision_number = "unknown" @@ -162,14 +150,11 @@ def get_git_revision(git_dir: str) -> (str, str): def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: """ Expands the paths given as value of the 'dir' attribute appearing in the QUARK modules - configuration by joining base_dir with that path + configuration by joining base_dir with that path. - :param j: the json to be adapted - expected to be a QUARK modules configuration or a part of it - :type j: dict|list - :param base_dir: the base directory to be used for path expansion - :type base_dir: str - :return: the adapted json - :rtype: dict|list + :param j: The JSON to be adapted - expected to be a QUARK modules configuration or a part of it + :param base_dir: The base directory to be used for path expansion + :return: The adapted json """ assert type(j) in [dict, list], f"unexpected type:{type(j)}" if type(j) == list: @@ -188,22 +173,19 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: def start_time_measurement() -> float: """ - Starts a time measurement + Starts a time measurement. :return: Starting point - :rtype: float """ return time.perf_counter() def end_time_measurement(start: float) -> float: """ - Returns the result of the time measurement in milliseconds + Returns the result of the time measurement in milliseconds. :param start: Starting point for the measurement - :type start: float :return: Time elapsed in ms - :rtype: float """ end = time.perf_counter() return round((end - start) * 1000, 3) @@ -211,7 +193,7 @@ def end_time_measurement(start: float) -> float: def stop_watch(position: int = None) -> Callable: """ - Usage as decorator to measure time, eg: + Usage as decorator to measure time, e.g.: ``` @stop_watch() def run(input_data,...): @@ -227,9 +209,7 @@ def run(input_data,...): :param position: The position at which the measured time gets inserted in the return tuple. If not specified the measured time will be appended to the original return value. - :type position: int :return: The wrapper function - :rtype: Callable """ def wrap(func): def wrapper(*args, **kwargs): diff --git a/src/utils_mpi.py b/src/utils_mpi.py index 7fd6c2a2..1a468da7 100644 --- a/src/utils_mpi.py +++ b/src/utils_mpi.py @@ -17,6 +17,12 @@ def is_running_mpiexec(): + """ + Determines if the script is running under mpiexec. + + :return: True if running under mpiexec, False otherwise + :rtype: bool + """ # This is not 100% robust but should cover MPICH & Open MPI for key in os.environ: if key.startswith("PMI_") or key.startswith("OMPI_COMM_WORLD_"): @@ -25,51 +31,74 @@ def is_running_mpiexec(): def is_running_mpi(): + """ + Determines if the MPI environment is available and import mpi4py if so. + + :return: MPI object if available, None otherwise + :rtype: MPI or None + """ if is_running_mpiexec(): try: from mpi4py import MPI # pylint: disable=C0415 except ImportError as e: raise RuntimeError( 'it seems you are running mpiexec/mpirun but mpi4py cannot be ' - 'imported, maybe you forgot to install it?') from e + 'imported, maybe you forgot to install it?' + ) from e else: MPI = None return MPI class MPIStreamHandler(logging.StreamHandler): + """ + A logging handler that only emits records from the root process in an MPI environment. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) MPI = is_running_mpi() - if MPI: - self.rank = MPI.COMM_WORLD.Get_rank() - else: - self.rank = 0 + self.rank = MPI.COMM_WORLD.Get_rank() if MPI else 0 def emit(self, record): - # don't log unless I am the root process + """ + Emits a log record if running on the root process. + + :param record: Log record + :type record: Logging.LOgRecord + """ if self.rank == 0: super().emit(record) class MPIFileHandler(logging.FileHandler): + """ + A logging handler that only emits records to a file from the root process in an MPI environment. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) MPI = is_running_mpi() - if MPI: - self.rank = MPI.COMM_WORLD.Get_rank() - else: - self.rank = 0 + self.rank = MPI.COMM_WORLD.Get_rank() if MPI else 0 def emit(self, record): - # don't log unless I am the root process + """ + Emits a log record if running on the root process. + + :param record: Log record + :type record: Logging.LOgRecord + """ if self.rank == 0: super().emit(record) def get_comm(): + """ + Retrieves the MPI communicator if running in an MPI environment, otherwise provides a mock comm class. + + return: MPI communicator or a mock class with limited methods + rtype: MPI.Comm or class + """ MPI = is_running_mpi() if MPI: comm = MPI.COMM_WORLD @@ -86,4 +115,5 @@ def Bcast(loss, root): @staticmethod def Barrier(): pass + return comm From 55d877fc5b0cef942500a9721c816beca8a82c1e Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 8 Oct 2024 11:57:55 +0200 Subject: [PATCH 07/40] Refactor code for PEP8 compliance and improved readability --- src/Plotter.py | 3 +- src/modules/applications/Application.py | 7 +- src/modules/applications/Mapping.py | 9 +- src/modules/applications/QML/QML.py | 5 +- .../generative_modeling/GenerativeModeling.py | 35 +++-- .../data/data_handler/ContinuousData.py | 48 ++++--- .../data/data_handler/DataHandler.py | 38 +++--- .../data/data_handler/DiscreteData.py | 51 ++++---- .../data_handler/MetricsGeneralization.py | 48 +++---- .../mappings/CustomQiskitNoisyBackend.py | 120 +++++++++--------- .../generative_modeling/mappings/Library.py | 26 ++-- .../mappings/LibraryPennylane.py | 46 +++---- .../mappings/LibraryQiskit.py | 65 ++++++---- .../mappings/PresetQiskitNoisyBackend.py | 76 +++++------ .../transformations/MinMax.py | 31 +++-- .../transformations/PIT.py | 48 ++++--- .../transformations/Transformation.py | 37 +++--- .../optimization/PVC/mappings/ISING.py | 2 +- .../optimization/SAT/mappings/Direct.py | 2 +- src/modules/circuits/Circuit.py | 9 +- src/modules/circuits/CircuitCardinality.py | 25 ++-- src/modules/circuits/CircuitCopula.py | 12 +- src/modules/circuits/CircuitStandard.py | 10 +- src/modules/devices/Device.py | 11 +- src/modules/devices/HelperClass.py | 3 +- src/modules/devices/Local.py | 3 +- .../devices/SimulatedAnnealingSampler.py | 5 +- src/modules/devices/braket/Braket.py | 5 +- src/modules/devices/braket/Ionq.py | 7 +- src/modules/devices/braket/LocalSimulator.py | 3 +- src/modules/devices/braket/OQC.py | 3 +- src/modules/devices/braket/Rigetti.py | 3 +- src/modules/devices/braket/SV1.py | 3 +- src/modules/devices/braket/TN1.py | 10 +- .../devices/pulser/MockNeutralAtomDevice.py | 6 +- src/modules/devices/pulser/Pulser.py | 9 +- src/modules/solvers/Annealer.py | 10 +- src/modules/solvers/ClassicalSAT.py | 12 +- src/modules/solvers/GreedyClassicalPVC.py | 11 +- src/modules/solvers/GreedyClassicalTSP.py | 13 +- src/modules/solvers/MIPsolverACL.py | 8 +- src/modules/solvers/NeutralAtomMIS.py | 17 +-- src/modules/solvers/PennylaneQAOA.py | 29 ++--- src/modules/solvers/QAOA.py | 107 ++++++++-------- src/modules/solvers/QiskitQAOA.py | 26 ++-- src/modules/solvers/RandomClassicalPVC.py | 11 +- src/modules/solvers/RandomClassicalSAT.py | 12 +- src/modules/solvers/RandomClassicalTSP.py | 12 +- .../solvers/ReverseGreedyClassicalPVC.py | 15 +-- .../solvers/ReverseGreedyClassicalTSP.py | 11 +- src/modules/solvers/Solver.py | 5 +- src/modules/training/QCBM.py | 4 +- .../legacy_classes/Application.py | 6 +- src/quark2_adapter/legacy_classes/Device.py | 6 +- src/quark2_adapter/legacy_classes/Mapping.py | 4 +- src/quark2_adapter/legacy_classes/Solver.py | 4 +- 56 files changed, 558 insertions(+), 589 deletions(-) diff --git a/src/Plotter.py b/src/Plotter.py index 4914a424..619cac04 100644 --- a/src/Plotter.py +++ b/src/Plotter.py @@ -13,7 +13,6 @@ # limitations under the License. from collections import defaultdict -from typing import List, Dict import logging import matplotlib.pyplot as plt @@ -31,7 +30,7 @@ class Plotter: """ @staticmethod - def visualize_results(results: List[Dict], store_dir: str) -> None: + def visualize_results(results: list[dict], store_dir: str) -> None: """ Function to plot the execution times of the benchmark. diff --git a/src/modules/applications/Application.py b/src/modules/applications/Application.py index 1ea35a03..c1a93b77 100644 --- a/src/modules/applications/Application.py +++ b/src/modules/applications/Application.py @@ -13,7 +13,6 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Any from modules.Core import Core @@ -31,7 +30,7 @@ def __init__(self, application_name: str): self.application_name = self.name self.application = None - def get_application(self) -> Any: + def get_application(self) -> any: """ Gets the application. @@ -44,7 +43,7 @@ def save(self, path: str, iter_count: int) -> None: """ Saves the concrete problem. - :param path: path of the experiment directory for this run - :param iter_count: the iteration count + :param path: Path of the experiment directory for this run + :param iter_count: The iteration count """ pass diff --git a/src/modules/applications/Mapping.py b/src/modules/applications/Mapping.py index 652dbf4c..25d18d94 100644 --- a/src/modules/applications/Mapping.py +++ b/src/modules/applications/Mapping.py @@ -13,7 +13,6 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Any, Dict, Tuple from modules.Core import Core @@ -23,7 +22,7 @@ class Mapping(Core, ABC): e.g., the application into a mathematical formulation suitable the submodule, e.g., a solver. """ - def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Maps the data to the correct target format. @@ -35,7 +34,7 @@ def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, floa output, preprocessing_time = self.map(input_data, config) return output, preprocessing_time - def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Reverse transformation/mapping from the submodule's format to the mathematical formulation suitable for the parent module. @@ -49,7 +48,7 @@ def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, flo return output, postprocessing_time @abstractmethod - def map(self, problem: Any, config: Dict) -> Tuple[Any, float]: + def map(self, problem: any, config: dict) -> tuple[any, float]: """ Maps the given problem into a specific format suitable for the submodule, e.g., a solver. @@ -59,7 +58,7 @@ def map(self, problem: Any, config: Dict) -> Tuple[Any, float]: """ pass - def reverse_map(self, solution: Any) -> Tuple[Any, float]: + def reverse_map(self, solution: any) -> tuple[any, float]: """ Maps the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed diff --git a/src/modules/applications/QML/QML.py b/src/modules/applications/QML/QML.py index c40855f2..ef7f6f86 100644 --- a/src/modules/applications/QML/QML.py +++ b/src/modules/applications/QML/QML.py @@ -13,18 +13,17 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Any, Dict from modules.applications.Application import Application class QML(Application, ABC): """ - QML Module for QUARK, is used by all QML applications + QML Module for QUARK, is used by all QML applications. """ @abstractmethod - def generate_problem(self, config: Dict) -> Any: + def generate_problem(self, config: dict) -> any: """ Creates a concrete problem and returns it. diff --git a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py b/src/modules/applications/QML/generative_modeling/GenerativeModeling.py index f703177c..4789a8db 100644 --- a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py +++ b/src/modules/applications/QML/generative_modeling/GenerativeModeling.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, Dict, Tuple, List +from typing import Union from utils import start_time_measurement, end_time_measurement from modules.applications.Application import Application @@ -31,18 +31,18 @@ class GenerativeModeling(QML): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("GenerativeModeling") self.submodule_options = ["Continuous Data", "Discrete Data"] self.data = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dicts with requirements of this module + :return: List of dicts with requirements of this module """ return [] @@ -58,20 +58,19 @@ def get_default_submodule(self, option: str) -> Union[ContinuousData, DiscreteDa raise NotImplementedError(f"Transformation Option {option} not implemented") return self.data - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this application. :return: Dictionary of configurable parameters - .. code-block:: python - - return { - "n_qubits": { - "values": [4, 6, 8, 10, 12], - "description": "How many qubits do you want to use?" - } - } - + .. code-block:: python + + return { + "n_qubits": { + "values": [4, 6, 8, 10, 12], + "description": "How many qubits do you want to use?" + } + } """ return { "n_qubits": { @@ -80,7 +79,7 @@ def get_parameter_options(self) -> Dict: } } - def generate_problem(self, config: Dict) -> Dict: + def generate_problem(self, config: dict) -> dict: """ The number of qubits is chosen for this problem. @@ -88,18 +87,16 @@ def generate_problem(self, config: Dict) -> Dict: :param config: Dictionary including the number of qubits :return: Dictionary with the number of qubits """ - application_config = {"n_qubits": config["n_qubits"]} return application_config - def preprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Dict, float]: + def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: """ Generate the actual problem instance in the preprocess function. :param input_data: Usually not used for this method :param config: Config for the problem creation :param kwargs: Optional additional arguments - :param kwargs: optional additional arguments :return: Tuple containing qubit number and the function's computation time """ start = start_time_measurement() @@ -107,7 +104,7 @@ def preprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Di output["store_dir_iter"] = f"{kwargs['store_dir']}/rep_{kwargs['rep_count']}" return output, end_time_measurement(start) - def postprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Dict, float]: + def postprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: """ Process the solution here, then validate and evaluate it. diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py index 740c26f7..d4fd9869 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Union, List, Any, Tuple, Dict +from typing import TypedDict, Union import logging import numpy as np @@ -45,15 +45,13 @@ def __init__(self): self.n_qubits = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ - return [ - {"name": "numpy", "version": "1.26.4"} - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_default_submodule(self, option: str) -> Union[PIT, MinMax]: if option == "MinMax": @@ -64,26 +62,24 @@ def get_default_submodule(self, option: str) -> Union[PIT, MinMax]: raise NotImplementedError(f"Transformation Option {option} not implemented") return self.transformation - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. :return: Dictionary of parameter options + .. code-block:: python - .. code-block:: python - - return { - "data_set": { - "values": ["X_2D", "O_2D", "MG_2D", "Stocks_2D"], - "description": "Which dataset do you want to use?" - }, - - "train_size": { - "values": [0.1, 0.3, 0.5, 0.7, 1.0], - "description": "What percentage of the dataset do you want to use for training?" - } - } + return { + "data_set": { + "values": ["X_2D", "O_2D", "MG_2D", "Stocks_2D"], + "description": "Which dataset do you want to use?" + }, + "train_size": { + "values": [0.1, 0.3, 0.5, 0.7, 1.0], + "description": "What percentage of the dataset do you want to use for training?" + } + } """ return { "data_set": { @@ -100,7 +96,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -111,14 +107,14 @@ class Config(TypedDict): data_set: int train_size: float - def data_load(self, gen_mod: dict, config: Config) -> Dict: + def data_load(self, gen_mod: dict, config: Config) -> dict: """ The chosen dataset is loaded and split into a training set. :param gen_mod: Dictionary with collected information of the previous modules :param config: Config specifying the parameters of the data handler - :return: dictionary including the mapped problem + :return: Dictionary including the mapped problem """ self.dataset_name = config["data_set"] self.n_qubits = gen_mod["n_qubits"] @@ -138,7 +134,7 @@ def data_load(self, gen_mod: dict, config: Config) -> Dict: return application_config - def evaluate(self, solution: Dict) -> Tuple[float, float]: + def evaluate(self, solution: dict) -> tuple[float, float]: """ Calculate KL in original space. diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py index f372ac48..9824ae93 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py @@ -33,7 +33,7 @@ class DataHandler(Core, ABC): def __init__(self, name: str): """ - Constructor method + Constructor method. """ super().__init__() self.dataset_name = name @@ -42,9 +42,9 @@ def __init__(self, name: str): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "numpy", "version": "1.26.4"}, @@ -56,10 +56,10 @@ def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, fl """ In this module, the preprocessing step is transforming the data to the correct target format. - :param input_data: collected information of the benchmarking process - :param config: config specifying the parameters of the training - :param kwargs: optional additional settings - :return: tuple with transformed problem and the time it took to map it + :param input_data: Collected information of the benchmarking process + :param config: Config specifying the parameters of the training + :param kwargs: Optional additional settings + :return: Tuple with transformed problem and the time it took to map it """ start = start_time_measurement() output = self.data_load(input_data, config) @@ -73,10 +73,10 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f """ In this module, the postprocessing step is transforming the data to the correct target format. - :param input_data: Any - :param config: config specifying the parameters of the training - :param kwargs: optional additional settings - :return: tuple with an output_dictionary and the time it took + :param input_data: any + :param config: Config specifying the parameters of the training + :param kwargs: Optional additional settings + :return: Tuple with an output_dictionary and the time it took """ start = start_time_measurement() store_dir_iter = input_data["store_dir_iter"] @@ -161,15 +161,15 @@ def data_load(self, gen_mod: dict, config: dict) -> tuple[any, float]: Helps to ensure that the model can effectively learn the underlying patterns and structure of the data, and produce high-quality outputs. - :param gen_mod: dictionary with collected information of the previous modules - :param config: config specifying the parameters of the data handler - :return: mapped problem and the time it took to create the mapping + :param gen_mod: Dictionary with collected information of the previous modules + :param config: Config specifying the parameters of the data handler + :return: Mapped problem and the time it took to create the mapping """ pass def generalisation(self) -> tuple[dict, float]: """ - Compute generalisation metrics + Compute generalisation metrics. :return: Evaluation and the time it took to create it """ @@ -183,8 +183,8 @@ def evaluate(self, solution: any) -> tuple[any, float]: """ Compute the best loss values. - :param solution: solution data - :return: evaluation data and the time it took to create it + :param solution: Solution data + :return: Evaluation data and the time it took to create it """ return None, 0.0 @@ -194,8 +194,8 @@ def tb_to_pd(logdir: str, rep: str) -> None: Converts TensorBoard event files in the specified log directory into a pandas DataFrame and saves it as a pickle file. - :param logdir: path to the log directory containing TensorBoard event files - :param rep: repetition counter + :param logdir: Path to the log directory containing TensorBoard event files + :param rep: Repetition counter """ event_acc = EventAccumulator(logdir) event_acc.Reload() diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py index ebcc3ab8..4c99b698 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py @@ -15,7 +15,7 @@ import itertools import logging from pprint import pformat -from typing import TypedDict, List, Dict, Tuple +from typing import TypedDict import numpy as np @@ -46,36 +46,39 @@ def __init__(self): self.solution_set = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ - return [ - {"name": "numpy", "version": "1.26.4"} - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_default_submodule(self, option: str) -> CircuitCardinality: + """ + Get the default submodule based on the given option. + + :param option: Submodule option + :return: Corresponding submodule + """ if option == "CircuitCardinality": return CircuitCardinality() else: raise NotImplementedError(f"Circuit Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. :return: A dictionary of parameter options - .. code-block:: python - - return { - "train_size": { - "values": [0.1, 0.3, 0.5, 0.7, 0.9, 1.0], - "description": "What percentage of the dataset do you want to use for training?" - } - } + .. code-block:: python + return { + "train_size": { + "values": [0.1, 0.3, 0.5, 0.7, 0.9, 1.0], + "description": "What percentage of the dataset do you want to use for training?" + } + } """ return { "train_size": { @@ -86,7 +89,7 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -96,13 +99,13 @@ class Config(TypedDict): train_size: int - def data_load(self, gen_mod: dict, config: Config) -> Dict: + def data_load(self, gen_mod: dict, config: Config) -> dict: """ The cardinality constrained dataset is created and split into a training set. :param gen_mod: Dictionary with collected information of the previous modules :param config: Config specifying the parameters of the data handler - :return: dictionary including the mapped problem + :return: Dictionary including the mapped problem """ dataset_name = "Cardinality_Constraint" self.n_qubits = gen_mod["n_qubits"] @@ -158,11 +161,11 @@ def data_load(self, gen_mod: dict, config: Config) -> Dict: return application_config - def generalisation(self) -> Tuple[Dict, float]: + def generalisation(self) -> tuple[dict, float]: """ Calculate generalization metrics for the generated. - :return: a tuple containing a dictionary of generalization metrics and the execution time + :return: A tuple containing a dictionary of generalization metrics and the execution time """ start = start_time_measurement() results = self.generalization_metrics.get_metrics(self.samples) @@ -171,13 +174,13 @@ def generalisation(self) -> Tuple[Dict, float]: return results, end_time_measurement(start) - def evaluate(self, solution: Dict) -> Tuple[Dict, float]: + def evaluate(self, solution: dict) -> tuple[dict, float]: """ Evaluates a given solution and calculates the histogram of generated samples and the minimum KL divergence value. - :param solution: dictionary containing the solution data, including generated samples and KL divergence values. - :return: a tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence + :param solution: Dictionary containing the solution data, including generated samples and KL divergence values. + :return: A tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence value, and the time it took to evaluate the solution. """ start = start_time_measurement() diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py b/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py index 788357ee..f2955953 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py @@ -20,13 +20,13 @@ class MetricsGeneralization: """ A class to compute generalization metrics for generated samples based on train and solution sets. - :param train_set: set of queries in the training set. + :param train_set: Set of queries in the training set. :type train_set: np.array - :param train_size: the fraction of queries used for training. + :param train_size: The fraction of queries used for training. :type train_size: float - :param solution_set: set of queries in the solution set. + :param solution_set: Set of queries in the solution set. :type solution_set: np.array - :param n_qubits: the number of qubits. + :param n_qubits: The number of qubits. :type n_qubits: int """ @@ -56,10 +56,10 @@ def get_masks(self) -> tuple[np.array, np.array]: def get_metrics(self, generated: np.array) -> dict: """ - Method that determines all generalization metrics of a given multiset of generated samples + Method that determines all generalization metrics of a given multiset of generated samples. - :param generated: generated samples - :return: dictionary with generalization metrics + :param generated: Generated samples + :return: Dictionary with generalization metrics """ g_new = np.sum(generated[self.mask_new]) g_sol = np.sum(generated[self.mask_sol]) @@ -79,47 +79,47 @@ def get_metrics(self, generated: np.array) -> dict: def fidelity(self, g_new: float, g_sol: float) -> float: """ - Method to determine the fidelity + Method to determine the fidelity. - :param g_new: multi-subset of unseen queries (noisy or valid) - :param g_sol: multi-subset of unseen and valid queries - :return: fidelity + :param g_new: Multi-subset of unseen queries (noisy or valid) + :param g_sol: Multi-subset of unseen and valid queries + :return: Fidelity """ return g_sol / g_new def coverage(self, g_sol_unique: float) -> float: """ - Method to determine the coverage + Method to determine the coverage. - :param g_sol_unique: subset of unique unseen and valid queries - :return: coverage + :param g_sol_unique: Subset of unique unseen and valid queries + :return: Coverage """ return g_sol_unique / (math.ceil(1 - self.train_size) * len(self.solution_set)) def normalized_rate(self, g_sol: float) -> float: """ - Method to determine the normalized_rate + Method to determine the normalized_rate. - :param g_sol: multi-subset of unseen and valid queries - :return: normalized_rate + :param g_sol: Multi-subset of unseen and valid queries + :return: Normalized_rate """ return g_sol / ((1 - self.train_size) * self.n_shots) def exploration(self, g_new: float) -> float: """ - Method to determine the exploration + Method to determine the exploration. - :param g_new: multi-subset of unseen queries (noisy or valid) - :return: exploration + :param g_new: Multi-subset of unseen queries (noisy or valid) + :return: Exploration """ return g_new / self.n_shots def precision(self, g_sol: float, g_train: float) -> float: """ - Method to determine the precision + Method to determine the precision. - :param g_sol: multi-subset of unseen and valid queries - :param g_train: number of queries that were memorized from the training set - :return: precision + :param g_sol: Multi-subset of unseen and valid queries + :param g_train: Number of queries that were memorized from the training set + :return: Precision """ return (np.sum(g_sol) + np.sum(g_train)) / self.n_shots diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py index cfa61c06..8ac1ef78 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -11,9 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from typing import Union import logging from time import perf_counter + import numpy as np from qiskit.circuit import QuantumCircuit, Parameter @@ -36,24 +38,23 @@ def split_string(s): class CustomQiskitNoisyBackend(Library): """ - This module maps a library-agnostic gate sequence to a qiskit circuit and creates an artificial noise model + This module maps a library-agnostic gate sequence to a qiskit circuit and creates an artificial noise model. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("NoisyQiskit") self.submodule_options = ["QCBM", "Inference"] - - circuit_transpiled = None + self.circuit_transpiled = None @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "qiskit", "version": "1.1.0"}, @@ -66,56 +67,56 @@ def get_parameter_options(self) -> dict: Returns the configurable settings for the Qiskit Library. :return: Dictionary with configurable settings - .. code-block:: python - - return { - "backend": { - "values": ["aer_simulator_gpu", "aer_simulator_cpu"], - "description": "Which backend do you want to use? " - "In the NoisyQiskit module only aer_simulators can be used." - }, - - "simulation_method": { - "values": ["automatic", "statevector", "density_matrix", "cpu_mps"], # TODO New names! - "description": "What simulation method should be used?" - }, - - "n_shots": { - "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model?" - # (If 'statevector' was selected as simulation_method, 'n_shots' is only relevant for - # studying generalization)" - }, - - "transpile_optimization_level": { - "values": [1, 2, 3, 0], - "description": "Switch between different optimization levels in the Qiskit transpile" - "routine. 1: light optimization, 2: heavy optimization, 3: even heavier optimization," - "0: no optimization. Level 1 recommended as standard option." - }, - - "noise_configuration": { - "values": ['Custom configurations', 'No noise'], - "description": "What noise configuration do you want to use?" - }, - "custom_readout_error": { - "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], - "description": "Add a custom readout error." - }, - "two_qubit_depolarizing_errors": { - "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], - "description": "Add a custom 2-qubit gate depolarizing error." - }, - "one_qubit_depolarizing_errors": { - "values": [0, 0.0001, 0.0005, 0.001, 0.005, 0.007, 0.01, 0.02], - "description": "Add a 1-qubit gate depolarizing error." - }, - "qubit_layout": { - # "values": [None, 'linear', 'circle', 'fully_connected', 'ibm_brisbane'], - "values": [None, 'linear', 'circle', 'fully_connected'], - "description": "How should the qubits be connected in the simulated chip: coupling_map " - } - } + .. code-block:: python + + return { + "backend": { + "values": ["aer_simulator_gpu", "aer_simulator_cpu"], + "description": "Which backend do you want to use? " + "In the NoisyQiskit module only aer_simulators can be used." + }, + + "simulation_method": { + "values": ["automatic", "statevector", "density_matrix", "cpu_mps"], # TODO New names! + "description": "What simulation method should be used?" + }, + + "n_shots": { + "values": [100, 1000, 10000, 1000000], + "description": "How many shots do you want use for estimating the PMF of the model?" + # (If 'statevector' was selected as simulation_method, 'n_shots' is only relevant for + # studying generalization)" + }, + + "transpile_optimization_level": { + "values": [1, 2, 3, 0], + "description": "Switch between different optimization levels in the Qiskit transpile" + "routine. 1: light optimization, 2: heavy optimization, 3: even heavier optimization," + "0: no optimization. Level 1 recommended as standard option." + }, + + "noise_configuration": { + "values": ['Custom configurations', 'No noise'], + "description": "What noise configuration do you want to use?" + }, + "custom_readout_error": { + "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], + "description": "Add a custom readout error." + }, + "two_qubit_depolarizing_errors": { + "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], + "description": "Add a custom 2-qubit gate depolarizing error." + }, + "one_qubit_depolarizing_errors": { + "values": [0, 0.0001, 0.0005, 0.001, 0.005, 0.007, 0.01, 0.02], + "description": "Add a 1-qubit gate depolarizing error." + }, + "qubit_layout": { + # "values": [None, 'linear', 'circle', 'fully_connected', 'ibm_brisbane'], + "values": [None, 'linear', 'circle', 'fully_connected'], + "description": "How should the qubits be connected in the simulated chip: coupling_map " + } + } """ return { @@ -193,6 +194,7 @@ def sequence_to_circuit(self, input_data: dict) -> dict: gate_sequence = input_data["gate_sequence"] circuit = QuantumCircuit(n_qubits, n_qubits) param_counter = 0 + for gate, wires in gate_sequence: if gate == "Hadamard": circuit.h(wires[0]) @@ -236,7 +238,6 @@ def sequence_to_circuit(self, input_data: dict) -> dict: input_data["circuit"] = circuit input_data.pop("gate_sequence") - logging.info(param_counter) input_data["n_params"] = len(circuit.parameters) return input_data @@ -250,11 +251,9 @@ def select_backend(config: str, n_qubits: int) -> Backend: :param n_qubits: Number of qubits :return: Configured qiskit backend """ - if config == "aer_simulator_gpu": backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") - elif config == "aer_simulator_cpu": backend = Aer.get_backend("aer_simulator") backend.set_options(device="CPU") @@ -285,7 +284,6 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: seed_transp = 42 # Remove seed if wanted logging.info(f'Using {optimization_level=} with seed: {seed_transp}') coupling_map = self.get_coupling_map(config_dict, n_qubits) - print(f"Generated coupling map: {coupling_map}") # Create a manual layout if needed (you can customize the layout based on your use case) manual_layout = Layout({circuit.qubits[i]: i for i in range(n_qubits)}) @@ -413,6 +411,7 @@ def build_noise_model(self, config_dict: dict) -> NoiseModel: :return: Constructed noise model """ noise_model = noise.NoiseModel() + if config_dict['custom_readout_error']: readout_error = config_dict['custom_readout_error'] noise_model.add_all_qubit_readout_error( @@ -449,6 +448,7 @@ def get_coupling_map(self, config_dict: dict, num_qubits: int) -> CouplingMap: :return: Coupling map """ layout = config_dict['qubit_layout'] + if layout == 'linear': return CouplingMap.from_line(num_qubits) elif layout == 'circle': diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index fe518d93..e622c2ed 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -11,18 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from abc import ABC, abstractmethod import logging -from typing import TypedDict, Any, Tuple, Dict +from typing import TypedDict from utils import start_time_measurement, end_time_measurement - from modules.Core import Core class Library(Core, ABC): """ - This class is an abstract base class for mapping a library-agnostic gate sequence to a library such as Qiskit + This class is an abstract base class for mapping a library-agnostic gate sequence to a library such as Qiskit. """ def __init__(self, name: str): @@ -34,7 +34,7 @@ def __init__(self, name: str): class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -45,14 +45,14 @@ class Config(TypedDict): backend: str n_shots: int - def preprocess(self, input_data: Dict, config: Config, **kwargs) -> Tuple[Dict, float]: + def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, float]: """ Base class for mapping the gate sequence to a library such as Qiskit. :param input_data: Collection of information from the previous modules :param config: Config specifying the number of qubits of the circuit - :param kwargs: optional keyword arguments - :return: tuple including dictionary with the function to execute the quantum circuit on a simulator or quantum + :param kwargs: Optional keyword arguments + :return: Tuple including dictionary with the function to execute the quantum circuit on a simulator or quantum hardware and the computation time of the function """ start = start_time_measurement() @@ -72,26 +72,26 @@ def preprocess(self, input_data: Dict, config: Config, **kwargs) -> Tuple[Dict, return output, end_time_measurement(start) - def postprocess(self, input_data: Dict, config: dict, **kwargs) -> Tuple[Dict, float]: + def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ This method corresponds to the identity and passes the information of the subsequent module back to the preceding module in the benchmarking process. :param input_data: Collected information of the benchmarking procesS :param config: Config specifying the number of qubits of the circuit - :param kwargs: optional keyword arguments - :return: tuple with input dictionary and the computation time of the function + :param kwargs: Optional keyword arguments + :return: Tuple with input dictionary and the computation time of the function """ start = start_time_measurement() return input_data, end_time_measurement(start) @abstractmethod - def sequence_to_circuit(self, input_data: Dict) -> Dict: + def sequence_to_circuit(self, input_data: dict) -> dict: pass @staticmethod @abstractmethod - def get_execute_circuit(self, circuit: Any, backend: Any, config: str, config_dict: Dict) -> Tuple[Any, Any]: + def get_execute_circuit(self, circuit: any, backend: any, config: str, config_dict: dict) -> tuple[any, any]: """ This method combines the circuit implementation and the selected backend and returns a function that will be called during training. @@ -109,7 +109,7 @@ def get_execute_circuit(self, circuit: Any, backend: Any, config: str, config_di @abstractmethod def select_backend(config: str, n_qubits: int) -> any: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend :param n_qubits: Number of qubits diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py index 41ab6eea..3e584ba5 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, Any, Dict, Tuple, List + +from typing import Union import numpy as np import pennylane as qml @@ -33,11 +34,11 @@ def __init__(self): self.submodule_options = ["QCBM", "QGAN", "Inference"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "pennylane", "version": "0.37.0"}, @@ -47,25 +48,24 @@ def get_requirements() -> List[Dict]: {"name": "jaxlib", "version": "0.4.30"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for the PennyLane Library. :return: Dictionary with configurable settings. - .. code-block:: python - - return { - "backend": { - "values": ["default.qubit", "default.qubit.jax", "lightning.qubit", "lightning.gpu"], - "description": "Which backend do you want to use?" - }, - - "n_shots": { - "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model?" - } - } - + .. code-block:: python + + return { + "backend": { + "values": ["default.qubit", "default.qubit.jax", "lightning.qubit", "lightning.gpu"], + "description": "Which backend do you want to use?" + }, + + "n_shots": { + "values": [100, 1000, 10000, 1000000], + "description": "How many shots do you want use for estimating the PMF of the model?" + } + } """ return { "backend": { @@ -89,7 +89,7 @@ def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: else: raise NotImplementedError(f"Training option {option} not implemented") - def sequence_to_circuit(self, input_data: Dict) -> Dict: + def sequence_to_circuit(self, input_data: dict) -> dict: """ Method that maps the gate sequence, that specifies the architecture of a quantum circuit to its PennyLane implementation. @@ -147,9 +147,9 @@ def create_circuit(params): return input_data @staticmethod - def select_backend(config: str, n_qubits: int) -> Any: + def select_backend(config: str, n_qubits: int) -> any: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend :param n_qubits: Number of qubits @@ -170,8 +170,8 @@ def select_backend(config: str, n_qubits: int) -> Any: @staticmethod def get_execute_circuit( - circuit: callable, backend: qml.device, config: str, config_dict: Dict - ) -> Tuple[any, any]: + circuit: callable, backend: qml.device, config: str, config_dict: dict + ) -> tuple[any, any]: """ This method combines the PennyLane circuit implementation and the selected backend and returns a function that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py index 6d649ab0..cd0ef9e8 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py @@ -11,13 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, Dict, List, Tuple, Any + import logging +from typing import Union +import numpy as np + from qiskit import QuantumCircuit, transpile from qiskit.circuit import Parameter from qiskit.providers import Backend from qiskit.quantum_info import Statevector -import numpy as np from modules.training.QCBM import QCBM from modules.training.QGAN import QGAN @@ -29,36 +31,36 @@ class LibraryQiskit(Library): """ - This module maps a library-agnostic gate sequence to a qiskit circuit + This module maps a library-agnostic gate sequence to a qiskit circuit. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("LibraryQiskit") self.submodule_options = ["QCBM", "QGAN", "Inference"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "qiskit", "version": "1.1.0"}, {"name": "numpy", "version": "1.26.4"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: - .. code-block:: python + :return: Dictionary with configurable settings + .. code-block:: python - return { + return { "backend": { "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", "cusvaer_simulator (only available in cuQuantum appliance)", "aer_simulator_gpu", @@ -101,10 +103,10 @@ def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: else: raise NotImplementedError(f"Option {option} not implemented") - def sequence_to_circuit(self, input_data: Dict) -> Dict: + def sequence_to_circuit(self, input_data: dict) -> dict: """ Maps the gate sequence, that specifies the architecture of a quantum circuit - to its Qiskit implementation. + to its Qiskit implementation. :param input_data: Collected information of the benchmarking process :return: Same dictionary but the gate sequence is replaced by its Qiskit implementation @@ -156,7 +158,7 @@ def sequence_to_circuit(self, input_data: Dict) -> Dict: @staticmethod def select_backend(config: str, n_qubits: int) -> any: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend :param n_qubits: Number of qubits @@ -179,22 +181,18 @@ def select_backend(config: str, n_qubits: int) -> any: from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") - elif config == "aer_simulator_cpu": from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="CPU") - elif config == "aer_statevector_simulator_gpu": from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend('statevector_simulator') backend.set_options(device="GPU") - elif config == "aer_statevector_simulator_cpu": from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend('statevector_simulator') backend.set_options(device="CPU") - elif config == "ionQ_Harmony": from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 @@ -207,7 +205,6 @@ def select_backend(config: str, n_qubits: int) -> any: online_date=device_wrapper.device.properties.service.updatedAt, backend_version="2", ) - elif config == "Amazon_SV1": from modules.devices.braket.SV1 import SV1 # pylint: disable=C0415 from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 @@ -227,8 +224,8 @@ def select_backend(config: str, n_qubits: int) -> any: return backend @staticmethod - def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: Dict) \ - -> Tuple[Any, Any]: # pylint: disable=W0221,R0915 + def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict + ) -> tuple[any, any]: # pylint: disable=W0221,R0915 """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. @@ -248,7 +245,9 @@ def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, circuit_transpiled.remove_final_measurements() def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] + all_circuits = [ + circuit_transpiled.assign_parameters(solution) for solution in solutions + ] pmfs = np.asarray([Statevector(c).probabilities() for c in all_circuits]) return pmfs, None @@ -256,13 +255,17 @@ def execute_circuit(solutions): import time as timetest # pylint: disable=C0415 def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] + all_circuits = [ + circuit_transpiled.assign_parameters(solution) for solution in solutions + ] jobs = backend.run(all_circuits, shots=n_shots) while not jobs.in_final_state(): logging.info("Waiting 10 seconds for task to finish") timetest.sleep(10) - samples_dictionary = [jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits] + samples_dictionary = [ + jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits + ] samples = [] for result in samples_dictionary: @@ -278,12 +281,20 @@ def execute_circuit(solutions): return pmfs, samples - elif config in ["cusvaer_simulator (only available in cuQuantum appliance)", "aer_simulator_cpu", - "aer_simulator_gpu"]: + elif config in [ + "cusvaer_simulator (only available in cuQuantum appliance)", + "aer_simulator_cpu", + "aer_simulator_gpu" + ]: + def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] + all_circuits = [ + circuit_transpiled.assign_parameters(solution) for solution in solutions + ] jobs = backend.run(all_circuits, shots=n_shots) - samples_dictionary = [jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits] + samples_dictionary = [ + jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits + ] samples = [] for result in samples_dictionary: diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py index e1433e48..297de789 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, Any, List, Dict, Tuple +from typing import Union import logging from time import perf_counter import numpy as np @@ -38,7 +38,7 @@ class PresetQiskitNoisyBackend(Library): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("PresetQiskitNoisyBackend") self.submodule_options = ["QCBM", "Inference"] @@ -46,11 +46,11 @@ def __init__(self): circuit_transpiled = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "qiskit", "version": "1.1.0"}, @@ -59,31 +59,30 @@ def get_requirements() -> List[Dict]: {"name": "numpy", "version": "1.26.4"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: - .. code-block:: python - - return { - "backend": { - "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", - "cusvaer_simulator (only available in cuQuantum applicance)", - "aer_simulator_gpu", - "aer_simulator_cpu", "ionQ_Harmony", "Amazon_SV1"], - "description": "Which backend do you want to use? (aer_statevector_simulator - uses the measurement probability vector, the others are shot based)" - }, - - "n_shots": { - "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model? - (If the aer_statevector_simulator selected, - only relevant for studying generalization)" - } - } - + :return: Dictionary with configurable settings. + .. code-block:: python + + return { + "backend": { + "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", + "cusvaer_simulator (only available in cuQuantum applicance)", + "aer_simulator_gpu", + "aer_simulator_cpu", "ionQ_Harmony", "Amazon_SV1"], + "description": "Which backend do you want to use? (aer_statevector_simulator + uses the measurement probability vector, the others are shot based)" + }, + + "n_shots": { + "values": [100, 1000, 10000, 1000000], + "description": "How many shots do you want use for estimating the PMF of the model? + (If the aer_statevector_simulator selected, + only relevant for studying generalization)" + } + } """ provider = FakeProviderForBackendV2() backends = provider.backends() @@ -131,7 +130,7 @@ def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: else: raise NotImplementedError(f"Option {option} not implemented") - def sequence_to_circuit(self, input_data: Dict) -> Dict: + def sequence_to_circuit(self, input_data: dict) -> dict: """ Maps the gate sequence, that specifies the architecture of a quantum circuit to its Qiskit implementation. @@ -193,7 +192,7 @@ def sequence_to_circuit(self, input_data: Dict) -> Dict: @staticmethod def select_backend(config: str, n_qubits: int) -> Backend: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend :param n_qubits: Number of qubits @@ -211,7 +210,7 @@ def select_backend(config: str, n_qubits: int) -> Backend: return backend def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pylint: disable=W0221 - config: str, config_dict: Dict) -> Tuple[Any, Any]: + config: str, config_dict: dict) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. @@ -240,7 +239,6 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli if config in ["aer_simulator_cpu", "aer_simulator_gpu"]: def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] jobs = backend.run(all_circuits, shots=n_shots) samples_dictionary = [jobs.result().get_counts(c).int_outcomes() for c in all_circuits] @@ -254,7 +252,6 @@ def execute_circuit(solutions): samples.append(target_iter) samples = np.asarray(samples) pmfs = samples / n_shots - return pmfs, samples else: @@ -306,17 +303,6 @@ def select_backend_configuration(self, noise_configuration: str, num_qubits: int else: raise ValueError(f"Unknown noise configuration: {noise_configuration}") - # IBM backend will be added with release 2.1 - # def get_ibm_backend(self, backend_name): - # service = QiskitRuntimeService() - # backend_identifier = backend_name.replace(' 127 Qubits', '').lower() - # backend = service.backend(backend_identifier) - # noise_model = NoiseModel.from_backend(backend) - # logging.info(f'Loaded with IBMQ Account {backend.name}, {backend.version}, {backend.num_qubits}') - # simulator = AerSimulator.from_backend(backend) - # simulator.noise_model = noise_model - # return simulator - def configure_backend(self, backend: Backend, device: str, simulation_method: str) -> None: """ This method configures the backend with the specified device and simulation method. @@ -332,7 +318,7 @@ def log_backend_info(self, backend: Backend): logging.info(f'Backend configuration: {backend.configuration()}') logging.info(f'Simulation method: {backend.options.method}') - def get_simulation_method_and_device(self, device: str, simulation_config: str) -> Tuple[str, str]: + def get_simulation_method_and_device(self, device: str, simulation_config: str) -> tuple[str, str]: """ This method determines the simulation method and device based on the provided configuration. @@ -381,8 +367,8 @@ def get_FakeBackend(self, noise_configuration: str, num_qubits: int) -> Backend: backend = filtered_backends[0] if num_qubits > backend.num_qubits: - logging.warning( - f'Requested number of qubits ({num_qubits}) exceeds the backend capacity. Using default aer_simulator.') + logging.warning(f'Requested number of qubits ({num_qubits}) exceeds the backend capacity. ' + f'Using default aer_simulator.') return Aer.get_backend("aer_simulator") noise_model = NoiseModel.from_backend(backend) diff --git a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py index 809d65d5..723d403e 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, List, Dict, Any, Tuple - +from typing import Union import numpy as np from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation @@ -24,7 +23,7 @@ class MinMax(Transformation): # pylint: disable=R0902 """ In min-max normalization each data point is shifted - such that it lies between 0 and 1 + such that it lies between 0 and 1. """ def __init__(self): @@ -41,11 +40,11 @@ def __init__(self): self.histogram_train_original = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "numpy", "version": "1.26.4"}] @@ -57,15 +56,15 @@ def get_default_submodule(self, option: str) -> Union[CircuitStandard, CircuitCa else: raise NotImplementedError(f"Circuit Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns empty dict as this transformation has no configurable settings + Returns empty dict as this transformation has no configurable settings. - :return: empty dict + :return: Empty dict """ return {} - def transform(self, input_data: Dict, config: Dict) -> Dict: + def transform(self, input_data: dict, config: dict) -> dict: """ Transforms the input dataset using MinMax transformation and computes histograms of the training dataset in the transformed space. @@ -124,12 +123,12 @@ def transform(self, input_data: Dict, config: Dict) -> Dict: return self.transform_config - def reverse_transform(self, input_data: Dict) -> Dict: + def reverse_transform(self, input_data: dict) -> dict: """ Transforms the solution back to the representation needed for validation/evaluation. - :param input_data: dictionary containing the solution - :return: solution transformed accordingly + :param input_data: Dictionary containing the solution + :return: Solution transformed accordingly """ best_results = input_data["best_sample"] depth = input_data["depth"] @@ -184,10 +183,10 @@ def reverse_transform(self, input_data: Dict) -> Dict: def fit_transform(self, data: np.ndarray) -> np.ndarray: """ - Method that performs the min max normalization + Method that performs the min max normalization. :param data: Data to be fitted - :return: fitted data + :return: Fitted data """ data_min = data.min() data_max = data.max() - data_min @@ -197,10 +196,10 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: def inverse_transform(self, data: np.ndarray) -> np.ndarray: """ - Method that performs the inverse min max normalization + Method that performs the inverse min max normalization. :param data: Data to be fitted - :return: data in original space + :return: Data in original space """ data_min = data.min() data_max = data.max() - data_min diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/QML/generative_modeling/transformations/PIT.py index 03ea6f14..b2f95981 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/QML/generative_modeling/transformations/PIT.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict, Tuple import numpy as np import pandas as pd @@ -40,22 +39,22 @@ def __init__(self): self.histogram_transformed = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "numpy", "version": "1.26.4"}, {"name": "pandas", "version": "2.2.2"} ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns empty dict as this transformation has no configurable settings + Returns empty dict as this transformation has no configurable settings. - :return: empty dict + :return: Empty dict """ return {} @@ -65,14 +64,14 @@ def get_default_submodule(self, option: str) -> CircuitCopula: else: raise NotImplementedError(f"Circuit Option {option} not implemented") - def transform(self, input_data: dict, config: Dict) -> Dict: + def transform(self, input_data: dict, config: dict) -> dict: """ Transforms the input dataset using PIT transformation and computes histograms of the training dataset in the transformed space. - :param input_data: dataset - :param config: config with the parameters specified in Config class - :return: dict with PIT transformation, time it took to map it + :param input_data: Dataset + :param config: Config with the parameters specified in Config class + :return: Dict with PIT transformation, time it took to map it """ self.dataset_name = input_data["dataset_name"] self.dataset = input_data["dataset"] @@ -130,12 +129,12 @@ def transform(self, input_data: dict, config: Dict) -> Dict: return self.transform_config - def reverse_transform(self, input_data: Dict) -> Dict: + def reverse_transform(self, input_data: dict) -> dict: """ Transforms the solution back to the representation needed for validation/evaluation. - :param input_data: dictionary containing the solution - :return: dictionary with solution transformed accordingly + :param input_data: Dictionary containing the solution + :return: Dictionary with solution transformed accordingly """ depth = input_data["depth"] architecture_name = input_data["architecture_name"] @@ -189,12 +188,11 @@ def reverse_transform(self, input_data: Dict) -> Dict: def fit_transform(self, data: np.ndarray) -> np.ndarray: """ - Takes the data points and applies the PIT + Takes the data points and applies the PIT. - :param data: data samples + :param data: Data samples :return: Transformed data points """ - df = pd.DataFrame(data) epit = df.copy(deep=True).transpose() self.reverse_epit_lookup = epit.copy(deep=True) @@ -207,18 +205,20 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: self.reverse_epit_lookup = self.reverse_epit_lookup.values return df.values - def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> List[float]: + def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> list[float]: """ - Takes one data point and applies the inverse PIT + Takes one data point and applies the inverse PIT. - :param values: data point + :param values: Data point :return: Data point after applying the inverse transformation """ values = values * (np.shape(self.reverse_epit_lookup)[1] - 1) rows = np.shape(self.reverse_epit_lookup)[0] + # if we are an integer do not use linear interpolation values_l = np.floor(values).astype(int) values_h = np.ceil(values).astype(int) + # if we are an integer then floor and ceiling are the same is_int_mask = 1 - (values_h - values_l) row_indexer = np.arange(rows) @@ -231,9 +231,9 @@ def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> List[float]: def inverse_transform(self, data: np.ndarray) -> np.ndarray: """ - Applies the inverse transformation to the full data set + Applies the inverse transformation to the full data set. - :param data: data set + :param data: Data set :return: Data set after applying the inverse transformation """ res = [self._reverse_emp_integral_trans_single(row) for row in data] @@ -241,6 +241,12 @@ def inverse_transform(self, data: np.ndarray) -> np.ndarray: return np.array(res)[:, 0, :] def emp_integral_trans(self, data: np.ndarray) -> np.ndarray: + """ + Applies the empirical integral transformation to the given data. + + :param data: Data points + :return: Empirically transformed data points + """ rank = np.argsort(data).argsort() length = data.size ecdf = np.linspace(0, 1, length, dtype=np.float64) diff --git a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py index 29f51d5d..87393da3 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py +++ b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py @@ -14,7 +14,6 @@ from itertools import product from abc import ABC, abstractmethod -from typing import Dict, Tuple, List import numpy as np @@ -24,49 +23,48 @@ class Transformation(Core, ABC): """ - The task of the transformation module is to translate data and problem specification of the application into - preprocessed format. + The task of the transformation module is to translate data and problem + specification of the application into preprocessed format. """ def __init__(self, name): """ - Constructor method + Constructor method. """ super().__init__() self.transformation_name = name @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "numpy", "version": "1.26.4"}] - def preprocess(self, input_data: Dict, config: Dict, **kwargs: Dict) -> Tuple[Dict, float]: + def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: """ In this module, the preprocessing step is transforming the data to the correct target format. :param input_data: Collected information of the benchmarking process :param config: Config specifying the parameters of the transformation :param kwargs: Additional optional arguments - :return: tuple with transformed problem and the time it took to map it + :return: Tuple with transformed problem and the time it took to map it """ - start = start_time_measurement() output = self.transform(input_data, config) return output, end_time_measurement(start) - def postprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, float]: + def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - Does the reverse transformation + Does the reverse transformation. :param input_data: Dictionary containing information of previously executed modules :param config: Dictionary containing additional information :param kwargs: Dictionary containing additional information - :return: tuple with the dictionary and the time the postprocessing took + :return: Tuple with the dictionary and the time the postprocessing took """ start = start_time_measurement() output = self.reverse_transform(input_data) @@ -77,7 +75,7 @@ def postprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, f return output, end_time_measurement(start) @abstractmethod - def transform(self, input_data: Dict, config: Dict) -> Dict: + def transform(self, input_data: dict, config: dict) -> dict: """ Helps to ensure that the model can effectively learn the underlying patterns and structure of the data, and produce high-quality outputs. @@ -88,7 +86,7 @@ def transform(self, input_data: Dict, config: Dict) -> Dict: """ return input_data - def reverse_transform(self, input_data: Dict) -> Dict: + def reverse_transform(self, input_data: dict) -> dict: """ Transforms the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. @@ -171,7 +169,7 @@ def generate_samples(results: np.ndarray, bin_data: np.ndarray, n_registers: int @staticmethod def generate_samples_efficient(results, bin_data: np.ndarray, n_registers: int, noisy: bool = True) -> np.ndarray: """ - Generate samples efficiently using numpy arrays based on measurement results and the grid bins + Generate samples efficiently using numpy arrays based on measurement results and the grid bins. :param results: Results of measurements. :param bin_data: Binned data. @@ -183,8 +181,11 @@ def generate_samples_efficient(results, bin_data: np.ndarray, n_registers: int, width = 1 / len(bin_data) ** (1 / n_registers) # Generate random noise or zeros - noise = 0.5 * width * np.random.uniform(low=-1, high=1, size=(n_shots, n_registers)) if noisy else np.zeros( - (n_shots, n_registers)) + noise = ( + 0.5 * width * np.random.uniform(low=-1, high=1, size=(n_shots, n_registers)) + if noisy + else np.zeros((n_shots, n_registers)) + ) # Create an array of bin_coords for each result, then stack them vertically bin_coords = bin_data[:, 1:] diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index e41faa1e..a60031fc 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -39,7 +39,7 @@ def __init__(self): self.key_mapping = None @staticmethod - def get_requirements() -> list[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. diff --git a/src/modules/applications/optimization/SAT/mappings/Direct.py b/src/modules/applications/optimization/SAT/mappings/Direct.py index dee6f11c..0b57b608 100644 --- a/src/modules/applications/optimization/SAT/mappings/Direct.py +++ b/src/modules/applications/optimization/SAT/mappings/Direct.py @@ -37,7 +37,7 @@ def __init__(self): self.submodule_options = ["ClassicalSAT", "RandomSAT"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() ->list[dict]: """ Return requirements of this module. diff --git a/src/modules/circuits/Circuit.py b/src/modules/circuits/Circuit.py index 579a7312..d815318f 100644 --- a/src/modules/circuits/Circuit.py +++ b/src/modules/circuits/Circuit.py @@ -13,7 +13,6 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Any, Dict, Tuple from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -33,7 +32,7 @@ def __init__(self, name: str): self.architecture_name = name @abstractmethod - def generate_gate_sequence(self, input_data: Dict, config: Any) -> Dict: + def generate_gate_sequence(self, input_data: dict, config: any) -> dict: """ Generates the library agnostic gate sequence, a well-defined definition of the quantum circuit. @@ -43,7 +42,7 @@ def generate_gate_sequence(self, input_data: Dict, config: Any) -> Dict: """ pass - def preprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, float]: + def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ Library-agnostic implementation of the gate sequence, that will be mapped to backend such as Qiskit in the subsequent module. @@ -62,13 +61,13 @@ def preprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, fl return circuit_constr, end_time_measurement(start) - def postprocess(self, input_data: Dict, config: Dict, **kwargs) -> Tuple[Dict, float]: + def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ Method that passes back information of the subsequent modules to the preceding modules. :param input_data: Collected information of the benchmarking process :param config: Config specifying the number of qubits of the circuit - :param kwargs: optional keyword arguments + :param kwargs: Optional keyword arguments :return: Same dictionary like input_data with architecture_name """ start = start_time_measurement() diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/circuits/CircuitCardinality.py index e0a2e8ec..37a78de8 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/circuits/CircuitCardinality.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, TypedDict, Any, Tuple, Dict +from typing import Union, TypedDict from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit @@ -30,7 +30,7 @@ class CircuitCardinality(Circuit): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("CircuitCardinality") self.submodule_options = [ @@ -40,20 +40,19 @@ def __init__(self): "PresetQiskitNoisyBackend" ] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this circuit. :return: Dictionary with parameter options - .. code-block:: python - - return { - "depth": { - "values": [2, 4, 8, 16], - "description": "What depth do you want?" - } - } + .. code-block:: python + return { + "depth": { + "values": [2, 4, 8, 16], + "description": "What depth do you want?" + } + } """ return { @@ -79,7 +78,7 @@ def get_default_submodule( class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -88,7 +87,7 @@ class Config(TypedDict): """ depth: int - def generate_gate_sequence(self, input_data: Dict, config: Config) -> Dict: + def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ Returns gate sequence of cardinality circuit architecture. diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/circuits/CircuitCopula.py index f2b9e157..efdbe2ea 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/circuits/CircuitCopula.py @@ -12,9 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, TypedDict, Any, Dict, Tuple, List +from typing import Union, TypedDict from itertools import combinations - from scipy.special import binom from modules.circuits.Circuit import Circuit @@ -33,14 +32,15 @@ class CircuitCopula(Circuit): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("DiscreteCopula") self.submodule_options = [ "LibraryQiskit", "LibraryPennylane", "CustomQiskitNoisyBackend", - "PresetQiskitNoisyBackend"] + "PresetQiskitNoisyBackend" + ] @staticmethod def get_requirements() -> list[dict]: @@ -51,7 +51,7 @@ def get_requirements() -> list[dict]: """ return [{"name": "scipy", "version": "1.12.0"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this Copula Circuit. @@ -105,7 +105,7 @@ class Config(TypedDict): """ depth: int - def generate_gate_sequence(self, input_data: Dict, config: Config) -> Dict: + def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ Returns gate sequence of copula architecture. diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/circuits/CircuitStandard.py index 260d1265..876f39a4 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/circuits/CircuitStandard.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union, TypedDict, Any, List, Tuple, Dict +from typing import Union, TypedDict from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit @@ -40,15 +40,15 @@ def __init__(self): ] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this standard circuit. @@ -102,7 +102,7 @@ class Config(TypedDict): """ depth: int - def generate_gate_sequence(self, input_data: Dict, config: Config) -> Dict: + def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ Returns gate sequence of standard architecture. diff --git a/src/modules/devices/Device.py b/src/modules/devices/Device.py index 9c7d9357..a7d1b76e 100644 --- a/src/modules/devices/Device.py +++ b/src/modules/devices/Device.py @@ -13,7 +13,6 @@ # limitations under the License. from abc import ABC -from typing import Dict, Any, Tuple from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -34,7 +33,7 @@ def __init__(self, device_name: str): self.config = None self.device_name = self.name - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the parameters to fine-tune the device. @@ -60,7 +59,7 @@ def set_config(self, config): """ self.config = config - def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Returns instance of device class (self) and time it takes to call config. @@ -73,12 +72,12 @@ def preprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, floa self.config = config return self, end_time_measurement(start) - def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Returns input data and adds device name to the metrics class instance. :param input_data: Input data passed by the parent module - :param config: solver config + :param config: Solver config :param kwargs: Optional keyword arguments :return: Output and time needed """ @@ -86,7 +85,7 @@ def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, flo self.metrics.add_metric("device", self.get_device_name()) return input_data, end_time_measurement(start) - def get_device(self) -> Any: + def get_device(self) -> any: """ Returns device. diff --git a/src/modules/devices/HelperClass.py b/src/modules/devices/HelperClass.py index 128f759d..7cd536f0 100644 --- a/src/modules/devices/HelperClass.py +++ b/src/modules/devices/HelperClass.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict from modules.devices.Device import Device from modules.Core import Core @@ -34,7 +33,7 @@ def __init__(self, device_name: str): self.device = device_name self.submodule_options = [] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/Local.py b/src/modules/devices/Local.py index d5f7dbc2..1f4ccd4a 100644 --- a/src/modules/devices/Local.py +++ b/src/modules/devices/Local.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict from modules.devices.Device import Device from modules.Core import Core @@ -31,7 +30,7 @@ def __init__(self): self.device = None self.submodule_options = [] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/SimulatedAnnealingSampler.py b/src/modules/devices/SimulatedAnnealingSampler.py index 59677e28..95091eb4 100644 --- a/src/modules/devices/SimulatedAnnealingSampler.py +++ b/src/modules/devices/SimulatedAnnealingSampler.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict import dwave.samplers from modules.devices.Device import Device @@ -33,7 +32,7 @@ def __init__(self): self.submodule_options = [] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -41,7 +40,7 @@ def get_requirements() -> List[Dict]: """ return [{"name": "dwave-samplers", "version": "1.3.0"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index 0aa32579..d146b177 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -17,7 +17,6 @@ import os from abc import ABC from datetime import datetime -from typing import List, Dict, Tuple import boto3 from botocore.config import Config @@ -45,7 +44,7 @@ def __init__(self, device_name: str, region: str = None, arn: str = None): # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return - + if device_name != "LocalSimulator": self._configure_aws_session(region) @@ -133,7 +132,7 @@ def _initialize_aws_session(self, profile_name: str, region: str, my_config: Con raise Exception("Please refer to the logged error message.") from exc @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. diff --git a/src/modules/devices/braket/Ionq.py b/src/modules/devices/braket/Ionq.py index 0113ee9b..cb79d3f7 100644 --- a/src/modules/devices/braket/Ionq.py +++ b/src/modules/devices/braket/Ionq.py @@ -13,7 +13,6 @@ # limitations under the License. import os -from typing import Dict from braket.aws import AwsDevice from modules.devices.braket.Braket import Braket @@ -22,12 +21,12 @@ class Ionq(Braket): """ - Class for using the IonQ devices on Amazon Braket + Class for using the IonQ devices on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-east-1::device/qpu/ionq/Harmony'): """ - Constructor method for initializing IonQ device on Amazon Braket + Constructor method for initializing IonQ device on Amazon Braket. """ super().__init__(region="us-east-1", device_name=device_name, arn=arn) self.submodule_options = [] @@ -40,7 +39,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-east-1::devic self.init_s3_storage("ionq") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/braket/LocalSimulator.py b/src/modules/devices/braket/LocalSimulator.py index fb035edb..741d6d5a 100644 --- a/src/modules/devices/braket/LocalSimulator.py +++ b/src/modules/devices/braket/LocalSimulator.py @@ -13,7 +13,6 @@ # limitations under the License. from braket.devices import LocalSimulator as LocalSimulatorBraket -from typing import Dict from modules.devices.braket.Braket import Braket from modules.Core import Core @@ -34,7 +33,7 @@ def __init__(self, device_name: str): self.device = LocalSimulatorBraket() self.submodule_options = [] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/braket/OQC.py b/src/modules/devices/braket/OQC.py index 01277dd0..75330db1 100644 --- a/src/modules/devices/braket/OQC.py +++ b/src/modules/devices/braket/OQC.py @@ -14,7 +14,6 @@ import os from braket.aws import AwsDevice -from typing import Dict from modules.devices.braket.Braket import Braket from modules.Core import Core @@ -40,7 +39,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:eu-west-2::devic self.init_s3_storage("oqc") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns an empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/braket/Rigetti.py b/src/modules/devices/braket/Rigetti.py index b2842107..59d8143d 100644 --- a/src/modules/devices/braket/Rigetti.py +++ b/src/modules/devices/braket/Rigetti.py @@ -13,7 +13,6 @@ # limitations under the License. import os -from typing import Dict from braket.aws import AwsDevice from modules.devices.braket.Braket import Braket @@ -40,7 +39,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-west-1::devic self.init_s3_storage("rigetti") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns an empty dictionary as this solver has no configurable settings. diff --git a/src/modules/devices/braket/SV1.py b/src/modules/devices/braket/SV1.py index 96e21792..2f117d31 100644 --- a/src/modules/devices/braket/SV1.py +++ b/src/modules/devices/braket/SV1.py @@ -13,7 +13,6 @@ # limitations under the License. import os -from typing import Dict from braket.aws import AwsDevice from modules.devices.braket.Braket import Braket @@ -40,7 +39,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum self.init_s3_storage("sv1") self.device = AwsDevice(arn, aws_session=self.aws_session) - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dicionary as this solver has no configurable settings. diff --git a/src/modules/devices/braket/TN1.py b/src/modules/devices/braket/TN1.py index 21b6c4bd..9dd41e39 100644 --- a/src/modules/devices/braket/TN1.py +++ b/src/modules/devices/braket/TN1.py @@ -13,7 +13,6 @@ # limitations under the License. import os -from typing import Dict from braket.aws import AwsDevice from modules.devices.braket.Braket import Braket @@ -45,14 +44,11 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} def get_default_submodule(self, option: str) -> Core: """ diff --git a/src/modules/devices/pulser/MockNeutralAtomDevice.py b/src/modules/devices/pulser/MockNeutralAtomDevice.py index cd00fbcf..b89cae0b 100644 --- a/src/modules/devices/pulser/MockNeutralAtomDevice.py +++ b/src/modules/devices/pulser/MockNeutralAtomDevice.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Dict +from typing import TypedDict import pulser from pulser.devices import MockDevice @@ -36,7 +36,7 @@ def __init__(self): self.backend = QutipBackend self.submodule_options = [] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this application. @@ -74,7 +74,7 @@ def get_backend_config(self) -> pulser.backend.config.EmulatorConfig: """ Returns backend configurations. - :return: backend config for the emulator + :return: Backend config for the emulator """ noise_types = [key for key, value in self.config.items() if value] noise_model = pulser.backend.noise_model.NoiseModel(noise_types=noise_types) diff --git a/src/modules/devices/pulser/Pulser.py b/src/modules/devices/pulser/Pulser.py index 87e100c9..46de8f75 100644 --- a/src/modules/devices/pulser/Pulser.py +++ b/src/modules/devices/pulser/Pulser.py @@ -13,7 +13,6 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Any, List, Dict from modules.devices.Device import Device @@ -33,7 +32,7 @@ def __init__(self, device_name: str): self.device = None self.backend = None - def get_backend(self) -> Any: + def get_backend(self) -> any: """ Returns backend. @@ -42,7 +41,7 @@ def get_backend(self) -> Any: return self.backend @abstractmethod - def get_backend_config(self) -> Any: + def get_backend_config(self) -> any: """ Returns backend configurations. @@ -51,10 +50,10 @@ def get_backend_config(self) -> Any: pass @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "pulser","version": "0.19.0"}] diff --git a/src/modules/solvers/Annealer.py b/src/modules/solvers/Annealer.py index 824f4a39..6ab5a5e4 100644 --- a/src/modules/solvers/Annealer.py +++ b/src/modules/solvers/Annealer.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Dict, Any, Tuple +from typing import TypedDict import logging from modules.solvers.Solver import Solver @@ -45,7 +45,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this solver. @@ -78,11 +78,11 @@ class Config(TypedDict): """ number_of_reads: int - def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[dict, float]: + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: """ Annealing Solver. - :param mapped_problem: dictionary with the key 'Q' where its value should be the QUBO + :param mapped_problem: Dictionary with the key 'Q' where its value should be the QUBO :param device_wrapper: Annealing device :param config: Annealing settings :param kwargs: Additional keyword arguments @@ -103,7 +103,7 @@ def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwarg response = device.sample_qubo(Q, num_reads=config['number_of_reads']) time_to_solve = end_time_measurement(start) - # take the result with the lowest energy: + # Take the result with the lowest energy: sample = response.lowest().first.sample logging.info(f'Annealing finished in {time_to_solve} ms.') diff --git a/src/modules/solvers/ClassicalSAT.py b/src/modules/solvers/ClassicalSAT.py index dcbe71ec..91ffa7b2 100644 --- a/src/modules/solvers/ClassicalSAT.py +++ b/src/modules/solvers/ClassicalSAT.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import logging from pysat.examples.rc2 import RC2 @@ -36,11 +36,11 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "python-sat", "version": "1.8.dev13"}] @@ -57,7 +57,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. @@ -71,7 +71,7 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: WCNF, device_wrapper: Any, config: Any, **kwargs: Dict) -> Tuple[List, float]: + def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: dict) -> tuple[list, float]: """ The given application is a problem instance from the pysat library. This uses the rc2 maxsat solver given in that library to return a solution. @@ -79,7 +79,7 @@ def run(self, mapped_problem: WCNF, device_wrapper: Any, config: Any, **kwargs: :param mapped_problem: Problem instance from the pysat library :param device_wrapper: Local device :param config: Empty dict - :param kwargs: no additionally settings needed + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ diff --git a/src/modules/solvers/GreedyClassicalPVC.py b/src/modules/solvers/GreedyClassicalPVC.py index 839ee542..7446babc 100644 --- a/src/modules/solvers/GreedyClassicalPVC.py +++ b/src/modules/solvers/GreedyClassicalPVC.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import networkx as nx @@ -34,7 +34,7 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -55,7 +55,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. @@ -69,17 +69,16 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Any, **kwargs: Dict) -> Tuple[Dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) -> tuple[dict, float]: """ Solve the PVC graph in a greedy fashion. - :param mapped_problem: graph representing a PVC problem + :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device :param config: Empty dict :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() diff --git a/src/modules/solvers/GreedyClassicalTSP.py b/src/modules/solvers/GreedyClassicalTSP.py index 6d5cbbb0..0e64d47e 100644 --- a/src/modules/solvers/GreedyClassicalTSP.py +++ b/src/modules/solvers/GreedyClassicalTSP.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import networkx as nx from networkx.algorithms import approximation as approx @@ -35,11 +35,11 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "networkx", "version": "3.2.1"}] @@ -56,7 +56,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. @@ -70,17 +70,16 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Any, **kwargs: Dict) ->Tuple[Dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) ->tuple[dict, float]: """ Solve the TSP graph in a greedy fashion. - :param mapped_problem: graph representing a TSP + :param mapped_problem: Graph representing a TSP :param device_wrapper: Local device :param config: Empty dict :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() diff --git a/src/modules/solvers/MIPsolverACL.py b/src/modules/solvers/MIPsolverACL.py index fc6f3356..54dccf9f 100644 --- a/src/modules/solvers/MIPsolverACL.py +++ b/src/modules/solvers/MIPsolverACL.py @@ -27,7 +27,7 @@ # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import pulp from modules.solvers.Solver import Solver @@ -48,7 +48,7 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -69,7 +69,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. @@ -83,7 +83,7 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: Dict, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: """ Solve the ACL problem as a mixed integer problem (MIP). diff --git a/src/modules/solvers/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py index 60f5fc0d..eb88f639 100644 --- a/src/modules/solvers/NeutralAtomMIS.py +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Any, Dict, List, Tuple +from typing import TypedDict import logging import numpy as np @@ -36,7 +36,7 @@ def __init__(self): self.submodule_options = ["MockNeutralAtomDevice"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -57,7 +57,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this solver. @@ -81,7 +81,7 @@ class Config(TypedDict): """ samples: int - def run(self, mapped_problem: Dict, device_wrapper: Any, config: Any, **kwargs: Dict) -> Tuple[List, float, Dict]: + def run(self, mapped_problem: dict, device_wrapper: any, config: any, **kwargs: dict) -> tuple[list, float, dict]: """ The given application is a problem instance from the pysat library. This uses the rc2 maxsat solver given in that library to return a solution. @@ -137,7 +137,7 @@ def _create_sequence(self, register:pulser.Register, device:pulser.devices._devi sequence.add(pulse, "Rydberg global") return sequence - def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> List[pulser.Pulse]: + def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: """ Creates pulses tuned to MIS problem. @@ -181,7 +181,7 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> List[p return pulses - def _filter_invalid_states(self, state_counts:Dict, nodes:List, edges:List) -> Dict: + def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> dict: """ Filters out invalid states that do not meet the problem constraints. @@ -204,7 +204,7 @@ def _filter_invalid_states(self, state_counts:Dict, nodes:List, edges:List) -> D return valid_state_counts - def _translate_state_to_nodes(self, state:str, nodes:List) -> List: + def _translate_state_to_nodes(self, state:str, nodes:list) -> list: """ Translates a state string into the corresponding list of nodes. @@ -214,7 +214,7 @@ def _translate_state_to_nodes(self, state:str, nodes:List) -> List: """ return [key for index, key in enumerate(nodes) if state[index] == '1'] - def _select_best_state(self, states:Dict, nodes:List) -> str: + def _select_best_state(self, states:dict, nodes:list) -> str: """ Selects the best state from the available valid states. @@ -230,4 +230,5 @@ def _select_best_state(self, states:Dict, nodes:List) -> str: # TODO: Clean up this monstrocity n_nodes = len(nodes) best_state = "0" * n_nodes + return best_state diff --git a/src/modules/solvers/PennylaneQAOA.py b/src/modules/solvers/PennylaneQAOA.py index 3fd45a3d..d7820061 100644 --- a/src/modules/solvers/PennylaneQAOA.py +++ b/src/modules/solvers/PennylaneQAOA.py @@ -20,7 +20,7 @@ from collections import Counter from functools import partial, wraps from time import time -from typing import TypedDict, Dict, Any, List, Tuple +from typing import TypedDict import matplotlib.pyplot as plt import numpy as np @@ -57,7 +57,7 @@ def __init__(self): ] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -114,9 +114,9 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this solver + Returns the configurable settings for this solver. :return: Dictionary of configuration settings .. code-block:: python @@ -190,7 +190,7 @@ class Config(TypedDict): stepsize: float @staticmethod - def normalize_data(data: Any, scale: float = 1.0) -> Any: + def normalize_data(data: any, scale: float = 1.0) -> any: """ Not used currently, as I just scale the coefficients in the qaoa_operators_from_ising. @@ -201,7 +201,7 @@ def normalize_data(data: Any, scale: float = 1.0) -> Any: return scale * data / np.max(np.abs(data)) @staticmethod - def qaoa_operators_from_ising(J: Any, t: Any, scale: float = 1.0) -> Tuple[Any, Any]: + def qaoa_operators_from_ising(J: any, t: any, scale: float = 1.0) -> tuple[any, any]: """ Generates pennylane cost and mixer hamiltonians from the Ising matrix J and vector t. @@ -209,7 +209,6 @@ def qaoa_operators_from_ising(J: Any, t: Any, scale: float = 1.0) -> Tuple[Any, :param t: t vector :param scale: Scaling factor :return: Cost Hamiltonian and mixer Hamiltonian - :rtype: tuple(any, any) """ # Define the scaling factor scaling_factor = scale * max(np.max(np.abs(J.flatten())), np.max(np.abs(t))) @@ -233,14 +232,14 @@ def qaoa_operators_from_ising(J: Any, t: Any, scale: float = 1.0) -> Tuple[Any, return h_cost, h_mixer # pylint: disable=R0915 - def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Any, Any, float]: + def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, any, float]: """ Runs Pennylane QAOA on the Ising problem. :param mapped_problem: Ising :param device_wrapper: Device to run the problem on :param config: QAOA solver settings - :param kwargs: contains store_dir for the plot of the optimization + :param kwargs: Contains store_dir for the plot of the optimization :return: Solution, the time it took to compute it and optional additional information """ J = mapped_problem['J'] @@ -455,9 +454,7 @@ def evaluate_params_probs(params): def monkey_init_array(self): """ - Here we create the timings array where we later append the quantum timings - :param self: - :return: + Here we create the timings array where we later append the quantum timings. """ self.timings = [] @@ -465,17 +462,17 @@ def monkey_init_array(self): def _pseudo_decor(fun, device): """ Massive shoutout to this guy: https://stackoverflow.com/a/25827070/10456906 - We use this decorator for measuring execute and batch_execute + We use this decorator for measuring execute and batch_execute. """ - # magic sauce to lift the name and doc of the function + # Magic sauce to lift the name and doc of the function @wraps(fun) def ret_fun(*args, **kwargs): - # pre function execution stuff here + # Pre function execution stuff here from time import time # pylint: disable=W0621 disable=C0415 disable=W0404 start_timing = time() * 1000 returned_value = fun(*args, **kwargs) - # post execution stuff here + # Post execution stuff here device.timings.append(round(time() * 1000 - start_timing, 3)) return returned_value diff --git a/src/modules/solvers/QAOA.py b/src/modules/solvers/QAOA.py index 2a65cfa7..c8a343c4 100644 --- a/src/modules/solvers/QAOA.py +++ b/src/modules/solvers/QAOA.py @@ -13,7 +13,7 @@ # limitations under the License. from time import sleep -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import logging import numpy as np @@ -43,7 +43,7 @@ def __init__(self): ] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -81,7 +81,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this solver. @@ -134,14 +134,14 @@ class Config(TypedDict): opt_method: str depth: int - def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Any, float]: + def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float]: """ Run QAOA algorithm on Ising. :param mapped_problem: dictionary with the keys 'J' and 't' :param device_wrapper: Instance of device :param config: Solver configuration settings - :param kwargs: no additionally settings needed + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ j = mapped_problem['J'] @@ -151,18 +151,18 @@ def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs else: j = np.real(j) - # set up the problem + # Set up the problem n_qubits = j.shape[0] # User-defined hypers depth = config['depth'] opt_method = config['opt_method'] # SLSQP, COBYLA, Nelder-Mead, BFGS, Powell, ... - # initialize reference solution (simple guess) + # Initialize reference solution (simple guess) bitstring_init = -1 * np.ones([n_qubits]) energy_init = np.dot(bitstring_init, np.dot(j, bitstring_init)) - # set tracker to keep track of results + # Set tracker to keep track of results tracker = { 'count': 1, # Elapsed optimization steps 'optimal_energy': energy_init, # Global optimal energy @@ -175,18 +175,18 @@ def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs 'params': [] # Track parameters } - # set options for classical optimization + # Set options for classical optimization options = {'disp': True, 'maxiter': 100} # options = {'disp': True, 'ftol': 1e-08, 'maxiter': 100, 'maxfev': 50} # example options ################################################################################## - # run QAOA optimization on graph + # Run QAOA optimization on graph ################################################################################## logging.info(f"Circuit depth hyperparameter:{depth}") logging.info(f"Problem size:{n_qubits}") - # kick off training + # Kick off training start = start_time_measurement() _, _, tracker = train( device=device_wrapper.get_device(), @@ -217,30 +217,29 @@ def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs # QAOA utils (source: # https://github.com/aws/amazon-braket-examples/blob/main/examples/hybrid_quantum_algorithms/QAOA/utils_qaoa.py) -# function to implement ZZ gate using CNOT gates +# Function to implement ZZ gate using CNOT gates def ZZgate(q1, q2, gamma): """ - function that returns a circuit implementing exp(-i \\gamma Z_i Z_j) using CNOT gates if ZZ not supported + function that returns a circuit implementing exp(-i \\gamma Z_i Z_j) using CNOT gates if ZZ not supported. """ - - # get a circuit + # Get a circuit circ_zz = Circuit() - # construct decomposition of ZZ + # Construct decomposition of ZZ circ_zz.cnot(q1, q2).rz(q2, gamma).cnot(q1, q2) return circ_zz -# function to implement evolution with driver Hamiltonian +# Function to implement evolution with driver Hamiltonian def driver(beta, n_qubits): """ - Returns circuit for driver Hamiltonian U(Hb, beta) + Returns circuit for driver Hamiltonian U(Hb, beta). """ - # instantiate circuit object + # Instantiate circuit object circ = Circuit() - # apply parametrized rotation around x to every qubit + # Apply parametrized rotation around x to every qubit for qubit in range(n_qubits): gate = Circuit().rx(qubit, 2 * beta) circ.add(gate) @@ -248,26 +247,26 @@ def driver(beta, n_qubits): return circ -# helper function for evolution with cost Hamiltonian +# Helper function for evolution with cost Hamiltonian def cost_circuit(gamma, n_qubits, ising, device): """ - returns circuit for evolution with cost Hamiltonian + returns circuit for evolution with cost Hamiltonian. """ - # instantiate circuit object + # Instantiate circuit object circ = Circuit() - # get all non-zero entries (edges) from Ising matrix + # Get all non-zero entries (edges) from Ising matrix idx = ising.nonzero() edges = list(zip(idx[0], idx[1])) - # apply ZZ gate for every edge (with corresponding interaction strength) + # Apply ZZ gate for every edge (with corresponding interaction strength) for qubit_pair in edges: - # get interaction strength from Ising matrix + # Get interaction strength from Ising matrix int_strength = ising[qubit_pair[0], qubit_pair[1]] - # for Rigetti we decompose ZZ using CNOT gates + # For Rigetti we decompose ZZ using CNOT gates if device.name in ["Rigetti", "Aspen-9"]: # TODO make this more flexible gate = ZZgate(qubit_pair[0], qubit_pair[1], gamma * int_strength) - # classical simulators and IonQ support ZZ gate + # Classical simulators and IonQ support ZZ gate else: gate = Circuit().zz(qubit_pair[0], qubit_pair[1], angle=2 * gamma * int_strength) circ.add(gate) @@ -275,25 +274,25 @@ def cost_circuit(gamma, n_qubits, ising, device): return circ -# function to build the QAOA circuit with depth p +# Function to build the QAOA circuit with depth p def circuit(params, device, n_qubits, ising): """ - function to return full QAOA circuit; depends on device as ZZ implementation depends on gate set of backend + function to return full QAOA circuit; depends on device as ZZ implementation depends on gate set of backend. """ - # initialize qaoa circuit with first Hadamard layer: for minimization start in |-> + # Initialize qaoa circuit with first Hadamard layer: for minimization start in |-> circ = Circuit() X_on_all = Circuit().x(range(0, n_qubits)) circ.add(X_on_all) H_on_all = Circuit().h(range(0, n_qubits)) circ.add(H_on_all) - # setup two parameter families + # Setup two parameter families circuit_length = int(len(params) / 2) gammas = params[:circuit_length] betas = params[circuit_length:] - # add QAOA circuit layer blocks + # Add QAOA circuit layer blocks for mm in range(circuit_length): circ.add(cost_circuit(gammas[mm], n_qubits, ising, device)) circ.add(driver(betas[mm], n_qubits)) @@ -301,67 +300,67 @@ def circuit(params, device, n_qubits, ising): return circ -# function that computes cost function for given params +# Function that computes cost function for given params # pylint: disable=R0917 # pylint: disable=R0913 def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_folder, verbose): """ objective function takes a list of variational parameters as input, - and returns the cost associated with those parameters + and returns the cost associated with those parameters. """ if verbose: logging.info("==================================" * 2) logging.info(f"Calling the quantum circuit. Cycle: {tracker['count']}") - # get a quantum circuit instance from the parameters + # Get a quantum circuit instance from the parameters qaoa_circuit = circuit(params, device, n_qubits, ising) - # classically simulate the circuit - # execute the correct device.run call depending on whether the backend is local or cloud based + # Classically simulate the circuit + # Execute the correct device.run call depending on whether the backend is local or cloud based if device.name in ["DefaultSimulator", "StateVectorSimulator"]: task = device.run(qaoa_circuit, shots=n_shots) else: task = device.run(qaoa_circuit, s3_folder, shots=n_shots, poll_timeout_seconds=3 * 24 * 60 * 60) - # get ID and status of submitted task + # Get ID and status of submitted task task_id = task.id status = task.state() logging.info(f"ID of task: {task_id}") logging.info(f"Status of task: {status}") - # wait for job to complete + # Wait for job to complete while status != 'COMPLETED': status = task.state() logging.info(f"Status: {status}") sleep(10) - # get result for this task + # Get result for this task result = task.result() logging.info(result) - # convert results (0 and 1) to ising (-1 and 1) + # Convert results (0 and 1) to ising (-1 and 1) meas_ising = result.measurements meas_ising[meas_ising == 0] = -1 - # get all energies (for every shot): (n_shots, 1) vector + # Get all energies (for every shot): (n_shots, 1) vector all_energies = np.diag(np.dot(meas_ising, np.dot(ising, np.transpose(meas_ising)))) - # find minimum and corresponding classical string + # Find minimum and corresponding classical string energy_min = np.min(all_energies) tracker["opt_energies"].append(energy_min) optimal_string = meas_ising[np.argmin(all_energies)] tracker["opt_bitstrings"].append(optimal_string) logging.info(tracker["optimal_energy"]) - # store optimal (classical) result/bitstring + # Store optimal (classical) result/bitstring if energy_min < tracker["optimal_energy"]: tracker.update({"optimal_energy": energy_min, "optimal_bitstring": optimal_string}) - # store global minimum + # Store global minimum tracker["global_energies"].append(tracker["optimal_energy"]) - # energy expectation value + # Energy expectation value energy_expect = np.sum(all_energies) / n_shots if verbose: @@ -369,7 +368,7 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol logging.info(f"Optimal classical string: {optimal_string}") logging.info(f"Energy expectation value (cost): {energy_expect}") - # update tracker + # Update tracker tracker.update({"count": tracker["count"] + 1, "res": result}) tracker["costs"].append(energy_expect) tracker["params"].append(params) @@ -381,7 +380,7 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol # pylint: disable=R0917 def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_folder, verbose=True): """ - function to run QAOA algorithm for given, fixed circuit depth p + function to run QAOA algorithm for given, fixed circuit depth p. """ logging.info("Starting the training.") logging.info("==================================" * 2) @@ -391,22 +390,22 @@ def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_ logging.info('Param "verbose" set to False. Will not print intermediate steps.') logging.info("==================================" * 2) - # initialize + # Initialize cost_energy = [] - # randomly initialize variational parameters within appropriate bounds + # Randomly initialize variational parameters within appropriate bounds gamma_initial = np.random.uniform(0, 2 * np.pi, p).tolist() beta_initial = np.random.uniform(0, np.pi, p).tolist() params0 = np.array(gamma_initial + beta_initial) - # set bounds for search space + # Set bounds for search space bnds_gamma = [(0, 2 * np.pi) for _ in range(int(len(params0) / 2))] bnds_beta = [(0, np.pi) for _ in range(int(len(params0) / 2))] bnds = bnds_gamma + bnds_beta tracker["params"].append(params0) - # run classical optimization (example: method='Nelder-Mead') + # Run classical optimization (example: method='Nelder-Mead') try: result = minimize( objective_function, @@ -421,7 +420,7 @@ def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_ logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") from e - # store result of classical optimization + # Store result of classical optimization result_energy = result.fun cost_energy.append(result_energy) logging.info(f"Final average energy (cost): {result_energy}") diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index c1ad4aaa..5db02676 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import Tuple, TypedDict, Dict, Any, List +from typing import TypedDict import numpy as np @@ -43,11 +43,11 @@ def __init__(self): self.ry = None @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "qiskit", "version": "1.1.0"}, @@ -69,7 +69,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns the configurable settings for this solver. @@ -151,7 +151,7 @@ class Config(TypedDict): method: str @staticmethod - def normalize_data(data: Any, scale: float = 1.0) -> Any: + def normalize_data(data: any, scale: float = 1.0) -> any: """ Not used currently, as I just scale the coefficients in the qaoa_operators_from_ising. @@ -161,14 +161,14 @@ def normalize_data(data: Any, scale: float = 1.0) -> Any: """ return scale * data / np.max(np.abs(data)) - def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Any, float]: + def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float]: """ Run Qiskit QAOA algorithm on Ising. - :param mapped_problem: dictionary with the keys 'J' and 't' + :param mapped_problem: Dictionary with the keys 'J' and 't' :param device_wrapper: Instance of device :param config: Config object for the solver - :param kwargs: no additionally settings needed + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ J = mapped_problem['J'] @@ -198,7 +198,7 @@ def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs logging.warning("No method selected in QiskitQAOA. Continue with NumPyMinimumEigensolver.") algorithm = NumPyMinimumEigensolver() - # run actual optimization algorithm + # Run actual optimization algorithm try: result = algorithm.compute_minimum_eigenvalue(ising_op) except ValueError as e: @@ -209,7 +209,7 @@ def run(self, mapped_problem: Any, device_wrapper: Any, config: Config, **kwargs best_bitstring = self._get_best_solution(result) return best_bitstring, end_time_measurement(start), {} - def _get_best_solution(self, result) -> Any: + def _get_best_solution(self, result) -> any: """ Gets the best solution from the result. @@ -237,9 +237,9 @@ def _get_best_solution(self, result) -> Any: return best_bitstring @staticmethod - def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> SparsePauliOp: + def _get_pauli_op(ising: tuple[np.ndarray, np.ndarray]) -> SparsePauliOp: """ - Creates a Pauli operator from the given Ising model representation + Creates a Pauli operator from the given Ising model representation. :param ising: Tuple with linear and quandratic terms .return: SparsePauliOp representing the Ising model @@ -247,7 +247,7 @@ def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> SparsePauliOp: pauli_list = [] number_qubits = len(ising[0]) - # linear terms + # Linear terms it = np.nditer(ising[0], flags=['multi_index']) for x in it: logging.debug(f"{x},{it.multi_index}") diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index 788ce1f0..1c1c232f 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Any, Tuple +from typing import TypedDict import random import networkx as nx @@ -34,11 +34,11 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "networkx", "version": "3.2.1"}] @@ -55,7 +55,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dictionary as this solver has no configurable settings. @@ -69,7 +69,7 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: """ Solve the PVC graph in a greedy fashion. @@ -79,7 +79,6 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Deep copy since we are modifying the graph. This ensures that the original graph remains unchanges # with a different graph mapped_problem = mapped_problem.copy() diff --git a/src/modules/solvers/RandomClassicalSAT.py b/src/modules/solvers/RandomClassicalSAT.py index 1acfa9b0..aa1bcd3e 100644 --- a/src/modules/solvers/RandomClassicalSAT.py +++ b/src/modules/solvers/RandomClassicalSAT.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Dict, Tuple, Any +from typing import TypedDict import logging import numpy as np @@ -31,17 +31,17 @@ class RandomSAT(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [ {"name": "python-sat", "version": "1.8.dev13"}, @@ -55,7 +55,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dict as this solver has no configurable settings. @@ -69,7 +69,7 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: WCNF, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[List, float]: + def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[list, float]: """ The given application is a problem instance from the pysat library. This generates a random solution to the problem. diff --git a/src/modules/solvers/RandomClassicalTSP.py b/src/modules/solvers/RandomClassicalTSP.py index eeae08a5..655d401d 100644 --- a/src/modules/solvers/RandomClassicalTSP.py +++ b/src/modules/solvers/RandomClassicalTSP.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, List, Any, Dict, Tuple +from typing import TypedDict import random import networkx as nx @@ -34,11 +34,11 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "networkx", "version": "3.2.1"}] @@ -49,11 +49,11 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dict as this solver has no configurable settings. - :return: empty dict + :return: Empty dict """ return {} @@ -63,7 +63,7 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: """ Solve the TSP graph in a greedy fashion. diff --git a/src/modules/solvers/ReverseGreedyClassicalPVC.py b/src/modules/solvers/ReverseGreedyClassicalPVC.py index 1dec4d78..52b6e327 100644 --- a/src/modules/solvers/ReverseGreedyClassicalPVC.py +++ b/src/modules/solvers/ReverseGreedyClassicalPVC.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Any, List, Dict, Tuple +from typing import TypedDict import networkx as nx @@ -47,15 +47,15 @@ def get_default_submodule(self, option: str) -> Core: raise NotImplementedError(f"Device Option {option} not implemented") @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. - :return: list of dict with requirements of this module + :return: List of dict with requirements of this module """ return [{"name": "networkx", "version": "3.2.1"}] - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dict as this solver has no configurable settings. @@ -65,21 +65,20 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Dict) -> Tuple[Dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: """ Solve the PVC graph in a greedy fashion. We take the worst choice at each step. - :param mapped_problem: graph representing a PVC problem + :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device :param config: Empty dicT :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Need to deep copy since we are modifying the graph in this function. # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() diff --git a/src/modules/solvers/ReverseGreedyClassicalTSP.py b/src/modules/solvers/ReverseGreedyClassicalTSP.py index 0b887cb8..bd7fa5f3 100644 --- a/src/modules/solvers/ReverseGreedyClassicalTSP.py +++ b/src/modules/solvers/ReverseGreedyClassicalTSP.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict, Dict, List, Any, Tuple +from typing import TypedDict import networkx as nx from networkx.algorithms import approximation as approx @@ -36,7 +36,7 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> List[Dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. @@ -57,7 +57,7 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Device Option {option} not implemented") - def get_parameter_options(self) -> Dict: + def get_parameter_options(self) -> dict: """ Returns empty dict as this solver has no configurable settings. @@ -67,11 +67,11 @@ def get_parameter_options(self) -> Dict: class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **kwargs: Any) -> Tuple[Dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: any) -> tuple[dict, float]: """ Solve the TSP graph in a greedy fashion. @@ -81,7 +81,6 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: Any, config: Config, **k :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Need to deep copy since we are modifying the graph in this function. # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() diff --git a/src/modules/solvers/Solver.py b/src/modules/solvers/Solver.py index e32bd374..e10dd890 100644 --- a/src/modules/solvers/Solver.py +++ b/src/modules/solvers/Solver.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict, Any, Tuple from abc import ABC, abstractmethod from modules.Core import Core @@ -23,7 +22,7 @@ class Solver(Core, ABC): defined objective function. """ - def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, float]: + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ The actual solving process is done here, as we have the device, which got provided by the device submodule, and the problem data provided by the parent module. @@ -38,7 +37,7 @@ def postprocess(self, input_data: Any, config: Dict, **kwargs) -> Tuple[Any, flo return output, elapsed_time @abstractmethod - def run(self, mapped_problem, device_wrapper, config, **kwargs) -> Tuple[Any, float, Dict]: + def run(self, mapped_problem, device_wrapper, config, **kwargs) -> tuple[any, float, dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. diff --git a/src/modules/training/QCBM.py b/src/modules/training/QCBM.py index 46cb7f77..b6ed5ce9 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/training/QCBM.py @@ -20,7 +20,7 @@ from matplotlib import figure, axes import matplotlib.pyplot as plt -from modules.training.Training import Training, Core +from modules.training.Training import Training, Core, GPU from utils_mpi import is_running_mpi, get_comm MPI = is_running_mpi() @@ -213,7 +213,7 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict input_data['MPI_size'] = size input_data["store_dir_iter"] += f"_{input_data['dataset_name']}_qubits{input_data['n_qubits']}" x0, options = self.setup_training(input_data, config) - + is_master = comm.Get_rank() == 0 if is_master: self.target = np.asarray(input_data["histogram_train"]) diff --git a/src/quark2_adapter/legacy_classes/Application.py b/src/quark2_adapter/legacy_classes/Application.py index ab5f3321..b1a422f4 100644 --- a/src/quark2_adapter/legacy_classes/Application.py +++ b/src/quark2_adapter/legacy_classes/Application.py @@ -32,11 +32,11 @@ def __init__(self, application_name: str): """ self.application_name = application_name self.application = None - self.mapping_options: list[str] = [] - self.sub_options: list[dict]= [] + self.mapping_options = [] + self.sub_options = [] self.problem = None - self.problems: dict = {} + self.problems = {} self.conf_idx = None super().__init__() diff --git a/src/quark2_adapter/legacy_classes/Device.py b/src/quark2_adapter/legacy_classes/Device.py index f5cf74da..7bc0d066 100644 --- a/src/quark2_adapter/legacy_classes/Device.py +++ b/src/quark2_adapter/legacy_classes/Device.py @@ -27,9 +27,9 @@ def __init__(self, device_name: str): :param device_name: Name of the device """ - self.device: any = None - self.device_name: str = device_name - self.config: dict = None + self.device = None + self.device_name = device_name + self.config = None def get_parameter_options(self) -> dict: """ diff --git a/src/quark2_adapter/legacy_classes/Mapping.py b/src/quark2_adapter/legacy_classes/Mapping.py index 456c6e91..05e0d716 100644 --- a/src/quark2_adapter/legacy_classes/Mapping.py +++ b/src/quark2_adapter/legacy_classes/Mapping.py @@ -26,8 +26,8 @@ def __init__(self): """ Constructor method. """ - self.solver_options: list[str] = [] - self.sub_options: list[dict] = None + self.solver_options = [] + self.sub_options = None super().__init__() @abstractmethod diff --git a/src/quark2_adapter/legacy_classes/Solver.py b/src/quark2_adapter/legacy_classes/Solver.py index 650ec4f4..d12da120 100644 --- a/src/quark2_adapter/legacy_classes/Solver.py +++ b/src/quark2_adapter/legacy_classes/Solver.py @@ -26,8 +26,8 @@ def __init__(self): """ Constructor method. """ - self.device_options: list[str] = [] - self.sub_options: list[dict] = None + self.device_options = [] + self.sub_options = None super().__init__() @abstractmethod From ef22df870b7ea5a7ea5dbdda191bb2e7f817e86f Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 8 Oct 2024 12:24:39 +0200 Subject: [PATCH 08/40] solved lint issue --- src/modules/Core.py | 4 ++-- .../QML/generative_modeling/mappings/Library.py | 2 +- .../generative_modeling/transformations/Transformation.py | 2 +- .../applications/optimization/ACL/mappings/QUBO.py | 3 ++- src/modules/applications/optimization/PVC/PVC.py | 8 +++++--- src/modules/devices/braket/Braket.py | 2 +- src/modules/devices/braket/Ionq.py | 2 +- src/modules/devices/braket/OQC.py | 2 +- src/modules/devices/braket/Rigetti.py | 2 +- src/modules/devices/braket/SV1.py | 2 +- src/modules/devices/braket/TN1.py | 2 +- src/modules/solvers/ReverseGreedyClassicalPVC.py | 2 +- src/modules/training/Inference.py | 6 +++--- 13 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/modules/Core.py b/src/modules/Core.py index f4bc5f00..7d5fb07f 100644 --- a/src/modules/Core.py +++ b/src/modules/Core.py @@ -93,8 +93,8 @@ def get_default_submodule(self, option: str) -> Core: def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - Essential method for the benchmarking process. This is always executed before traversing down to the next module, - passing the data returned by this function. + Essential method for the benchmarking process. This is always executed before traversing down + to the next module, passing the data returned by this function. :param input_data: Data for the module, comes from the parent module if that exists :param config: Config for the module diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index e622c2ed..01461702 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -91,7 +91,7 @@ def sequence_to_circuit(self, input_data: dict) -> dict: @staticmethod @abstractmethod - def get_execute_circuit(self, circuit: any, backend: any, config: str, config_dict: dict) -> tuple[any, any]: + def get_execute_circuit(circuit: any, backend: any, config: str, config_dict: dict) -> tuple[any, any]: """ This method combines the circuit implementation and the selected backend and returns a function that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py index 87393da3..ae64d05b 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py +++ b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py @@ -183,7 +183,7 @@ def generate_samples_efficient(results, bin_data: np.ndarray, n_registers: int, # Generate random noise or zeros noise = ( 0.5 * width * np.random.uniform(low=-1, high=1, size=(n_shots, n_registers)) - if noisy + if noisy else np.zeros((n_shots, n_registers)) ) diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index b293f8c6..228124fb 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -171,7 +171,8 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra for argument in penalty: if isinstance(argument, list): # squared variables in diagonals (x^2 == x) - if len(argument) == 2 and any(isinstance(elem, str) and variable in elem for elem in argument) and col == row: + if len(argument) == 2 and any(isinstance(elem, str) and variable in elem for elem in argument) \ + and col == row: parameter += argument[0] # Multiplication of different variables not on diagonal if len(argument) == 3 and variable in argument and variable2 in argument and variable > variable2: diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index df1b500e..79af8184 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -154,7 +154,7 @@ def generate_problem(self, config: Config) -> nx.Graph: unwanted_seams = seams_in_graph[-len(seams_in_graph) + seams:] unwanted_nodes = [x for x in graph.nodes if x[0] in unwanted_seams] - + for node in unwanted_nodes: graph.remove_node(node) @@ -177,7 +177,7 @@ def generate_problem(self, config: Config) -> nx.Graph: all_possible_edges = [ (edges[0], edges[1], t_start, t_end, c_start, c_end) for edges in all_possible_edges - for c_end in config + for c_end in config for c_start in config for t_end in tool for t_start in tool if edges[0] != edges[1] @@ -242,7 +242,9 @@ def process_solution(self, solution: dict) -> tuple[list, bool]: parsed_route = ' ->\n'.join( [ - f' Node {visit[0][1]} of Seam {visit[0][0]} using config {visit[1]} & tool {visit[2]}' for visit in route + f' Node {visit[0][1]} of Seam {visit[0][0]} using config ' + f' {visit[1]} & tool {visit[2]}' + for visit in route ] ) logging.info(f"Route found:\n{parsed_route}") diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index d146b177..680943d2 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -174,7 +174,7 @@ def _create_s3_bucket(boto3_session: boto3.Session, bucket_name: str = 'quark-be else: location = {"LocationConstraint": region} s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) - + s3_client.put_public_access_block( Bucket=bucket_name, PublicAccessBlockConfiguration={ diff --git a/src/modules/devices/braket/Ionq.py b/src/modules/devices/braket/Ionq.py index cb79d3f7..cab9f6a6 100644 --- a/src/modules/devices/braket/Ionq.py +++ b/src/modules/devices/braket/Ionq.py @@ -35,7 +35,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-east-1::devic # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return - + self.init_s3_storage("ionq") self.device = AwsDevice(arn, aws_session=self.aws_session) diff --git a/src/modules/devices/braket/OQC.py b/src/modules/devices/braket/OQC.py index 75330db1..1aeb3879 100644 --- a/src/modules/devices/braket/OQC.py +++ b/src/modules/devices/braket/OQC.py @@ -35,7 +35,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:eu-west-2::devic # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return - + self.init_s3_storage("oqc") self.device = AwsDevice(arn, aws_session=self.aws_session) diff --git a/src/modules/devices/braket/Rigetti.py b/src/modules/devices/braket/Rigetti.py index 59d8143d..f1276a30 100644 --- a/src/modules/devices/braket/Rigetti.py +++ b/src/modules/devices/braket/Rigetti.py @@ -35,7 +35,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-west-1::devic # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return - + self.init_s3_storage("rigetti") self.device = AwsDevice(arn, aws_session=self.aws_session) diff --git a/src/modules/devices/braket/SV1.py b/src/modules/devices/braket/SV1.py index 2f117d31..ff6b0e60 100644 --- a/src/modules/devices/braket/SV1.py +++ b/src/modules/devices/braket/SV1.py @@ -35,7 +35,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return - + self.init_s3_storage("sv1") self.device = AwsDevice(arn, aws_session=self.aws_session) diff --git a/src/modules/devices/braket/TN1.py b/src/modules/devices/braket/TN1.py index 9dd41e39..2877d5ef 100644 --- a/src/modules/devices/braket/TN1.py +++ b/src/modules/devices/braket/TN1.py @@ -38,7 +38,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return - + self.init_s3_storage("tn1") self.device = AwsDevice(arn, aws_session=self.aws_session) diff --git a/src/modules/solvers/ReverseGreedyClassicalPVC.py b/src/modules/solvers/ReverseGreedyClassicalPVC.py index 52b6e327..ac183163 100644 --- a/src/modules/solvers/ReverseGreedyClassicalPVC.py +++ b/src/modules/solvers/ReverseGreedyClassicalPVC.py @@ -79,7 +79,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **k :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Need to deep copy since we are modifying the graph in this function. + # Need to deep copy since we are modifying the graph in this function. # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() start = start_time_measurement() diff --git a/src/modules/training/Inference.py b/src/modules/training/Inference.py index e4a934bc..c5d97598 100644 --- a/src/modules/training/Inference.py +++ b/src/modules/training/Inference.py @@ -14,7 +14,7 @@ from typing import TypedDict import numpy as np -from modules.training.Training import Training, Core +from modules.training.Training import Training, Core, GPU class Inference(Training): @@ -98,8 +98,8 @@ def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> di pmfs, samples = execute_circuit([parameters.get() if GPU else parameters]) pmfs = np.asarray(pmfs) samples = ( - self.sample_from_pmf(pmf=pmfs[0], n_shots=input_data["n_shots"]) - if samples is None + self.sample_from_pmf(pmf=pmfs[0], n_shots=input_data["n_shots"]) + if samples is None else samples[0] ) From f755a83ae2aa1388f5a5d75fe762919a26712c2f Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 8 Oct 2024 13:10:50 +0200 Subject: [PATCH 09/40] solved lint issue --- .../mappings/CustomQiskitNoisyBackend.py | 2 +- .../applications/optimization/ACL/ACL.py | 6 +++--- .../optimization/ACL/mappings/QUBO.py | 19 +++++++++++++------ .../optimization/SAT/mappings/ChoiISING.py | 2 +- src/modules/devices/braket/Braket.py | 2 ++ .../legacy_classes/Application.py | 2 +- 6 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py index 8ac1ef78..84c1fe03 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -262,7 +262,7 @@ def select_backend(config: str, n_qubits: int) -> Backend: return backend - def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict + def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict # pylint: disable=W0221 ) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index 51319b37..fbda3d90 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -176,8 +176,8 @@ def _generate_tiny_model(self, df, vehicles): # Create empty lists for different vehicle parameters. This is required for proper indexing in the model weight_list = [0] * (len(vehicles)) - for i in range(len(vehicles)): - df_new = df.loc[df['Type'] == vehicles[i]] + for i, vehicle in enumerate(vehicles): + df_new = df.loc[df['Type'] == vehicle] weight_list[i] = int(df_new["Weight"].iloc[0]) # Set of available cars @@ -351,7 +351,7 @@ def _generate_small_model(self, df, vehicles): self.application = prob - def _generate_full_model(self, df, vehicles): + def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 """ Generate the problem model for the Full configuration. """ diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index 228124fb..e4ef2355 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -171,17 +171,24 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra for argument in penalty: if isinstance(argument, list): # squared variables in diagonals (x^2 == x) - if len(argument) == 2 and any(isinstance(elem, str) and variable in elem for elem in argument) \ - and col == row: - parameter += argument[0] + if ( + len(argument) == 2 + and any(isinstance(elem, str) and variable in elem for elem in argument) + and col == row + ): + parameter += argument[0] # Multiplication of different variables not on diagonal - if len(argument) == 3 and variable in argument and variable2 in argument and variable > variable2: - parameter += argument[0] + if ( + len(argument) == 3 + and variable in argument and variable2 in argument and variable > variable2 + ): + parameter += argument[0] # this value is already taking into account the factor 2 from quadratic term # For the variables on the diagonal, if the parameter is zero # we still have to check the sign in # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. - elif isinstance(argument, str) and variable in argument and variable2 in argument and variable == variable2: + elif isinstance(argument, str) and variable in argument \ + and variable2 in argument and variable == variable2: if "-" in argument: parameter += -1 diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py index 976f2d29..84b292d2 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py @@ -131,7 +131,7 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: start = start_time_measurement() # convert raw solution into the right format to use reverse_map() of ChoiQUBO.py - solution_dict = {i: el for i, el in enumerate(solution)} + solution_dict = dict(enumerate(solution)) # reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index 680943d2..b7784100 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -39,6 +39,8 @@ def __init__(self, device_name: str, region: str = None, arn: str = None): self.device = None self.arn = arn self.s3_destination_folder = None + self.boto_session = None + self.aws_session = None if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest diff --git a/src/quark2_adapter/legacy_classes/Application.py b/src/quark2_adapter/legacy_classes/Application.py index b1a422f4..f151fac7 100644 --- a/src/quark2_adapter/legacy_classes/Application.py +++ b/src/quark2_adapter/legacy_classes/Application.py @@ -145,7 +145,7 @@ def process_solution(self, solution) -> tuple[any, float]: return solution, 0 @abstractmethod - def validate(self, solution) -> Tuple[bool, float]: + def validate(self, solution) -> tuple[bool, float]: """ Check if the solution is valid. From f5ac6830faed3f49457b435a5c4e56725be198ce Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 8 Oct 2024 13:13:01 +0200 Subject: [PATCH 10/40] solved lint issue --- .../applications/optimization/SAT/mappings/DinneenISING.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py index 089225df..f6a97ba6 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py @@ -123,7 +123,7 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: start = start_time_measurement() # Convert raw solution into the right format to use reverse_map() of ChoiQUBO.py - solution_dict = {i: el for i, el in enumerate(solution) } + solution_dict = dict(enumerate(solution)) # Reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) From 9283a98591f5bdbb1af0435a3171d35cfef6e07e Mon Sep 17 00:00:00 2001 From: "Marvin Erdmann (FG-231)" Date: Tue, 8 Oct 2024 16:15:00 +0200 Subject: [PATCH 11/40] More changes towards PEP8 standard --- src/BenchmarkManager.py | 34 ++++++++++---------- src/BenchmarkRecord.py | 4 +-- src/ConfigManager.py | 8 ++--- src/Installer.py | 14 +++----- src/Plotter.py | 2 +- src/main.py | 4 +-- src/modules/Core.py | 4 ++- src/modules/devices/braket/Braket.py | 10 +++--- src/modules/solvers/Solver.py | 4 +-- src/modules/training/Inference.py | 5 ++- src/modules/training/QCBM.py | 5 +-- src/modules/training/QGAN.py | 48 +++++++++++++--------------- src/modules/training/Training.py | 35 ++++++++++---------- src/utils.py | 4 +-- src/utils_mpi.py | 17 +++++----- 15 files changed, 96 insertions(+), 102 deletions(-) diff --git a/src/BenchmarkManager.py b/src/BenchmarkManager.py index 1a244f3c..ed71f517 100644 --- a/src/BenchmarkManager.py +++ b/src/BenchmarkManager.py @@ -47,15 +47,15 @@ class JobStatus(Enum): FAILED = 3 -def _prepend_instruction(result: tuple) -> tuple: +def _prepend_instruction(result: tuple) -> tuple[Instruction, tuple]: """ - If the given list does not contain an instruction as first entry a + If the given list does not contain an Instruction as first entry a PROCEED is inserted at position 0 such that it is guaranteed that - the first entry of the returned list is an INSTRUCTION with PROCEED + the first entry of the returned list is an Instruction with PROCEED as default. - :param result: The tuple to which the instruction is to be prepended - :return: The tuple with an INSTRUCTION as first entry + :param result: The tuple to which the Instruction is to be prepended + :return: The tuple with an Instruction as first entry """ if isinstance(result[0], Instruction): return result @@ -63,7 +63,7 @@ def _prepend_instruction(result: tuple) -> tuple: return Instruction.PROCEED, *result -def postprocess(module_instance: Core, *args, **kwargs) -> tuple: +def postprocess(module_instance: Core, *args, **kwargs) -> tuple[Instruction, tuple]: """ Wraps module_instance.postprocess such that the first entry of the result list is guaranteed to be an Instruction. See _prepend_instruction. @@ -75,7 +75,7 @@ def postprocess(module_instance: Core, *args, **kwargs) -> tuple: return _prepend_instruction(result) -def preprocess(module_instance: Core, *args, **kwargs) -> tuple: +def preprocess(module_instance: Core, *args, **kwargs) -> tuple[Instruction, tuple]: """ Wraps module_instance.preprocess such that the first entry of the result list is guaranteed to be an Instruction. See _prepend_instruction. @@ -159,9 +159,9 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod """ Executes the benchmarks according to the given settings. - :param benchmark_config_manager: Instance of BenchmarkConfigManager class, where config is already set. - :param app_modules: The list of application modules as specified in the application modules configuration. - :param store_dir: Target directory to store the results of the benchmark (if you decided to store it) + :param benchmark_config_manager: Instance of BenchmarkConfigManager class, where config is already set + :param app_modules: The list of application modules as specified in the application modules configuration + :param store_dir: Target directory to store the results of the benchmark (if user decided to store it) :param interrupted_results_path: Result file from which the information for the interrupted jobs will be read. If store_dir is None the parent directory of interrupted_results_path will be used as store_dir. @@ -188,7 +188,7 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod results = self._collect_all_results() self._save_as_json(results) - def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # pylint: disable=R0915 + def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # pylint: disable=R0915 """ Goes through the benchmark backlog, which contains all the benchmarks to execute. @@ -213,7 +213,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # py logging.info(f"Running backlog item {idx_backlog + 1}/{len(benchmark_backlog)}," f" Iteration {i}/{repetitions}:") - # getting information of interrupted jobs + # Getting information of interrupted jobs job_info_with_meta_data = {} if interrupted_results: for entry in interrupted_results: @@ -280,7 +280,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # py logging.exception(f"Error during benchmark run: {error}", exc_info=True) quark_job_status = JobStatus.FAILED if job_info: - # restore results/infos from previous run + # Restore results/infos from previous run benchmark_records.append(job_info) if self.fail_fast: raise @@ -313,7 +313,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # py if break_flag: break - # print overall status information + # Log overall status information status_report = " ".join([f"{status.name}:{count}" for status, count in job_status_count_total.items()]) logging.info(80 * "=") logging.info(f"====== Run {len(benchmark_backlog)} backlog items " @@ -333,13 +333,13 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # py # pylint: disable=R0917 def traverse_config(self, module: dict, input_data: any, path: str, rep_count: int, previous_job_info: - dict = None) -> tuple[any, BenchmarkRecord]: + dict = None) -> tuple[Instruction, any, BenchmarkRecord]: """ Executes a benchmark by traversing down the initialized config recursively until it reaches the end. Then traverses up again. Once it reaches the root/application, a benchmark run is finished. :param module: Current module - :param input_data: The input data needed to execute the current module. + :param input_data: The input data needed to execute the current module :param path: Path in case the modules want to store anything :param rep_count: The iteration count :param previous_job_info: Information about previous job @@ -352,7 +352,7 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i submodule_job_info = None if previous_job_info and previous_job_info.get("submodule"): assert module['name'] == previous_job_info["submodule"]["module_name"], \ - f"asyncronous job info given, but no information about module {module['name']} stored in it" #TODO!! + f"asyncronous job info given, but no information about module {module['name']} stored in it" # TODO if 'submodule' in previous_job_info and previous_job_info['submodule']: submodule_job_info = previous_job_info['submodule'] diff --git a/src/BenchmarkRecord.py b/src/BenchmarkRecord.py index 0ea8be9c..d822c806 100644 --- a/src/BenchmarkRecord.py +++ b/src/BenchmarkRecord.py @@ -121,7 +121,7 @@ def linked_list_to_dict(self, llist: deque, module_level: int = 0) -> dict: @final def start_linked_list_to_dict(self) -> dict: """ - Helper function to start linked_list_to_dict function, which merges the various Metrics objects + Helper function to start linked_list_to_dict function which merges the various Metrics objects to one dictionary. :return: Resulting dictionary of linked_list_to_dict @@ -132,7 +132,7 @@ def start_linked_list_to_dict(self) -> dict: @final def get(self) -> dict: """ - Returns a dictionary containing all benchmark information and a nested dictionary, in which each level + Returns a dictionary containing all benchmark information and a nested dictionary in which each level contains the metrics of the respective module. :return: Dictionary containing all the records of the benchmark diff --git a/src/ConfigManager.py b/src/ConfigManager.py index 32d70caa..17a8f66e 100644 --- a/src/ConfigManager.py +++ b/src/ConfigManager.py @@ -152,7 +152,7 @@ def translate_legacy_config(config: dict) -> BenchmarkConfig: Translates the QUARK 1 config format to QUARK 2 format. :param config: QUARK 1 config - :return: Translated Config + :return: Translated config """ logging.info("Trying to translate QUARK 1 config to QUARK 2 config format") try: @@ -232,7 +232,7 @@ def get_config(self) -> BenchmarkConfig: """ Returns the config. - :return: Returns the config + :return: config """ return self.config @@ -315,12 +315,12 @@ def print(self) -> None: @staticmethod def _query_for_config(param_opts: dict, prefix: str = "") -> dict: """ - For a given module config, queries users in an interactive mode, which of the options they would like to + For a given module config, queries users in an interactive mode which of the options they would like to include in the final benchmark config. :param param_opts: Dictionary containing the options for a parameter including a description :param prefix: Prefix string, which is attached when interacting with the user - :return: Dictionary containing the decisions of the user on what to include in the benchmark. + :return: Dictionary containing the decisions of the user on what to include in the benchmark """ config = {} for key, config_answer in param_opts.items(): diff --git a/src/Installer.py b/src/Installer.py index 4400678f..4c0c5c08 100644 --- a/src/Installer.py +++ b/src/Installer.py @@ -29,7 +29,7 @@ class Installer: """ - Installer class that can be used by the user to install certain QUARK modules and also return the required python + Installer class that can be used by the user to install certain QUARK modules and also return the required Python packages for the demanded modules. """ @@ -127,7 +127,7 @@ def check_for_configs(self) -> list: def set_active_env(self, name: str) -> None: """ - Sets active env to active_env.json. + Sets the active env to active_env.json. :param name: Name of the env """ @@ -291,17 +291,13 @@ def _create_module_db_helper(module: Core, name: str) -> dict: """ Recursive helper function for create_module_db. - :param module: Modulen instance + :param module: Module instance :param name: Name of the module :return: Module dict """ return { "name": name, "class": module.__class__.__name__, - # TODO Verify the following really works as intended - # Since some modules are initialized with parameters in their constructor, we need to check what these - # parameters were. Hence we check whether any parameters in the class instance match the one from - # the constructor. "args": {k: v for k, v in module.__dict__.items() if k in inspect.signature(module.__init__).parameters.keys()}, "module": module.__module__, @@ -376,7 +372,7 @@ def create_conda_file(self, requirements: dict, name: str, directory: str = None :param requirements: Collected requirements :param name: Name of the conda env - :param directory: Directory where the file should be saved. If None self.envs_dir will be taken + :param directory: Directory where the file should be saved. If None self.envs_dir will be taken. """ if directory is None: directory = self.envs_dir @@ -402,7 +398,7 @@ def create_req_file(self, requirements: dict, name: str, directory: str = None) :param requirements: Collected requirements :param name: Name of the env - :param directory: Directory where the file should be saved. If None self.envs_dir will be taken + :param directory: Directory where the file should be saved. If None self.envs_dir will be taken. """ if directory is None: directory = self.envs_dir diff --git a/src/Plotter.py b/src/Plotter.py index 619cac04..90858386 100644 --- a/src/Plotter.py +++ b/src/Plotter.py @@ -220,7 +220,7 @@ def _get_config_keys(results: list[dict]) -> tuple[list, list]: @staticmethod def _extract_columns(config: dict, rest_result: dict) -> dict: """ - Funtion to extract and summarize certain data fields like the time spent in every module + Function to extract and summarize certain data fields like the time spent in every module from the nested module chain. :param config: Dictionary containing multiple data fields like the config a module diff --git a/src/main.py b/src/main.py index 47b250d5..4194521a 100644 --- a/src/main.py +++ b/src/main.py @@ -84,7 +84,7 @@ def start_benchmark_run(config_file: str = None, store_dir: str = None, """ setup_logging() - # Helper for Hybrid Jobs + # Helper for hybrid jobs if not config_file: config_file = os.environ["AMZN_BRAKET_HP_FILE"] if not store_dir: @@ -209,7 +209,7 @@ def handler_env_run(args: argparse.Namespace) -> None: """ Orchestrates the requests to the QUARK module environment. - :param args: Namespace with the arguments by the user + :param args: Namespace with the arguments given by the user """ installer = Installer() if args.createmoduledb: diff --git a/src/modules/Core.py b/src/modules/Core.py index 7d5fb07f..cf6663ba 100644 --- a/src/modules/Core.py +++ b/src/modules/Core.py @@ -65,6 +65,8 @@ def get_parameter_options(self) -> dict: :return: Available settings for this application """ + raise NotImplementedError("Please don't use the base version of get_parameter_options. " + "Implement your own override instead.") @final def get_submodule(self, option: str) -> Core: @@ -88,7 +90,7 @@ def get_default_submodule(self, option: str) -> Core: :param option: String with the chosen submodule :return: Module of type Core """ - raise NotImplementedError("Please don't use the base version of this method. " + raise NotImplementedError("Please don't use the base version of get_default_submodule. " "Implement your own override instead.") def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index b7784100..84803295 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -50,7 +50,7 @@ def __init__(self, device_name: str, region: str = None, arn: str = None): if device_name != "LocalSimulator": self._configure_aws_session(region) - def _configure_aws_session(self, region: str): + def _configure_aws_session(self, region: str) -> None: """ Configures the AWS session for the Braket device. @@ -64,7 +64,7 @@ def _configure_aws_session(self, region: str): self._initialize_aws_session(profile_name, region, my_config) @staticmethod - def _setup_proxy(): + def _setup_proxy() -> any: """ Sets up proxy configuration if available in the environment variables. @@ -150,7 +150,7 @@ def init_s3_storage(self, folder_name: str) -> None: """ Initializes an S3 storage bucket for Amazon Braket. - :param folder_name: Name of the s3 folder + :param folder_name: Name of the s3 bucket """ run_timestamp = datetime.today().date() username = getpass.getuser() @@ -161,12 +161,12 @@ def init_s3_storage(self, folder_name: str) -> None: @staticmethod def _create_s3_bucket(boto3_session: boto3.Session, bucket_name: str = 'quark-benchmark-framework', - region: str = 'us-east-1'): + region: str = 'us-east-1') -> None: """ Creates an S3 bucket with specific configurations. :param boto3-session: Boto3 session - :param bucket_name: Name of the S3 bucket + :param bucket_name: Name of the s3 bucket :param region: AWS region """ s3_client = boto3_session.client('s3', region_name=region) diff --git a/src/modules/solvers/Solver.py b/src/modules/solvers/Solver.py index e10dd890..30238f6d 100644 --- a/src/modules/solvers/Solver.py +++ b/src/modules/solvers/Solver.py @@ -24,7 +24,7 @@ class Solver(Core, ABC): def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - The actual solving process is done here, as we have the device, which got provided by the device submodule, + The actual solving process is done here, using the device which is provided by the device submodule and the problem data provided by the parent module. :param input_data: Data passed to the run function of the solver @@ -37,7 +37,7 @@ def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, flo return output, elapsed_time @abstractmethod - def run(self, mapped_problem, device_wrapper, config, **kwargs) -> tuple[any, float, dict]: + def run(self, mapped_problem: any, device_wrapper: any, config: any, **kwargs) -> tuple[any, float, dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. diff --git a/src/modules/training/Inference.py b/src/modules/training/Inference.py index c5d97598..6016a15e 100644 --- a/src/modules/training/Inference.py +++ b/src/modules/training/Inference.py @@ -36,8 +36,7 @@ def get_requirements() -> list[dict]: """ Returns requirements of this module. - :return: list of dict with requirements of this module. - :rtype: list[dict] + :return: List of dict with requirements of this module. """ return [{"name": "numpy", "version": "1.26.4"}] @@ -45,7 +44,7 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for this circuit. - :return: Configuration settings for the pretrained model. + :return: Configuration settings for the pretrained model .. code-block:: python return { diff --git a/src/modules/training/QCBM.py b/src/modules/training/QCBM.py index b6ed5ce9..64b77631 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/training/QCBM.py @@ -65,7 +65,7 @@ def get_requirements() -> list[dict]: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for the quantum circuit born machine + This function returns the configurable settings for the quantum circuit born machine. :return: Configuration settings for QCBM .. code-block:: python @@ -280,7 +280,8 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict return input_data - def data_visualization(self, loss_epoch, pmfs_model, samples, epoch): + def data_visualization(self, loss_epoch: np.ndarray, pmfs_model: np.ndarray, samples: any, epoch: int) -> ( + np.ndarray): """ Visualizes the data and metrics for training. diff --git a/src/modules/training/QGAN.py b/src/modules/training/QGAN.py index 543eb19a..07449be7 100644 --- a/src/modules/training/QGAN.py +++ b/src/modules/training/QGAN.py @@ -18,20 +18,19 @@ import torch from torch.utils.data import DataLoader from torch import nn -import torch.nn.functional as F +import torch.nn.functional as funct from tensorboardX import SummaryWriter import numpy as np import matplotlib.pyplot as plt from modules.training.Training import Training, Core -from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation from utils_mpi import is_running_mpi, get_comm MPI = is_running_mpi() comm = get_comm() -class QGAN(Training): # pylint: disable=R0902 +class QGAN(Training): # pylint: disable=R0902 """ Class for QGAN """ @@ -260,7 +259,7 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict This function starts the training of the QGAN. :param input_data: Dictionary with the variables from the circuit needed to start the training - :param config: Annealing settings + :param config: Training settings :param kwargs: Optional additional arguments :return: Dictionary including the solution """ @@ -280,22 +279,22 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict # Data from real distribution for training the discriminator real_data = data.float().to(self.device) self.discriminator.zero_grad() - outD_real = self.discriminator(real_data).view(-1) - errD_real = self.criterion(outD_real, self.real_labels) - errD_real.backward() + out_d_real = self.discriminator(real_data).view(-1) + err_d_real = self.criterion(out_d_real, self.real_labels) + err_d_real.backward() # Use Quantum Variational Circuit to generate fake samples fake_data, _ = self.generator.execute(self.params, self.batch_size) fake_data = fake_data.float().to(self.device) - outD_fake = self.discriminator(fake_data).view(-1) - errD_fake = self.criterion(outD_fake, self.fake_labels) - errD_fake.backward() + out_d_fake = self.discriminator(fake_data).view(-1) + err_d_fake = self.criterion(out_d_fake, self.fake_labels) + err_d_fake.backward() - errD = errD_real + errD_fake + err_d = err_d_real + err_d_fake self.optimizer_discriminator.step() - outD_fake = self.discriminator(fake_data).view(-1) - errG = self.criterion(outD_fake, self.real_labels) + out_d_fake = self.discriminator(fake_data).view(-1) + err_g = self.criterion(out_d_fake, self.real_labels) fake_data, _ = self.generator.execute(self.params,self.batch_size) gradients= self.generator.compute_gradient( self.params, @@ -309,8 +308,8 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict self.params = updated_params self.discriminator_weights = self.discriminator.state_dict() - generator_losses.append(errG.item()) - discriminator_losses.append(errD.item()) + generator_losses.append(err_g.item()) + discriminator_losses.append(err_d.item()) # Calculate loss _, pmfs_model = self.generator.execute(self.params, self.n_shots) @@ -324,8 +323,8 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict self.writer.add_scalar("metrics/KL_circuit_evals", loss, circuit_evals) # Calculate and log the loss values at the end of each epoch - self.writer.add_scalar('Loss/GAN_Generator', errG.item(), circuit_evals) - self.writer.add_scalar('Loss/GAN_Discriminator', errD.item(), circuit_evals) + self.writer.add_scalar('Loss/GAN_Generator', err_g.item(), circuit_evals) + self.writer.add_scalar('Loss/GAN_Discriminator', err_d.item(), circuit_evals) if loss < best_kl_divergence: best_kl_divergence = loss @@ -339,7 +338,7 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict log_message = ( f"Epoch: {epoch + 1}/{self.n_epochs}, " f"Batch: {batch + 1}/{len(self.bins_train) // self.batch_size}, " - f"Discriminator Loss: {errD.item()}, Generator Loss: {errG.item()}, KL Divergence: {loss} " + f"Discriminator Loss: {err_d.item()}, Generator Loss: {err_g.item()}, KL Divergence: {loss} " ) logging.info(log_message) @@ -397,17 +396,16 @@ def __init__(self, input_length: int): def forward(self, x: torch.Tensor) -> float: """ - InitializeS the weight tensor of the linear - layers with values using a Xavier uniform distribution. + Initializes the weight tensor of the linear layers with values using a Xavier uniform distribution. :param x: Input of the discriminator :type x: torch.Tensor :return: Probability fake/real sample :rtype: float """ - h = F.leaky_relu(self.dense1(x)) - h = F.leaky_relu(self.dense2(h)) - return F.sigmoid(h) + h = funct.leaky_relu(self.dense1(x)) + h = funct.leaky_relu(self.dense2(h)) + return funct.sigmoid(h) @staticmethod def weights_init(m: nn.Linear) -> None: @@ -467,8 +465,8 @@ def compute_gradient(self, params: np.ndarray, discriminator: torch.nn.Module, c :param discriminator: Discriminator of the QGAN :param criterion: Loss function :param label: Label indicating of sample is true or fake - :param device: torch device (e.g. CPU or CUDA) - :return: samples and the probability distribution generated by the quantum circuit + :param device: Torch device (e.g., CPU or CUDA) + :return: Samples and the probability distribution generated by the quantum circuit """ shift = 0.5 * np.pi gradients = np.zeros(len(params)) # Initialize gradients as an array of zeros diff --git a/src/modules/training/Training.py b/src/modules/training/Training.py index 2d9dbe54..9fb2ec4e 100644 --- a/src/modules/training/Training.py +++ b/src/modules/training/Training.py @@ -53,15 +53,14 @@ def get_requirements() -> list[dict]: """ return [{"name": "numpy", "version": "1.26.4"}] - def postprocess(self, input_data: dict, config: dict, **kwargs): + def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ Perform the actual training of the machine learning model. :param input_data: Collected information of the benchmarking process :param config: Training settings :param kwargs: Optional additional arguments - :return: Training results and the postprocessing time. - :rtype: + :return: Training results and the postprocessing time """ start = start_time_measurement() logging.info("Start training") @@ -83,26 +82,26 @@ def start_training(self, input_data: dict, config: any, **kwargs: dict) -> dict: :param input_data: A representation of the quantum machine learning model that will be trained :param config: Config specifying the parameters of the training (dict-like Config type defined in children) - :param kwargs: optional additional settings + :param kwargs: Optional additional settings :return: Solution, the time it took to compute it and some optional additional information """ pass def sample_from_pmf(self, pmf: np.ndarray, n_shots: int) -> np.ndarray: """ - Function to sample from the probability mass function generated by the quantum circuit. + This function samples from the probability mass function generated by the quantum circuit. :param pmf: Probability mass function generated by the quantum circuit :param n_shots: Number of shots - :return: number of counts in the 2**n_qubits bins + :return: Number of counts in the 2**n_qubits bins """ samples = np.random.choice(self.n_states_range, size=n_shots, p=pmf) counts = np.bincount(samples, minlength=len(self.n_states_range)) return counts - def kl_divergence(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: + def kl_divergence(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: """ - Kullback-Leibler divergence, that is used as a loss function. + This function calculates the Kullback-Leibler divergence, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit :param pmf_target: Probability mass function of the target distribution @@ -111,9 +110,9 @@ def kl_divergence(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: pmf_model[pmf_model == 0] = 1e-8 return np.sum(pmf_target * np.log(pmf_target / pmf_model), axis=1) - def nll(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: + def nll(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: """ - Negative log likelihood, that is used as a loss function. + This function calculates th negative log likelihood, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit :param pmf_target: Probability mass function of the target distribution @@ -122,9 +121,9 @@ def nll(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: pmf_model[pmf_model == 0] = 1e-8 return -np.sum(pmf_target * np.log(pmf_model), axis=1) - def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: + def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: """ - Maximum mean discrepancy, that is used as a loss function. + This function calculates the maximum mean discrepancy, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit :param pmf_target: Probability mass function of the target distribution @@ -155,23 +154,23 @@ def __init__(self): self.start_recording = self.start_recording_gpu if GPU else self.start_recording_cpu self.stop_recording = self.stop_recording_gpu if GPU else self.stop_recording_cpu - def start_recording_cpu(self): + def start_recording_cpu(self) -> None: """ - Function to start time measurement on the CPU. + This is a function to start time measurement on the CPU. """ self.start_cpu = start_time_measurement() def stop_recording_cpu(self) -> float: """ - Function to stop time measurement on the CPU. + This is a function to stop time measurement on the CPU. .return: Elapsed time in milliseconds """ return end_time_measurement(self.start_cpu) - def start_recording_gpu(self): + def start_recording_gpu(self) -> None: """ - Function to start time measurement on the GPU. + This is a function to start time measurement on the GPU. """ self.start_gpu = np.cuda.Event() self.end_gpu = np.cuda.Event() @@ -179,7 +178,7 @@ def start_recording_gpu(self): def stop_recording_gpu(self) -> float: """ - Function to stop time measurement on the GPU. + This is a function to stop time measurement on the GPU. :return: Elapsed time in milliseconds """ diff --git a/src/utils.py b/src/utils.py index 637f7050..8f94398b 100644 --- a/src/utils.py +++ b/src/utils.py @@ -73,7 +73,7 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: raise ValueError(f"{name} not found in {options}") -def _import_class(module_path: str, class_name: str, base_dir: str = None) -> type: +def _import_class(module_path: str, class_name: str, base_dir: str = None) -> any: """ Helper function which allows to replace hard-coded imports of the form 'import MyClass from path.to.mypkg' by calling _import_class('path.to.mypkg', 'MyClass'). @@ -154,7 +154,7 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: :param j: The JSON to be adapted - expected to be a QUARK modules configuration or a part of it :param base_dir: The base directory to be used for path expansion - :return: The adapted json + :return: The adapted JSON """ assert type(j) in [dict, list], f"unexpected type:{type(j)}" if type(j) == list: diff --git a/src/utils_mpi.py b/src/utils_mpi.py index 1a468da7..a188b4b7 100644 --- a/src/utils_mpi.py +++ b/src/utils_mpi.py @@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs): MPI = is_running_mpi() self.rank = MPI.COMM_WORLD.Get_rank() if MPI else 0 - def emit(self, record): + def emit(self, record) -> None: """ Emits a log record if running on the root process. @@ -81,7 +81,7 @@ def __init__(self, *args, **kwargs): MPI = is_running_mpi() self.rank = MPI.COMM_WORLD.Get_rank() if MPI else 0 - def emit(self, record): + def emit(self, record) -> None: """ Emits a log record if running on the root process. @@ -92,18 +92,17 @@ def emit(self, record): super().emit(record) -def get_comm(): +def get_comm() -> any: """ Retrieves the MPI communicator if running in an MPI environment, otherwise provides a mock comm class. return: MPI communicator or a mock class with limited methods - rtype: MPI.Comm or class """ - MPI = is_running_mpi() - if MPI: - comm = MPI.COMM_WORLD + mpi = is_running_mpi() + if mpi: + Comm = mpi.COMM_WORLD else: - class comm(): + class Comm(): @staticmethod def Get_rank(): return 0 @@ -116,4 +115,4 @@ def Bcast(loss, root): def Barrier(): pass - return comm + return Comm From 1d9d17aad7ddff623ff51f62ca3ec2756621c345 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 10:03:54 +0200 Subject: [PATCH 12/40] modified readme file --- AUTHORS | 2 ++ README.md | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 0aedfa1c..9397a0e5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,6 +5,8 @@ Philipp Ross (BMW Group) Marvin Erdmann (BMW Group) Andre Luckow (BMW Group) +Greshma Shaji (BMW Group) Florian Kiwit (BMW Group) Jürgen Schwitalla (Eviden) Chris van den Oetelaar (Capgemini) +Niklas Steinmann (Fraunhofer FOKUS) diff --git a/README.md b/README.md index 331af9cc..dbcea7e8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,17 @@ Content of the environment: In case you want to use custom modules files (for example, to use external modules from other repositories), you can still use the ```--modules``` option. You can find the documentation in the respective Read the Docs section. +## Git Large File Storage (LFS) +Some files in this repository are large and tracked using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. + +### Installing Git LFS +1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): + - On Linux/macOS + ```bash + git lfs install + ``` + - On Windows. Download and install Git LFS from the [Official page](https://git-lfs.com/) + ## Running a Benchmark ```bash @@ -84,6 +95,10 @@ Example run (You need to check at least one option with an ``X`` for the checkbo PVC SAT > TSP + ACL + MIS + SCP + GenerativeModeling 2023-03-21 09:18:36,440 [INFO] Import module modules.applications.optimization.TSP.TSP [?] (Option for TSP) How many nodes does you graph need?: @@ -94,6 +109,7 @@ Example run (You need to check at least one option with an ``X`` for the checkbo [ ] 10 [ ] 14 [ ] 16 + [ ] Custom Range [?] What submodule do you want?: [ ] Ising @@ -116,11 +132,20 @@ Example run (You need to check at least one option with an ``X`` for the checkbo 2023-03-21 09:18:51,388 [INFO] All 3 nodes got visited 2023-03-21 09:18:51,388 [INFO] Total distance (without return): 727223.0 2023-03-21 09:18:51,388 [INFO] Total distance (including return): 1436368.0 +2023-03-21 09:18:51,388 [INFO] +2023-03-21 09:18:51,388 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== +2023-03-21 09:18:51,389 [INFO] +2023-03-21 09:18:51,389 [INFO] =============== Run finished =============== 2023-03-21 09:18:51,389 [INFO] -2023-03-21 09:18:51,389 [INFO] ============================================================ +2023-03-21 09:18:51,389 [INFO] ================================================================================ +2023-03-21 09:18:51,389 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 +2023-03-21 09:18:51,389 [INFO] ================================================================================ 2023-03-21 09:18:51,389 [INFO] 2023-03-21 09:18:51,389 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50/results.json 2023-03-21 09:18:51,746 [INFO] Finished creating plots. +2023-03-21 09:18:51,389 [INFO] ============================================================ +2023-03-21 09:18:51,389 [INFO] ==================== QUARK finished! ==================== +2023-03-21 09:18:51,389 [INFO] ============================================================ ``` From 24b824d7df61f3eb47d3c001396fe378849a9915 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 16:38:47 +0200 Subject: [PATCH 13/40] modified readme file --- README.md | 56 ++++++++++++----------- docs/tutorial.rst | 113 +++++++++++++++++++++++++++++----------------- 2 files changed, 101 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index dbcea7e8..eedf3c7f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Example run (You need to check at least one option with an ``X`` for the checkbo SCP GenerativeModeling -2023-03-21 09:18:36,440 [INFO] Import module modules.applications.optimization.TSP.TSP +2024-10-09 15:05:52,610 [INFO] Import module modules.applications.optimization.TSP.TSP [?] (Option for TSP) How many nodes does you graph need?: > [X] 3 [ ] 4 @@ -118,34 +118,38 @@ Example run (You need to check at least one option with an ``X`` for the checkbo [ ] ReverseGreedyClassicalTSP [ ] RandomTSP -2023-03-21 09:18:49,563 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. -2023-03-21 09:18:49,566 [INFO] Submodule configuration finished -[?] How many repetitions do you want?: 1 -2023-03-21 09:18:50,577 [INFO] Import module modules.applications.optimization.TSP.TSP -2023-03-21 09:18:50,948 [INFO] Created Benchmark run directory /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50 -2023-03-21 09:18:51,025 [INFO] Codebase is based on revision 075201825fa71c24b5567e1290966081be7dbdc0 and has some uncommitted changes -2023-03-21 09:18:51,026 [INFO] Running backlog item 1/1, Iteration 1/1: -2023-03-21 09:18:51,388 [INFO] Route found: +2024-10-09 15:06:20,897 [INFO] Import module modules.solvers.GreedyClassicalTSP +2024-10-09 15:06:20,933 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. +2024-10-09 15:06:20,933 [INFO] Import module modules.devices.Local +2024-10-09 15:06:20,946 [INFO] Submodule configuration finished +[?] How many repetitions do you want?: 1P +2024-10-09 15:07:11,573 [INFO] Import module modules.applications.optimization.TSP.TSP +2024-10-09 15:07:11,573 [INFO] Import module modules.solvers.GreedyClassicalTSP +2024-10-09 15:07:11,574 [INFO] Import module modules.devices.Local +2024-10-09 15:07:12,194 [INFO] [INFO] Created Benchmark run directory /Users/user1/quark/benchmark_runs/tsp-2024-10-09-15-07-11 +2024-10-09 15:07:12,194 [INFO] Codebase is based on revision 1d9d17aad7ddff623ff51f62ca3ec2756621c345 and has no uncommitted changes +2024-10-09 15:07:12,195 [INFO] Running backlog item 1/1, Iteration 1/1: +2024-10-09 15:07:12,386 [INFO] Route found: Node 0 -> Node 2 -> Node 1 -2023-03-21 09:18:51,388 [INFO] All 3 nodes got visited -2023-03-21 09:18:51,388 [INFO] Total distance (without return): 727223.0 -2023-03-21 09:18:51,388 [INFO] Total distance (including return): 1436368.0 -2023-03-21 09:18:51,388 [INFO] -2023-03-21 09:18:51,388 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== -2023-03-21 09:18:51,389 [INFO] -2023-03-21 09:18:51,389 [INFO] =============== Run finished =============== -2023-03-21 09:18:51,389 [INFO] -2023-03-21 09:18:51,389 [INFO] ================================================================================ -2023-03-21 09:18:51,389 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 -2023-03-21 09:18:51,389 [INFO] ================================================================================ -2023-03-21 09:18:51,389 [INFO] -2023-03-21 09:18:51,389 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50/results.json -2023-03-21 09:18:51,746 [INFO] Finished creating plots. -2023-03-21 09:18:51,389 [INFO] ============================================================ -2023-03-21 09:18:51,389 [INFO] ==================== QUARK finished! ==================== -2023-03-21 09:18:51,389 [INFO] ============================================================ +2024-10-09 15:07:12,386 [INFO] All 3 nodes got visited +2024-10-09 15:07:12,386 [INFO] Total distance (without return): 727223.0 +2024-10-09 15:07:12,386 [INFO] Total distance (including return): 1436368.0 +2024-10-09 15:07:12,386 [INFO] +2024-10-09 15:07:12,386 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== +2024-10-09 15:07:12,387 [INFO] +2024-10-09 15:07:12,387 [INFO] =============== Run finished =============== +2024-10-09 15:07:12,387 [INFO] +2024-10-09 15:07:12,387 [INFO] ================================================================================ +2024-10-09 15:07:12,387 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 +2024-10-09 15:07:12,387 [INFO] ================================================================================ +2024-10-09 15:07:12,395 [INFO] +2024-10-09 15:07:12,400 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2024-10-09-15-07-11/results.json +2024-10-09 15:07:12,942 [INFO] Finished creating plots. +2024-10-09 15:07:12,943 [INFO] ============================================================ +2024-10-09 15:07:12,944 [INFO] ==================== QUARK finished! ==================== +2024-10-09 15:07:12,944 [INFO] ============================================================ ``` diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 4c80bae5..95c789d2 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -65,6 +65,17 @@ You can also visualize the contents of your QUARK environment: In case you want to use custom modules files (for example to use external modules from other repositories), you can still use the ``--modules`` option. You can find the documentation in the Dynamic Imports section. +## Git Large File Storage (LFS) +Some files in this repository are large and tracked using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. + +Installing Git LFS +~~~~~~~~~~~~~~~~~~~~ +1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): + - On Linux/macOS + ```bash + git lfs install + ``` + - On Windows. Download and install Git LFS from the [Official page](https://git-lfs.com/) Running a Benchmark ~~~~~~~~~~~~~~~~~~~~ @@ -86,48 +97,66 @@ Example run (You need to check at least one option with an ``X`` for the checkbo :: - (quark) % python src/main.py - [?] What application do you want?: TSP - PVC - SAT - > TSP - - 2023-03-21 09:18:36,440 [INFO] Import module modules.applications.optimization.TSP.TSP - [?] (Option for TSP) How many nodes does you graph need?: - > [X] 3 - [ ] 4 - [ ] 6 - [ ] 8 - [ ] 10 - [ ] 14 - [ ] 16 - - [?] What submodule do you want?: - [ ] Ising - [ ] Qubo - > [X] GreedyClassicalTSP - [ ] ReverseGreedyClassicalTSP - [ ] RandomTSP - - 2023-03-21 09:18:49,563 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. - 2023-03-21 09:18:49,566 [INFO] Submodule configuration finished - [?] How many repetitions do you want?: 1 - 2023-03-21 09:18:50,577 [INFO] Import module modules.applications.optimization.TSP.TSP - 2023-03-21 09:18:50,948 [INFO] Created Benchmark run directory /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50 - 2023-03-21 09:18:51,025 [INFO] Codebase is based on revision 075201825fa71c24b5567e1290966081be7dbdc0 and has some uncommitted changes - 2023-03-21 09:18:51,026 [INFO] Running backlog item 1/1, Iteration 1/1: - 2023-03-21 09:18:51,388 [INFO] Route found: - Node 0 -> - Node 2 -> - Node 1 - 2023-03-21 09:18:51,388 [INFO] All 3 nodes got visited - 2023-03-21 09:18:51,388 [INFO] Total distance (without return): 727223.0 - 2023-03-21 09:18:51,388 [INFO] Total distance (including return): 1436368.0 - 2023-03-21 09:18:51,389 [INFO] - 2023-03-21 09:18:51,389 [INFO] ============================================================ - 2023-03-21 09:18:51,389 [INFO] - 2023-03-21 09:18:51,389 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50/results.json - 2023-03-21 09:18:51,746 [INFO] Finished creating plots. +(quark) % python src/main.py +[?] What application do you want?: TSP + PVC + SAT + > TSP + ACL + MIS + SCP + GenerativeModeling + +2024-10-09 15:05:52,610 [INFO] Import module modules.applications.optimization.TSP.TSP +[?] (Option for TSP) How many nodes does you graph need?: + > [X] 3 + [ ] 4 + [ ] 6 + [ ] 8 + [ ] 10 + [ ] 14 + [ ] 16 + [ ] Custom Range + +[?] What submodule do you want?: + [ ] Ising + [ ] Qubo + > [X] GreedyClassicalTSP + [ ] ReverseGreedyClassicalTSP + [ ] RandomTSP + +2024-10-09 15:06:20,897 [INFO] Import module modules.solvers.GreedyClassicalTSP +2024-10-09 15:06:20,933 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. +2024-10-09 15:06:20,933 [INFO] Import module modules.devices.Local +2024-10-09 15:06:20,946 [INFO] Submodule configuration finished +[?] How many repetitions do you want?: 1P +2024-10-09 15:07:11,573 [INFO] Import module modules.applications.optimization.TSP.TSP +2024-10-09 15:07:11,573 [INFO] Import module modules.solvers.GreedyClassicalTSP +2024-10-09 15:07:11,574 [INFO] Import module modules.devices.Local +2024-10-09 15:07:12,194 [INFO] [INFO] Created Benchmark run directory /Users/user1/quark/benchmark_runs/tsp-2024-10-09-15-07-11 +2024-10-09 15:07:12,194 [INFO] Codebase is based on revision 1d9d17aad7ddff623ff51f62ca3ec2756621c345 and has no uncommitted changes +2024-10-09 15:07:12,195 [INFO] Running backlog item 1/1, Iteration 1/1: +2024-10-09 15:07:12,386 [INFO] Route found: + Node 0 -> + Node 2 -> + Node 1 +2024-10-09 15:07:12,386 [INFO] All 3 nodes got visited +2024-10-09 15:07:12,386 [INFO] Total distance (without return): 727223.0 +2024-10-09 15:07:12,386 [INFO] Total distance (including return): 1436368.0 +2024-10-09 15:07:12,386 [INFO] +2024-10-09 15:07:12,386 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== +2024-10-09 15:07:12,387 [INFO] +2024-10-09 15:07:12,387 [INFO] =============== Run finished =============== +2024-10-09 15:07:12,387 [INFO] +2024-10-09 15:07:12,387 [INFO] ================================================================================ +2024-10-09 15:07:12,387 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 +2024-10-09 15:07:12,387 [INFO] ================================================================================ +2024-10-09 15:07:12,395 [INFO] +2024-10-09 15:07:12,400 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2024-10-09-15-07-11/results.json +2024-10-09 15:07:12,942 [INFO] Finished creating plots. +2024-10-09 15:07:12,943 [INFO] ============================================================ +2024-10-09 15:07:12,944 [INFO] ==================== QUARK finished! ==================== +2024-10-09 15:07:12,944 [INFO] ============================================================ All used config files, logs and results are stored in a folder in the From 3c57d18a6130c87005c6b1d7c0f77eba35d7c2e6 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 16:40:19 +0200 Subject: [PATCH 14/40] modified readme file --- docs/tutorial.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 95c789d2..a9abba99 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -72,9 +72,9 @@ Installing Git LFS ~~~~~~~~~~~~~~~~~~~~ 1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): - On Linux/macOS - ```bash - git lfs install - ``` + .. code:: bash + git lfs install + - On Windows. Download and install Git LFS from the [Official page](https://git-lfs.com/) Running a Benchmark From cac5f3cdcbf74046ba7328e6a6e3d447654367d6 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 16:43:24 +0200 Subject: [PATCH 15/40] modified readme file --- docs/tutorial.rst | 125 +++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index a9abba99..349c75d7 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -72,8 +72,9 @@ Installing Git LFS ~~~~~~~~~~~~~~~~~~~~ 1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): - On Linux/macOS - .. code:: bash - git lfs install + :: + + git lfs install - On Windows. Download and install Git LFS from the [Official page](https://git-lfs.com/) @@ -97,66 +98,66 @@ Example run (You need to check at least one option with an ``X`` for the checkbo :: -(quark) % python src/main.py -[?] What application do you want?: TSP - PVC - SAT - > TSP - ACL - MIS - SCP - GenerativeModeling - -2024-10-09 15:05:52,610 [INFO] Import module modules.applications.optimization.TSP.TSP -[?] (Option for TSP) How many nodes does you graph need?: - > [X] 3 - [ ] 4 - [ ] 6 - [ ] 8 - [ ] 10 - [ ] 14 - [ ] 16 - [ ] Custom Range - -[?] What submodule do you want?: - [ ] Ising - [ ] Qubo - > [X] GreedyClassicalTSP - [ ] ReverseGreedyClassicalTSP - [ ] RandomTSP - -2024-10-09 15:06:20,897 [INFO] Import module modules.solvers.GreedyClassicalTSP -2024-10-09 15:06:20,933 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. -2024-10-09 15:06:20,933 [INFO] Import module modules.devices.Local -2024-10-09 15:06:20,946 [INFO] Submodule configuration finished -[?] How many repetitions do you want?: 1P -2024-10-09 15:07:11,573 [INFO] Import module modules.applications.optimization.TSP.TSP -2024-10-09 15:07:11,573 [INFO] Import module modules.solvers.GreedyClassicalTSP -2024-10-09 15:07:11,574 [INFO] Import module modules.devices.Local -2024-10-09 15:07:12,194 [INFO] [INFO] Created Benchmark run directory /Users/user1/quark/benchmark_runs/tsp-2024-10-09-15-07-11 -2024-10-09 15:07:12,194 [INFO] Codebase is based on revision 1d9d17aad7ddff623ff51f62ca3ec2756621c345 and has no uncommitted changes -2024-10-09 15:07:12,195 [INFO] Running backlog item 1/1, Iteration 1/1: -2024-10-09 15:07:12,386 [INFO] Route found: - Node 0 -> - Node 2 -> - Node 1 -2024-10-09 15:07:12,386 [INFO] All 3 nodes got visited -2024-10-09 15:07:12,386 [INFO] Total distance (without return): 727223.0 -2024-10-09 15:07:12,386 [INFO] Total distance (including return): 1436368.0 -2024-10-09 15:07:12,386 [INFO] -2024-10-09 15:07:12,386 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== -2024-10-09 15:07:12,387 [INFO] -2024-10-09 15:07:12,387 [INFO] =============== Run finished =============== -2024-10-09 15:07:12,387 [INFO] -2024-10-09 15:07:12,387 [INFO] ================================================================================ -2024-10-09 15:07:12,387 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 -2024-10-09 15:07:12,387 [INFO] ================================================================================ -2024-10-09 15:07:12,395 [INFO] -2024-10-09 15:07:12,400 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2024-10-09-15-07-11/results.json -2024-10-09 15:07:12,942 [INFO] Finished creating plots. -2024-10-09 15:07:12,943 [INFO] ============================================================ -2024-10-09 15:07:12,944 [INFO] ==================== QUARK finished! ==================== -2024-10-09 15:07:12,944 [INFO] ============================================================ + (quark) % python src/main.py + [?] What application do you want?: TSP + PVC + SAT + > TSP + ACL + MIS + SCP + GenerativeModeling + + 2024-10-09 15:05:52,610 [INFO] Import module modules.applications.optimization.TSP.TSP + [?] (Option for TSP) How many nodes does you graph need?: + > [X] 3 + [ ] 4 + [ ] 6 + [ ] 8 + [ ] 10 + [ ] 14 + [ ] 16 + [ ] Custom Range + + [?] What submodule do you want?: + [ ] Ising + [ ] Qubo + > [X] GreedyClassicalTSP + [ ] ReverseGreedyClassicalTSP + [ ] RandomTSP + + 2024-10-09 15:06:20,897 [INFO] Import module modules.solvers.GreedyClassicalTSP + 2024-10-09 15:06:20,933 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. + 2024-10-09 15:06:20,933 [INFO] Import module modules.devices.Local + 2024-10-09 15:06:20,946 [INFO] Submodule configuration finished + [?] How many repetitions do you want?: 1P + 2024-10-09 15:07:11,573 [INFO] Import module modules.applications.optimization.TSP.TSP + 2024-10-09 15:07:11,573 [INFO] Import module modules.solvers.GreedyClassicalTSP + 2024-10-09 15:07:11,574 [INFO] Import module modules.devices.Local + 2024-10-09 15:07:12,194 [INFO] [INFO] Created Benchmark run directory /Users/user1/quark/benchmark_runs/tsp-2024-10-09-15-07-11 + 2024-10-09 15:07:12,194 [INFO] Codebase is based on revision 1d9d17aad7ddff623ff51f62ca3ec2756621c345 and has no uncommitted changes + 2024-10-09 15:07:12,195 [INFO] Running backlog item 1/1, Iteration 1/1: + 2024-10-09 15:07:12,386 [INFO] Route found: + Node 0 -> + Node 2 -> + Node 1 + 2024-10-09 15:07:12,386 [INFO] All 3 nodes got visited + 2024-10-09 15:07:12,386 [INFO] Total distance (without return): 727223.0 + 2024-10-09 15:07:12,386 [INFO] Total distance (including return): 1436368.0 + 2024-10-09 15:07:12,386 [INFO] + 2024-10-09 15:07:12,386 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== + 2024-10-09 15:07:12,387 [INFO] + 2024-10-09 15:07:12,387 [INFO] =============== Run finished =============== + 2024-10-09 15:07:12,387 [INFO] + 2024-10-09 15:07:12,387 [INFO] ================================================================================ + 2024-10-09 15:07:12,387 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 + 2024-10-09 15:07:12,387 [INFO] ================================================================================ + 2024-10-09 15:07:12,395 [INFO] + 2024-10-09 15:07:12,400 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2024-10-09-15-07-11/results.json + 2024-10-09 15:07:12,942 [INFO] Finished creating plots. + 2024-10-09 15:07:12,943 [INFO] ============================================================ + 2024-10-09 15:07:12,944 [INFO] ==================== QUARK finished! ==================== + 2024-10-09 15:07:12,944 [INFO] ============================================================ All used config files, logs and results are stored in a folder in the From aac9102d62761b797637e9f1b86d0e503d0f3735 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 16:45:28 +0200 Subject: [PATCH 16/40] modified readme file --- docs/tutorial.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 349c75d7..71a3bbb9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -65,11 +65,12 @@ You can also visualize the contents of your QUARK environment: In case you want to use custom modules files (for example to use external modules from other repositories), you can still use the ``--modules`` option. You can find the documentation in the Dynamic Imports section. -## Git Large File Storage (LFS) +Git Large File Storage (LFS) +~~~~~~~~~~~~~~~~~~~~ Some files in this repository are large and tracked using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. Installing Git LFS -~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^ 1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): - On Linux/macOS :: From e341097d52c5695bd65e8b5e29553eb5c5fc4167 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 16:53:09 +0200 Subject: [PATCH 17/40] modified tutorial.rst file --- docs/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 71a3bbb9..638e3f4f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -71,13 +71,13 @@ Some files in this repository are large and tracked using **Git LFS**. If you ar Installing Git LFS ^^^^^^^^^^^^^^^^^^^ -1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): +1. Install Git LFS by following the instructions on `Git LFS `_ - On Linux/macOS :: git lfs install - - On Windows. Download and install Git LFS from the [Official page](https://git-lfs.com/) + - On Windows. Download and install Git LFS from the `Official page `_ Running a Benchmark ~~~~~~~~~~~~~~~~~~~~ From c06f632f963396c93b7c2dd23235b618e20d9259 Mon Sep 17 00:00:00 2001 From: q666911 Date: Wed, 9 Oct 2024 16:59:20 +0200 Subject: [PATCH 18/40] modified tutorial.rst file --- docs/analysis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/analysis.rst b/docs/analysis.rst index 76090b68..b4f87043 100644 --- a/docs/analysis.rst +++ b/docs/analysis.rst @@ -11,7 +11,7 @@ Python Example import json # Load the results - filename = "benchmark_runs/tsp-2023-03-13-15-31-17/results.json" + filename = "benchmark_runs/tsp-2024-10-09-15-07-11/results.json" with open(filename) as f: results = json.load(f) From 44e669146c60a0d195266e4ed909de74a85f870e Mon Sep 17 00:00:00 2001 From: q666911 Date: Thu, 10 Oct 2024 09:31:41 +0200 Subject: [PATCH 19/40] modified tutorial.rst file --- docs/tutorial.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 638e3f4f..d5552083 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -2,7 +2,8 @@ QUARK: A Framework for Quantum Computing Application Benchmarking ================================================================= Quantum Computing Application Benchmark (QUARK) is a framework for orchestrating benchmarks of different industry applications on quantum computers. -QUARK supports various applications, like the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, or the robot path optimization in the PVC sealing use case. +QUARK supports various applications such as the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, robot path optimization in the PVC sealing use case +as well as new additions like the Maximum Independent Set (MIS), Set Cover Problem (SCP) and Auto Carrier Loading (ACL). It also features different solvers (e.g., simulated /quantum annealing and the quantum approximate optimization algorithm (QAOA)), quantum devices (e.g., IonQ and Rigetti), and simulators. It is designed to be easily extendable in all of its components: applications, mappings, solvers, devices, and any other custom modules. From 32c5e08c54e3e2d7ffe3275ad60ec9f6505c2c9d Mon Sep 17 00:00:00 2001 From: q666911 Date: Thu, 10 Oct 2024 10:37:46 +0200 Subject: [PATCH 20/40] modified readme file --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eedf3c7f..2b7c4fc3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # QUARK: A Framework for Quantum Computing Application Benchmarking Quantum Computing Application Benchmark (QUARK) is a framework for orchestrating benchmarks of different industry applications on quantum computers. -QUARK supports various applications, like the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, or the robot path optimization in the PVC sealing use case. +QUARK supports various applications such as the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, robot path optimization in the PVC sealing use case +as well as new additions like the Maximum Independent Set (MIS), Set Cover Problem (SCP) and Auto Carrier Loading (ACL). It also features different solvers (e.g., simulated /quantum annealing and the quantum approximate optimization algorithm (QAOA)), quantum devices (e.g., IonQ and Rigetti), and simulators. It is designed to be easily extendable in all of its components: applications, mappings, solvers, devices, and any other custom modules. From bad62a5fda61453322736d05aae8059d18d3e23e Mon Sep 17 00:00:00 2001 From: "Marvin Erdmann (FG-231)" Date: Fri, 11 Oct 2024 16:32:32 +0200 Subject: [PATCH 21/40] Changes towards PEP8 standardization --- README.md | 4 +- docs/tutorial.rst | 4 +- src/Plotter.py | 4 +- .../generative_modeling/GenerativeModeling.py | 7 + .../data/data_handler/ContinuousData.py | 5 +- .../data/data_handler/DataHandler.py | 18 +-- .../data/data_handler/DiscreteData.py | 12 +- .../data_handler/MetricsGeneralization.py | 17 +-- .../mappings/CustomQiskitNoisyBackend.py | 2 + .../generative_modeling/mappings/Library.py | 4 +- .../mappings/LibraryPennylane.py | 12 +- .../mappings/LibraryQiskit.py | 27 ++-- .../mappings/PresetQiskitNoisyBackend.py | 10 ++ .../transformations/PIT.py | 1 + .../transformations/Transformation.py | 40 ++--- .../applications/optimization/ACL/ACL.py | 142 +++++++++--------- .../optimization/ACL/mappings/ISING.py | 12 +- .../optimization/ACL/mappings/QUBO.py | 26 ++-- .../applications/optimization/MIS/MIS.py | 10 +- .../optimization/MIS/data/graph_layouts.py | 58 +++---- .../optimization/MIS/mappings/NeutralAtom.py | 8 +- .../applications/optimization/Optimization.py | 4 +- .../applications/optimization/PVC/PVC.py | 22 +-- .../optimization/PVC/mappings/ISING.py | 12 +- .../optimization/PVC/mappings/QUBO.py | 35 +++-- .../applications/optimization/SAT/SAT.py | 24 +-- .../optimization/SAT/mappings/ChoiISING.py | 14 +- .../optimization/SAT/mappings/ChoiQUBO.py | 56 +++---- .../optimization/SAT/mappings/DinneenISING.py | 14 +- .../optimization/SAT/mappings/DinneenQUBO.py | 11 +- .../optimization/SAT/mappings/QubovertQUBO.py | 14 +- .../applications/optimization/SCP/SCP.py | 4 +- .../applications/optimization/TSP/TSP.py | 25 ++- .../TSP/data/createReferenceGraph.py | 2 + .../optimization/TSP/mappings/ISING.py | 10 +- .../optimization/TSP/mappings/QUBO.py | 5 +- src/modules/circuits/Circuit.py | 4 +- src/modules/circuits/CircuitCardinality.py | 12 +- src/modules/circuits/CircuitCopula.py | 9 +- src/modules/circuits/CircuitStandard.py | 6 +- src/modules/devices/HelperClass.py | 6 +- src/modules/devices/Local.py | 7 +- .../devices/SimulatedAnnealingSampler.py | 4 +- src/modules/devices/braket/Braket.py | 2 +- src/modules/devices/braket/Ionq.py | 7 +- src/modules/devices/braket/LocalSimulator.py | 4 +- src/modules/devices/braket/OQC.py | 7 +- src/modules/devices/braket/Rigetti.py | 7 +- src/modules/devices/braket/SV1.py | 12 +- src/modules/devices/braket/TN1.py | 6 +- .../devices/pulser/MockNeutralAtomDevice.py | 4 +- src/modules/solvers/Annealer.py | 11 +- src/modules/solvers/ClassicalSAT.py | 4 +- src/modules/solvers/GreedyClassicalPVC.py | 5 +- src/modules/solvers/GreedyClassicalTSP.py | 8 +- src/modules/solvers/MIPsolverACL.py | 3 +- src/modules/solvers/NeutralAtomMIS.py | 26 ++-- src/modules/solvers/PennylaneQAOA.py | 44 +++--- src/modules/solvers/QAOA.py | 87 ++++++++--- src/modules/solvers/QiskitQAOA.py | 24 +-- src/modules/solvers/RandomClassicalPVC.py | 5 +- src/modules/solvers/RandomClassicalSAT.py | 5 +- src/modules/solvers/RandomClassicalTSP.py | 4 +- .../solvers/ReverseGreedyClassicalPVC.py | 5 +- .../solvers/ReverseGreedyClassicalTSP.py | 3 +- src/modules/training/Inference.py | 6 + src/modules/training/QCBM.py | 6 + src/modules/training/QGAN.py | 6 + src/utils.py | 6 +- 69 files changed, 566 insertions(+), 464 deletions(-) diff --git a/README.md b/README.md index 2b7c4fc3..95079b2f 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,10 @@ In case you want to use custom modules files (for example, to use external modul You can find the documentation in the respective Read the Docs section. ## Git Large File Storage (LFS) -Some files in this repository are large and tracked using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. +QUARK stores data and config files using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. ### Installing Git LFS -1. Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): +Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): - On Linux/macOS ```bash git lfs install diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d5552083..a01220b4 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -68,11 +68,11 @@ You can find the documentation in the Dynamic Imports section. Git Large File Storage (LFS) ~~~~~~~~~~~~~~~~~~~~ -Some files in this repository are large and tracked using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. +QUARK stores data and config files using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. Installing Git LFS ^^^^^^^^^^^^^^^^^^^ -1. Install Git LFS by following the instructions on `Git LFS `_ +Install Git LFS by following the instructions on `Git LFS `_ - On Linux/macOS :: diff --git a/src/Plotter.py b/src/Plotter.py index 90858386..0805a051 100644 --- a/src/Plotter.py +++ b/src/Plotter.py @@ -146,7 +146,7 @@ def plot_times(application_name: str, application_axis: str, results: list[dict] def plot_application_score(application_name: str, application_axis: str, results: list[dict], store_dir: str) -> None: """ - Funtion to create plots showing the application score. + Function to create plots showing the application score. :param application_name: Name of the application :param application_axis: Name of the application axis @@ -223,7 +223,7 @@ def _extract_columns(config: dict, rest_result: dict) -> dict: Function to extract and summarize certain data fields like the time spent in every module from the nested module chain. - :param config: Dictionary containing multiple data fields like the config a module + :param config: Dictionary containing multiple data fields like the config of a module :param rest_result: Rest of the module chain :return: Extracted data """ diff --git a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py b/src/modules/applications/QML/generative_modeling/GenerativeModeling.py index 4789a8db..6947e9c7 100644 --- a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py +++ b/src/modules/applications/QML/generative_modeling/GenerativeModeling.py @@ -50,6 +50,13 @@ def get_solution_quality_unit(self) -> str: return "minimum KL" def get_default_submodule(self, option: str) -> Union[ContinuousData, DiscreteData]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "Continuous Data": self.data = ContinuousData() elif option == "Discrete Data": diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py index d4fd9869..0c3e39dd 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py @@ -108,7 +108,6 @@ class Config(TypedDict): train_size: float def data_load(self, gen_mod: dict, config: Config) -> dict: - """ The chosen dataset is loaded and split into a training set. @@ -136,7 +135,7 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: def evaluate(self, solution: dict) -> tuple[float, float]: """ - Calculate KL in original space. + Calculates KL in original space. :param solution: a dictionary containing the solution data, including histogram_generated_original and histogram_train_original @@ -160,7 +159,7 @@ def evaluate(self, solution: dict) -> tuple[float, float]: def kl_divergence(self, target: np.ndarray, q: np.ndarray) -> float: """ - Function to calculate KL divergence + Function to calculate KL divergence. :param target: Probability mass function of the target distribution :param q: Probability mass function generated by the quantum circuit diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py index 9824ae93..5120ed03 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py @@ -28,7 +28,7 @@ class DataHandler(Core, ABC): """ The task of the DataHandler module is to translate the application’s data - and problem specification into preproccesed format. + and problem specification into preprocessed format. """ def __init__(self, name: str): @@ -52,7 +52,7 @@ def get_requirements() -> list[dict]: {"name": "tensorboard", "version": "2.17.0"} ] - def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: + def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[any, float]: """ In this module, the preprocessing step is transforming the data to the correct target format. @@ -73,7 +73,7 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f """ In this module, the postprocessing step is transforming the data to the correct target format. - :param input_data: any + :param input_data: Original data :param config: Config specifying the parameters of the training :param kwargs: Optional additional settings :return: Tuple with an output_dictionary and the time it took @@ -90,13 +90,13 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f if self.generalization_mark is not None: self.metrics.add_metric_batch({"KL_best": evaluation["KL_best"]}) - metrics, _ = self.generalisation() + metrics, _ = self.generalization() - # Save generalisation metrics + # Save generalization metrics with open(f"{store_dir_iter}/record_gen_metrics_{kwargs['rep_count']}.pkl", 'wb') as f: pickle.dump(metrics, f) - self.metrics.add_metric_batch({"generalisation_metrics": metrics}) + self.metrics.add_metric_batch({"generalization_metrics": metrics}) else: self.metrics.add_metric_batch({"KL_best": evaluation}) @@ -167,9 +167,9 @@ def data_load(self, gen_mod: dict, config: dict) -> tuple[any, float]: """ pass - def generalisation(self) -> tuple[dict, float]: + def generalization(self) -> tuple[dict, float]: """ - Compute generalisation metrics. + Computes generalization metrics. :return: Evaluation and the time it took to create it """ @@ -181,7 +181,7 @@ def generalisation(self) -> tuple[dict, float]: @abstractmethod def evaluate(self, solution: any) -> tuple[any, float]: """ - Compute the best loss values. + Computes the best loss values. :param solution: Solution data :return: Evaluation data and the time it took to create it diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py index 4c99b698..647a3f0b 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py @@ -29,7 +29,7 @@ class DiscreteData(DataHandler): """ A data handler for discrete datasets with cardinality constraints. This class creates a dataset with a cardinality constraint and provides - methods for generalisation metrics computing and evaluation. + methods for generalization metrics computing and evaluation. """ def __init__(self): @@ -161,11 +161,11 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: return application_config - def generalisation(self) -> tuple[dict, float]: + def generalization(self) -> tuple[dict, float]: """ Calculate generalization metrics for the generated. - :return: A tuple containing a dictionary of generalization metrics and the execution time + :return: Tuple containing a dictionary of generalization metrics and the execution time """ start = start_time_measurement() results = self.generalization_metrics.get_metrics(self.samples) @@ -179,9 +179,9 @@ def evaluate(self, solution: dict) -> tuple[dict, float]: Evaluates a given solution and calculates the histogram of generated samples and the minimum KL divergence value. - :param solution: Dictionary containing the solution data, including generated samples and KL divergence values. - :return: A tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence - value, and the time it took to evaluate the solution. + :param solution: Dictionary containing the solution data, including generated samples and KL divergence values + :return: Tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence + value, and the time it took to evaluate the solution """ start = start_time_measurement() self.samples = solution["best_sample"] diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py b/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py index f2955953..d11baf6f 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py +++ b/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py @@ -19,18 +19,15 @@ class MetricsGeneralization: """ A class to compute generalization metrics for generated samples based on train and solution sets. - - :param train_set: Set of queries in the training set. - :type train_set: np.array - :param train_size: The fraction of queries used for training. - :type train_size: float - :param solution_set: Set of queries in the solution set. - :type solution_set: np.array - :param n_qubits: The number of qubits. - :type n_qubits: int """ - def __init__(self, train_set, train_size, solution_set, n_qubits) -> None: + def __init__(self, train_set: np.array, train_size: float, solution_set: np.array, n_qubits: int): + """ + :param train_set: Set of queries in the training set + :param train_size: The fraction of queries used for training + :param solution_set: Set of queries in the solution set + :param n_qubits: The number of qubits. + """ self.train_set = train_set self.train_size = train_size self.solution_set = solution_set diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py index 84c1fe03..df8501e3 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -32,6 +32,7 @@ logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) + def split_string(s): return s.split(' ', 1)[0] @@ -174,6 +175,7 @@ def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: :param option: The option to select the submodule :return: The selected submodule + :raises NotImplemented: If the provided option is not implemented """ if option == "QCBM": return QCBM() diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index 01461702..6ea660d9 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -96,9 +96,9 @@ def get_execute_circuit(circuit: any, backend: any, config: str, config_dict: di This method combines the circuit implementation and the selected backend and returns a function that will be called during training. - :param circuit: Implementation of the quantum circuiT + :param circuit: Implementation of the quantum circuit :param backend: Configured backend - :param config: Name of the PennyLane devicE + :param config: Name of the PennyLane device :param config_dict: Dictionary including the number of shots :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py index 3e584ba5..3d2bb899 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py @@ -79,6 +79,13 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: + """ + Returns the default submodule based on the provided option. + + :param option: The option to select the submodule + :return: The selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() @@ -169,9 +176,8 @@ def select_backend(config: str, n_qubits: int) -> any: return backend @staticmethod - def get_execute_circuit( - circuit: callable, backend: qml.device, config: str, config_dict: dict - ) -> tuple[any, any]: + def get_execute_circuit(circuit: callable, backend: qml.device, config: str, config_dict: dict) \ + -> tuple[any, any]: """ This method combines the PennyLane circuit implementation and the selected backend and returns a function that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py index cd0ef9e8..b52e295b 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py @@ -83,17 +83,24 @@ def get_parameter_options(self) -> dict: "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", "cusvaer_simulator (only available in cuQuantum appliance)", "aer_simulator_gpu", "aer_simulator_cpu", "ionQ_Harmony", "Amazon_SV1", "ibm_brisbane IBM Quantum Platform"], - "description": "Which backend do you want to use? (aer_statevector_simulator\ - uses the measurement probability vector, the others are shot based)" + "description": "Which backend do you want to use? (aer_statevector_simulator uses the measurement " + "probability vector, the others are shot based)" }, "n_shots": { "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model?\ - (If the aer_statevector_simulator selected, only relevant for studying generalization)" + "description": "How many shots do you want use for estimating the PMF of the model? " + "(If the aer_statevector_simulator selected, only relevant for studying generalization)" } } def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: + """ + Returns the default submodule based on the provided option. + + :param option: The option to select the submodule + :return: The selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() elif option == "QGAN": @@ -194,8 +201,8 @@ def select_backend(config: str, n_qubits: int) -> any: backend = Aer.get_backend('statevector_simulator') backend.set_options(device="CPU") elif config == "ionQ_Harmony": - from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 - from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 + from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 + from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 device_wrapper = Ionq("ionQ", "arn:aws:braket:::device/qpu/ionq/ionQdevice") backend = AWSBraketBackend( device=device_wrapper.device, @@ -206,8 +213,8 @@ def select_backend(config: str, n_qubits: int) -> any: backend_version="2", ) elif config == "Amazon_SV1": - from modules.devices.braket.SV1 import SV1 # pylint: disable=C0415 - from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 + from modules.devices.braket.SV1 import SV1 # pylint: disable=C0415 + from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 device_wrapper = SV1("SV1", "arn:aws:braket:::device/quantum-simulator/amazon/sv1") backend = AWSBraketBackend( device=device_wrapper.device, @@ -224,8 +231,8 @@ def select_backend(config: str, n_qubits: int) -> any: return backend @staticmethod - def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict - ) -> tuple[any, any]: # pylint: disable=W0221,R0915 + def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict) \ + -> tuple[any, any]: # pylint: disable=W0221,R0915 """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py index 297de789..152bf0f1 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py +++ b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -123,6 +123,13 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() elif option == "Inference": @@ -138,6 +145,7 @@ def sequence_to_circuit(self, input_data: dict) -> dict: :param input_data: Collected information of the benchmarking process :return: Same dictionary but the gate sequence is replaced by it Qiskit implementation """ + # TODO: Identical to CustomQiskitNoisyBackend.sequence_to_circuit -> move to Library n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] circuit = QuantumCircuit(n_qubits, n_qubits) @@ -198,6 +206,7 @@ def select_backend(config: str, n_qubits: int) -> Backend: :param n_qubits: Number of qubits :return: Configured qiskit backend """ + # TODO: Identical to CustomQiskitNoisyBackend.select_backend -> move to Library if config == "aer_simulator_gpu": backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") @@ -222,6 +231,7 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit """ + # TODO: Identical to CustomQiskitNoisyBackend.get_execute_circuit -> move to Library n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits start = perf_counter() diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/QML/generative_modeling/transformations/PIT.py index b2f95981..47f94329 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/QML/generative_modeling/transformations/PIT.py @@ -73,6 +73,7 @@ def transform(self, input_data: dict, config: dict) -> dict: :param config: Config with the parameters specified in Config class :return: Dict with PIT transformation, time it took to map it """ + # TODO: PIT.transform is almost identical to MinMax.transform -> function should be moved to Transformation.py self.dataset_name = input_data["dataset_name"] self.dataset = input_data["dataset"] self.n_qubits = input_data["n_qubits"] diff --git a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py index ae64d05b..a52853e0 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py +++ b/src/modules/applications/QML/generative_modeling/transformations/Transformation.py @@ -80,8 +80,8 @@ def transform(self, input_data: dict, config: dict) -> dict: Helps to ensure that the model can effectively learn the underlying patterns and structure of the data, and produce high-quality outputs. - :param input_data: Input data for transformation. - :param config: Configuration parameters for the transformation. + :param input_data: Input data for transformation + :param config: Configuration parameters for the transformation :return: Transformed data. """ return input_data @@ -92,8 +92,8 @@ def reverse_transform(self, input_data: dict) -> dict: This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. - :param input_data: The input data to be transformed. - :return: Transformed data. + :param input_data: The input data to be transformed + :return: Transformed data """ return input_data @@ -102,9 +102,9 @@ def compute_discretization(n_qubits: int, n_registered: int) -> np.ndarray: """ Compute discretization for the grid. - :param n_qubits: Total number of qubits. - :param n_registered: Number of qubits to be registered. - :return: Discretization data. + :param n_qubits: Total number of qubits + :param n_registered: Number of qubits to be registered + :return: Discretization data """ n = 2 ** (n_qubits // n_registered) n_bins = n ** n_registered @@ -121,9 +121,9 @@ def compute_discretization_efficient(n_qubits: int, n_registers: int) -> np.ndar """ Compute grid discretization. - :param n_qubits: Total number of qubits. - :param n_registers: Number of qubits to be registered. - :return: Discretization data. + :param n_qubits: Total number of qubits + :param n_registers: Number of qubits to be registered + :return: Discretization data """ n = 2 ** (n_qubits // n_registers) n_bins = n ** n_registers @@ -144,11 +144,11 @@ def generate_samples(results: np.ndarray, bin_data: np.ndarray, n_registers: int """ Generate samples based on measurement results and the grid bins. - :param results: Results of measurements. - :param bin_data: Binned data. - :param n_registers: Number of registers. - :param noisy: Flag indicating whether to add noise. - :return: Generated samples. + :param results: Results of measurements + :param bin_data: Binned data + :param n_registers: Number of registers + :param noisy: Flag indicating whether to add noise + :return: Generated samples """ n_shots = np.sum(results) width = 1 / len(bin_data) ** (1 / n_registers) @@ -171,11 +171,11 @@ def generate_samples_efficient(results, bin_data: np.ndarray, n_registers: int, """ Generate samples efficiently using numpy arrays based on measurement results and the grid bins. - :param results: Results of measurements. - :param bin_data: Binned data. - :param n_registers: Number of registers. - :param noisy: Flag indicating whether to add noise. - :return: Generated samples. + :param results: Results of measurements + :param bin_data: Binned data + :param n_registers: Number of registers + :param noisy: Flag indicating whether to add noise + :return: Generated samples """ n_shots = np.sum(results) width = 1 / len(bin_data) ** (1 / n_registers) diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index fbda3d90..1bb8f8b5 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -79,9 +79,9 @@ def get_default_submodule(self, option: str) -> Core: """ Returns the default submodule based on the provided option. - :param option: The submodule option to retrieve - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "MIPsolverACL": from modules.solvers.MIPsolverACL import MIPaclp # pylint: disable=C0415 @@ -161,19 +161,22 @@ def generate_problem(self, config: Config) -> dict: # pylint: disable=R0915 self.application = problem_instance return self.application - def _generate_tiny_model(self, df, vehicles): + def _generate_tiny_model(self, df: any, vehicles: list) -> None: """ Generate the problem model for the Tiny configurations. + + :param df: Datafile + :param vehicles: List of vehicle types """ # Weight parameters - # max. total weight on truck / trailer + # Max. total weight on truck / trailer wt = [100] - # max. weight on the four levels + # Max. weight on the four levels wl = [50, 60] - # max. weights on platforms p, if not angled + # Max. weights on platforms p, if not angled wp = [23, 23, 23, 26, 17] - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model + # Create empty lists for different vehicle parameters. This is required for proper indexing in the model. weight_list = [0] * (len(vehicles)) for i, vehicle in enumerate(vehicles): @@ -230,9 +233,12 @@ def _generate_tiny_model(self, df, vehicles): self.application = prob - def _generate_small_model(self, df, vehicles): + def _generate_small_model(self, df: any, vehicles: list) -> None: """ Generate the problem model for the Small configuration + + :param df: Datafile + :param vehicles: List of vehicle types """ # Parameters for the small model (2 levels with 3 and 2 platforms each) @@ -243,13 +249,13 @@ def _generate_small_model(self, df, vehicles): # Considers base truck height and height distance between vehicles (~10cm) hmax_truck = [34, 34, 33, 36, 32, 36] # Weight parameters - # max. total weight on truck / trailer + # Max. total weight on truck / trailer wt = [100] - # max. weight on the two levels + # Max. weight on the two levels wl = [65, 50] - # max. weights on platforms p, if not angled + # Max. weights on platforms p, if not angled wp = [23, 23, 23, 26, 17] - # max. weight on p, if sp is used + # Max. weight on p, if sp is used wsp = [28, 28, 28] _, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) @@ -314,7 +320,7 @@ def _generate_small_model(self, df, vehicles): # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length for L in plats_l: prob += (pulp.lpSum(x[p, v] * length_list[v] for p in platforms_level_array[L] for v in vecs) - <= lmax_l[L]) + <= lmax_l[L]) # (6) Height constraints for truck and trailer, analogue to length constraints # Truck @@ -351,9 +357,12 @@ def _generate_small_model(self, df, vehicles): self.application = prob - def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 + def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disable=R0915 """ Generate the problem model for the Full configuration. + + :param df: Datafile + :param vehicles: List of vehicle types """ # Horizontal Coefficients: Length reduction # 1 = forward, 0 = backward @@ -363,8 +372,8 @@ def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 # Vertical Coefficients: Height increase h_coef = np.array([[0.40, 1, 1, 1], - [0.17, 0.22, 0.21, 0.22], - [0.17, 0.38, 0.32, 0.32]]) + [0.17, 0.22, 0.21, 0.22], + [0.17, 0.38, 0.32, 0.32]]) # Length parameters # Level 1 (Truck up), 2 (Truck down), 3 (Trailer up), 4 (Trailer down) @@ -376,17 +385,17 @@ def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 hmax_trailer = [36, 32, 32, 34] # Weight parameters - # max. total weight + # Max. total weight wmax = 180 - # max. total weight on truck / trailer + # Max. total weight on truck / trailer wt = [100, 100] - # max. weight on the four levels + # Max. weight on the four levels wl = [50, 60, 50, 90] - # max. weights on platforms p, if not angled + # Max. weights on platforms p, if not angled wp = [23, 23, 23, 26, 17, 26, 26, 26, 23, 26] - # max. weights on p, angled (if possible: 1, 2, 4, 7, 8, 9): + # Max. weights on p, angled (if possible: 1, 2, 4, 7, 8, 9): wpa = [20, 22, 17, 18, 19, 22] - # max. weight on p, if sp is used + # Max. weight on p, if sp is used wsp = [28, 28, 28, 28, 28, 28] class_list, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) @@ -458,7 +467,7 @@ def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used for q in plats_sp: - prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs)\ + prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] # (3.1) It is always only possible to use a single split-platform for any given p @@ -516,37 +525,37 @@ def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 # platform v(p) for L in plats_l: prob += pulp.lpSum(x[p, v] * length_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][0]*length_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][1]*length_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][2]*length_list[v]) - - xay4[platforms_angled_array.index(p), v] - * int(v_coef[class_list[v]][3]*length_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) - for v in vecs)\ + - xay1[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][0] * length_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][1] * length_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][2] * length_list[v]) + - xay4[platforms_angled_array.index(p), v] + * int(v_coef[class_list[v]][3] * length_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) + for v in vecs) \ + pulp.lpSum(x[p, v] * length_list[v] - for p in self.diffset(platforms_level_array[L], platforms_angled_array) - for v in vecs) \ + for p in self.diffset(platforms_level_array[L], platforms_angled_array) + for v in vecs) \ <= lmax_l[L] # (5) Platforms can not be angled, if they are part of a split platform for q in plats_sp: prob += pulp.lpSum(a_p[platforms_angled_array.index(p)] - for p in self.intersectset(platforms_angled_array, split_platforms_array[q]))\ + for p in self.intersectset(platforms_angled_array, split_platforms_array[q])) \ <= len(split_platforms_array[q]) * (1 - sp[q]) # (6) Weight constraint if split platform is used, gamma == 1 for q in plats_sp: prob += pulp.lpSum(sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) - for v in vecs) >= 2 * gamma[q] + for v in vecs) >= 2 * gamma[q] # If split platform is used, weight limit == wsp, if not, then weight limit == wp for q in plats_sp: for p in split_platforms_array[q]: prob += (pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] + (1 - gamma[q]) * - wp[p]) + wp[p]) # (7) If a platform that can be angled is angled, weight limit == wpa # Need another linearization for that: @@ -563,12 +572,12 @@ def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 # (8) Weight constraint for every level for p_l in plats_l: prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= - wl[p_l]) + wl[p_l]) # (9) Weight constraint for truck and trailer for p_t in plats_t: prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) - <= wt[p_t]) + <= wt[p_t]) # (10) Weight constraint for entire auto carrier prob += pulp.lpSum(weight_list[v] * x[p, v] for p in plats for v in vecs) <= wmax @@ -577,42 +586,41 @@ def _generate_full_model(self, df, vehicles): # pylint: disable=R0915 # Truck for h in plats_h1: prob += pulp.lpSum(x[p, v] * height_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][0]*height_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][1]*height_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][2]*height_list[v]) - - xay4[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][3]*height_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) - for v in vecs)\ + - xay1[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][0] * height_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][1] * height_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][2] * height_list[v]) + - xay4[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][3] * height_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) + for v in vecs) \ + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) - for v in vecs) \ + for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) + for v in vecs) \ <= hmax_truck[h] # Trailer for h in plats_h2: prob += pulp.lpSum(x[p, v] * height_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][0]*height_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][1]*height_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][2]*height_list[v]) - - xay4[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][3]*height_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) - for v in vecs)\ + - xay1[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][0] * height_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][1] * height_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][2] * height_list[v]) + - xay4[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][3] * height_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) + for v in vecs) \ + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) - for v in vecs) \ + for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) + for v in vecs) \ <= hmax_trailer[h] - self.application = prob - def _get_vehicle_params(self, df, vehicles): + def _get_vehicle_params(self, df: any, vehicles: list) -> tuple[list, list, list, list]: """ Extract vehicle parameters for the problem formulation diff --git a/src/modules/applications/optimization/ACL/mappings/ISING.py b/src/modules/applications/optimization/ACL/mappings/ISING.py index f9e072e2..ee4722a7 100644 --- a/src/modules/applications/optimization/ACL/mappings/ISING.py +++ b/src/modules/applications/optimization/ACL/mappings/ISING.py @@ -36,7 +36,7 @@ def __init__(self): """ super().__init__() self.submodule_options = ["QAOA", "QiskitQAOA"] - self.global_variables = 0 + self.global_variables = [] logging.warning("Currently, all scenarios are too large to be solved with an Ising model.") logging.warning("Consider using another mapping until the modelling is refined.") @@ -120,6 +120,7 @@ def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ Use Ising mapping of qiskit-optimize. + :param problem: Dict containing the problem parameters :param config: Config with the parameters specified in Config class :return: Dict with the Ising, time it took to map it """ @@ -157,7 +158,7 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. - :param solution: bit_string containing the solution + :param solution: Dict with a bit_string containing the solution :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() @@ -192,10 +193,11 @@ def _convert_ising_to_qubo(solution: any) -> any: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option - :return: Default submodule + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index e4ef2355..bc99746c 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -26,6 +26,8 @@ from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement +# TODO Large chunks of this code is duplicated in ACL.mappings.ISING -> unify + class Qubo(Mapping): """ @@ -38,7 +40,7 @@ def __init__(self): """ super().__init__() self.submodule_options = ["Annealer"] - self.global_variables = 0 + self.global_variables = [] @staticmethod def get_requirements() -> list[dict]: @@ -93,7 +95,7 @@ def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: qp.integer_var(lowerbound=lb, upperbound=ub, name=name) # Objective function - obj_arguments = {arg["name"]: arg["value"] for arg in problem["objective"]["coefficients"] } + obj_arguments = {arg["name"]: arg["value"] for arg in problem["objective"]["coefficients"]} # Maximize if problem["parameters"]["sense"] == -1: @@ -183,12 +185,12 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra and variable in argument and variable2 in argument and variable > variable2 ): parameter += argument[0] - # this value is already taking into account the factor 2 from quadratic term - # For the variables on the diagonal, if the parameter is zero - # we still have to check the sign in - # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. - elif isinstance(argument, str) and variable in argument \ - and variable2 in argument and variable == variable2: + # This value is already taking into account the factor 2 from quadratic term + # For the variables on the diagonal, if the parameter is zero + # We still have to check the sign in + # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. + elif (isinstance(argument, str) and variable in argument + and variable2 in argument and variable == variable2): if "-" in argument: parameter += -1 @@ -203,6 +205,7 @@ def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ Converts linear program created with pulp to quadratic program to Ising with qiskit to QUBO matrix. + :param problem: Dict containing the problem parameters :param config: Config with the parameters specified in Config class :return: Dict with the QUBO, time it took to map it """ @@ -259,10 +262,11 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option - :return: Default submodule + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/MIS/MIS.py b/src/modules/applications/optimization/MIS/MIS.py index 669341f3..7037c9ee 100644 --- a/src/modules/applications/optimization/MIS/MIS.py +++ b/src/modules/applications/optimization/MIS/MIS.py @@ -31,7 +31,7 @@ class MIS(Optimization): """ The maximum independent set (MIS) problem is a combinatorial optimization problem that seeks to find the largest subset of vertices in a graph such that no two vertices are adjacent. MIS has numerous application in computer - science, network design, resource allocation, and even in physics, where finding optimal confiigurations can + science, network design, resource allocation, and even in physics, where finding optimal configurations can solve fundamental problems related to stability and energy minimization. In a graph, the maximum independent set represents a set of nodes such that no two nodes share an edge. This @@ -70,11 +70,11 @@ def get_solution_quality_unit(self) -> str: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: Submodule option to retrieve - :return: Corresponding submodule object - :raises NotImplementedError: If the option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "NeutralAtom": from modules.applications.optimization.MIS.mappings.NeutralAtom import NeutralAtom # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/MIS/data/graph_layouts.py b/src/modules/applications/optimization/MIS/data/graph_layouts.py index 6708c20b..c66abded 100644 --- a/src/modules/applications/optimization/MIS/data/graph_layouts.py +++ b/src/modules/applications/optimization/MIS/data/graph_layouts.py @@ -21,28 +21,20 @@ # define R_rydberg R_rydberg = 9.75 -def generate_hexagonal_graph( - n_nodes: int, - spacing: float, - filling_fraction: float=1.0 -)-> nx.Graph: + +def generate_hexagonal_graph(n_nodes: int, spacing: float, filling_fraction: float = 1.0) -> nx.Graph: """ Generate a hexagonal graph layout based on the number of nodes and spacing. - Args: - n_nodes (int): The number of nodes in the graph. - spacing (float): The spacing between nodes (atoms). - filling_fraction (float): The fraction of available places in the - lattice to be filled with nodes. (default: 1.0) - - Returns: - nx.Graph: Networkx Graph representing the hexagonal graph layout + :param n_nodes: The number of nodes in the graph + :param spacing: The spacing between nodes (atoms) + :param filling_fraction: The fraction of available places in the lattice to be filled with nodes. (default: 1.0) + :return: Networkx Graph representing the hexagonal graph layout """ if not 0.0 < filling_fraction <= 1.0: raise ValueError("The filling fraction must be in the domain of (0.0, 1.0].") - # Create a layout large enough to contain the desired number of atoms at - # the filling fraction + # Create a layout large enough to contain the desired number of atoms at the filling fraction n_traps = int(n_nodes / filling_fraction) hexagonal_layout = pulser.register.special_layouts.TriangularLatticeLayout( n_traps=n_traps, spacing=spacing @@ -51,7 +43,7 @@ def generate_hexagonal_graph( # Fill the layout with traps reg = hexagonal_layout.hexagonal_register(n_traps) ids = reg._ids # pylint: disable=W0212 - coords = [coord.tolist() for coord in reg._coords] # pylint: disable=W0212 + coords = [coord.tolist() for coord in reg._coords] # pylint: disable=W0212 traps = dict(zip(ids, coords)) # Remove random atoms to get the desired number of atoms @@ -60,7 +52,7 @@ def generate_hexagonal_graph( traps.pop(atom_to_remove) # Rename the atoms - node_positions = {i: traps[trap] for i, trap in enumerate(traps.keys())} # pylint: disable=C0206 + node_positions = {i: traps[trap] for i, trap in enumerate(traps.keys())} # pylint: disable=C0206 # Create the graph hexagonal_graph = nx.Graph() @@ -76,26 +68,19 @@ def generate_hexagonal_graph( return hexagonal_graph -def _generate_edges(node_positions: dict[int, list[float]], radius: float = R_rydberg) -> list[tuple[int, int]]: +def _generate_edges(node_positions: dict[list[int, list[float]]], radius: float = R_rydberg) -> list[tuple]: """ - Generate edges between vertices within a given distance 'radius', which - defaults to R_rydberg. - - Parameters: - node_positions (dict): A dictionary with the node ids as keys, and the node coordinates as values. - radius (float): When the distance between two nodes is smaller than this radius, an edge is generated between them. + Generate edges between vertices within a given distance 'radius', which defaults to R_rydberg. - Returns: - list[tuple]: A list of 2-tuples. Each 2-tuple contains two different node ids and - represents an edge between those nodes. + :param node_positions: A dictionary with the node ids as keys, and the node coordinates as values + :param radius: When the distance between two nodes is smaller than this radius, an edge is generated between them + :return: A list of 2-tuples. Each 2-tuple contains two different node ids and represents an edge between those nodes """ edges = [] vertex_keys = list(node_positions.keys()) for i, vertex_key in enumerate(vertex_keys): for neighbor_key in vertex_keys[i + 1:]: - distance = _vertex_distance( - node_positions[vertex_key], node_positions[neighbor_key] - ) + distance = _vertex_distance(node_positions[vertex_key], node_positions[neighbor_key]) if distance <= radius: edges.append((vertex_key, neighbor_key)) return edges @@ -106,15 +91,10 @@ def _vertex_distance(v0: tuple[float, ...], v1: tuple[float, ...]) -> float: Calculates distance between two n-dimensional vertices. For 2 dimensions: distance = sqrt((x0 - x1)**2 + (y0 - y1)**2) - Args: - v0 (tuple): Coordinates of the first vertex. - v1 (tuple): Coordinates of the second vertex. - - Returns: - float: Distance between the vertices. + :param v0: Coordinates of the first vertex + :param v1: Coordinates of the second vertex + return: Distance between the vertices """ - squared_difference = sum( - (coordinate0 - coordinate1) ** 2 for coordinate0, coordinate1 in zip(v0, v1) - ) + squared_difference = sum((coordinate0 - coordinate1) ** 2 for coordinate0, coordinate1 in zip(v0, v1)) return math.sqrt(squared_difference) diff --git a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py index 27c69fc5..04c1bd9b 100644 --- a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py +++ b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py @@ -78,11 +78,11 @@ def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: Submodule option to retrieve - :return: Corresponding submodule object - :raises NotImplementedError: If the option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "NeutralAtomMIS": from modules.solvers.NeutralAtomMIS import NeutralAtomMIS # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/Optimization.py b/src/modules/applications/optimization/Optimization.py index cffbfa39..aa09c6e0 100644 --- a/src/modules/applications/optimization/Optimization.py +++ b/src/modules/applications/optimization/Optimization.py @@ -20,7 +20,7 @@ class Optimization(Application, ABC): """ - Optimization Module for QUARK, is used by all Optimization applications + Optimization Module for QUARK, is used by all Optimization applications. """ @abstractmethod @@ -48,7 +48,7 @@ def evaluate(self, solution: any) -> tuple[float, float]: Checks how good the solution is. :param solution: Provided solution - :return: Tuple witht the evaluation and the time it took to create it + :return: Tuple with the evaluation and the time it took to create it """ pass diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index 79af8184..d07c0756 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -74,11 +74,11 @@ def get_solution_quality_unit(self) -> str: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option to retrieve - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "Ising": from modules.applications.optimization.PVC.mappings.ISING import Ising # pylint: disable=C0415 @@ -108,7 +108,7 @@ def get_parameter_options(self) -> dict: return { "seams": { "values": list(range(1, 18)), - "description": "How many seams does your graph need?" + "description": "How many seams should be sealed?" } } """ @@ -116,7 +116,7 @@ def get_parameter_options(self) -> dict: "seams": { "values": list(range(1, 18)), # In the current implementation the graph can only be as large as the reference input graph - "description": "How many seams does you graph need?" + "description": "How many seams should be sealed?" } } @@ -162,13 +162,13 @@ def generate_problem(self, config: Config) -> nx.Graph: logging.error("Graph is not connected!") raise ValueError("Graph is not connected!") - #Gather unique configurations and tools + # Gather unique configurations and tools config = [x[2]['c_start'] for x in graph.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in graph.edges(data=True)])) tool = [x[2]['t_start'] for x in graph.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in graph.edges(data=True)])) - # fill the rest of the missing edges with high values + # Fill the rest of the missing edges with high values current_edges = [ (edge[0], edge[1], edge[2]['t_start'], edge[2]['t_end'], edge[2]['c_start'], edge[2]['c_end']) for edge in graph.edges(data=True) @@ -185,7 +185,7 @@ def generate_problem(self, config: Config) -> nx.Graph: missing_edges = [item for item in all_possible_edges if item not in current_edges] - # add these edges with very high values + # Add these edges with very high values for edge in missing_edges: graph.add_edge( edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], weight=100000 @@ -199,7 +199,7 @@ def generate_problem(self, config: Config) -> nx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: dict) -> tuple[list, bool]: + def process_solution(self, solution: dict) -> tuple[list, float]: """ Converts solution dictionary to list of visited seams. @@ -235,7 +235,7 @@ def process_solution(self, solution: dict) -> tuple[list, bool]: if node is None: route[idx] = nodes_unassigned.pop(0) - # cycle solution to start at provided start location + # Cycle solution to start at provided start location if start is not None and route[0] != start: idx = route.index(start) route = route[idx:] + route[:idx] diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index a60031fc..dfd45ec5 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -62,14 +62,14 @@ def get_parameter_options(self) -> dict: return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" + "description": "By which factor would you like to multiply your Lagrange?" } } """ return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" + "description": "By which factor would you like to multiply your Lagrange?" } } @@ -143,11 +143,11 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option to retrieve - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/PVC/mappings/QUBO.py b/src/modules/applications/optimization/PVC/mappings/QUBO.py index df60355d..3d3c8b80 100644 --- a/src/modules/applications/optimization/PVC/mappings/QUBO.py +++ b/src/modules/applications/optimization/PVC/mappings/QUBO.py @@ -54,7 +54,7 @@ def get_parameter_options(self) -> dict: return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" + "description": "By which factor would you like to multiply your Lagrange?" } } @@ -62,7 +62,7 @@ def get_parameter_options(self) -> dict: return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" + "description": "By which factor would you like to multiply your Lagrange?" } } @@ -81,7 +81,7 @@ def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: Maps the networkx graph to a QUBO formulation. :param problem: Networkx graph representing the PVC problem - :param config: config dictionary with the mapping configuration + :param config: Config dictionary with the mapping configuration :return: Tuple containing the QUBO dictionary and the time it took to map it """ # Inspired by https://dnx.readthedocs.io/en/latest/_modules/dwave_networkx/algorithms/tsp.html @@ -128,25 +128,24 @@ def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: for c_start in config: q[((node, c_start, t_start, pos_1), (node, c_start, t_start, pos_1))] -= lagrange for t_end in tool: - # for all configs and tools + # For all configs and tools for c_end in config: if c_start != c_end or t_start != t_end: q[((node, c_start, t_start, pos_1), (node, c_end, t_end, pos_1))] += 1.0 * lagrange for pos_2 in range(pos_1 + 1, timesteps): - # penalize visiting same node again in another timestep + # Penalize visiting same node again in another timestep q[((node, c_start, t_start, pos_1), (node, c_end, t_end, pos_2))] += 2.0 * lagrange - # penalize visiting other node of same seam + # Penalize visiting other node of same seam if node != (0, 0): # (0,0) is the base node, it is not a seam - # get the other nodes of the same seam + # Get the other nodes of the same seam other_seam_nodes = [ x for x in problem.nodes if x[0] == node[0] and x[1] != node ] for other_seam_node in other_seam_nodes: - # penalize visiting other node of same seam - q[( - (node, c_start, t_start, pos_1), (other_seam_node, c_end, t_end, pos_2)) - ] += 2.0 * lagrange + # Penalize visiting other node of same seam + q[((node, c_start, t_start, pos_1), + (other_seam_node, c_end, t_end, pos_2))] += 2.0 * lagrange # Constraint to only visit a single node in a single timestep for pos in range(timesteps): @@ -172,15 +171,15 @@ def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: if item["c_start"] == c_start and item["t_start"] == t_start and item["c_end"] == c_end and item["t_end"] == t_end ) - # since it is the other direction we switch start and end of tool and config + # Since it is the other direction we switch start and end of tool and config edge_v_u = next( item for item in list(problem[v][u].values()) if item["c_start"] == c_end and item["t_start"] == t_end and item["c_end"] == c_start and item["t_end"] == t_start ) - # going from u -> v + # Going from u -> v q[((u, c_start, t_start, pos), (v, c_end, t_end, nextpos))] += edge_u_v['weight'] - # going from v -> u + # Going from v -> u q[((v, c_end, t_end, pos), (u, c_start, t_start, nextpos))] += edge_v_u['weight'] logging.info("Created Qubo") @@ -189,11 +188,11 @@ def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option to retrieve - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/SAT.py b/src/modules/applications/optimization/SAT/SAT.py index d4d4c41d..abb62310 100644 --- a/src/modules/applications/optimization/SAT/SAT.py +++ b/src/modules/applications/optimization/SAT/SAT.py @@ -20,7 +20,7 @@ from nnf import Var, And, Or from nnf.dimacs import dump -from modules.applications.Application import Application +from modules.Core import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement @@ -79,7 +79,7 @@ def get_requirements() -> list[dict]: def get_solution_quality_unit(self) -> str: return "Evaluation" - def get_default_submodule(self, option: str) -> Application: + def get_default_submodule(self, option: str) -> Core: """ Returns the default submodule based on the provided option. @@ -114,7 +114,7 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for this application. - :return: Dictionary with cnfigurable settings + :return: Dictionary with configurable settings .. code-block:: python return { @@ -130,14 +130,14 @@ def get_parameter_options(self) -> dict: "custom_input": True, "allow_ranges": True, "postproc": int, - "description": "What clause:variable ratio do you want for the (hard) constraints?" + "description": "What clause-to-variable ratio do you want for the (hard) constraints?" }, "clvar_ratio_test": { "values": [2, 3, 4, 4.2, 5], "custom_input": True, "allow_ranges": True, "postproc": int, - "description": "What clause:variable ratio do you want for the tests (soft con.)?" + "description": "What clause-to-variable ratio do you want for the tests (soft con.)?" }, "problem_set": { "values": list(range(10)), @@ -145,7 +145,7 @@ def get_parameter_options(self) -> dict: }, "max_tries": { "values": [100], - "description": "Maximum number of tries to create problem" + "description": "Maximum number of tries to create problem?" } } """ @@ -162,14 +162,14 @@ def get_parameter_options(self) -> dict: "custom_input": True, "allow_ranges": True, "postproc": int, - "description": "What clause:variable ratio do you want for the (hard) constraints?" + "description": "What clause-to-variable ratio do you want for the (hard) constraints?" }, "clvar_ratio_test": { "values": [2, 3, 4, 4.2, 5], "custom_input": True, "allow_ranges": True, "postproc": int, - "description": "What clause:variable ratio do you want for the tests (soft constraints)?" + "description": "What clause-to-variable ratio do you want for the tests (soft constraints)?" }, "problem_set": { "values": list(range(10)), @@ -177,7 +177,7 @@ def get_parameter_options(self) -> dict: }, "max_tries": { "values": [100], - "description": "Maximum number of tries to create problem" + "description": "Maximum number of tries to create problem?" } } @@ -219,9 +219,9 @@ def generate_problem(self, config: Config) -> tuple[nnf.And, list]: self.application = {} def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): - # iterate over the desired number of attempts: break if we find a solvable instance. + # Iterate over the desired number of attempts: break if we find a solvable instance. for attempt in range(nr_tries): - # initialize random number generator -- multiply the attempt to traverse distinct random seeds + # Initialize random number generator -- multiply the attempt to traverse distinct random seeds # for the hard and soft constraints, respectively (since rseed of the hard and soft constraints differs # by 1). rng = np.random.default_rng(rseed + attempt * 2) @@ -280,7 +280,7 @@ def validate(self, solution: dict) -> tuple[bool, float]: Validate a given solution against the constraints. :param solution: The solution to validate - :return: True if the solution is valid, False otherwise, time it took to complete + :return: True if the solution is valid, False otherwise, and time it took to complete """ start = start_time_measurement() diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py index 84b292d2..63f463d7 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py @@ -59,7 +59,7 @@ def get_parameter_options(self) -> dict: return { "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + "description": "What Bh/A ratio do you want? (How strongly to enforce hard constraints)" }, "soft_reward": { "values": [0.1, 1, 2], @@ -71,7 +71,7 @@ def get_parameter_options(self) -> dict: return { "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + "description": "What Bh/A ratio do you want? (How strongly to enforce hard constraints)" }, "soft_reward": { "values": [0.1, 1, 2], @@ -96,7 +96,7 @@ def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Uses the ChoiQUBO formulation and converts it to an Ising. - :param problem: the SAT problem + :param problem: SAT problem :param config: Dictionary with the mapping config :return: Dict with the ising, time it took to map it """ @@ -140,11 +140,11 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py index 3ca89768..5ecf048e 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py @@ -55,7 +55,7 @@ def get_parameter_options(self) -> dict: return { "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + "description": "What Bh/A ratio do you want? (How strongly to enforce hard constraints)" }, "soft_reward": { "values": [0.1, 1, 2], @@ -69,7 +69,7 @@ def get_parameter_options(self) -> dict: "values": [0.1, 0.5, 0.9, 0.99], "description": ( "What Bh/A ratio do you want?" - "(How strongly to enforce hard cons.)" + "(How strongly to enforce hard constraints)" ) }, "soft_reward": { @@ -100,18 +100,18 @@ def map(self, problem: tuple[And, list], config: Config) -> tuple[dict, float]: solving MaxSAT then corresponds to solving an instance of the Maximal Independent Set problem. See Andrew Lucas (2014), or the original publication by Choi (1004.2226). - :param problem: A tuple conatining hard and soft constraints + :param problem: A tuple containing hard and soft constraints :param config: Config with the parameters specified in Config class :return: Dictionary containing the QUBO representation and the time taken """ start = start_time_measurement() hard_constraints, soft_constraints = problem - A = 1 - Bh = config['hard_reward'] * A + a = 1 + bh = config['hard_reward'] * a # divide Bh by the number of test clauses, such that fulfilling a test result is less favourable than # satisfying a constraint, which aim to prioritize. - Bs = Bh * config['soft_reward'] / len(soft_constraints) + bs = bh * config['soft_reward'] / len(soft_constraints) # Count the number of different variables that appear in the vehicle options problem: self.nr_vars = len(hard_constraints.vars().union(And(soft_constraints).vars())) @@ -125,7 +125,7 @@ def _add_clause(clause, curr_edges, curr_lit_occ, pos): # Connect the literals within one clause for cmb in combinations(literals, 2): # Add a weight for each edge within clause - curr_edges[cmb] = A + curr_edges[cmb] = a # Add the occurrences of the variables to the occurrences dictionary for var in clause.children: if var.name not in curr_lit_occ.keys(): @@ -152,7 +152,7 @@ def _add_clause(clause, curr_edges, curr_lit_occ, pos): # Employ the notation from nnf, where the tilde symbol ~ corresponds to negation. lit_true, lit_false = f"{literal}-{pos_true}", f"~{literal}-{pos_false}" # Add a penalty for each such edge: - edges[(lit_true, lit_false)] = A + edges[(lit_true, lit_false)] = a # Collect all different nodes that we have in our graph, omitting repetitions: node_set = set([]) @@ -169,21 +169,21 @@ def _remap_pair(pair): """Small helper function that maps the nodes of an edge to binary variables""" return relabel_dict[pair[0]], relabel_dict[pair[1]] - # Save the Qubo corresponding to the graph. - Q = {_remap_pair(key): val for key, val in edges.items()} + # Save the QUBO corresponding to the graph. + q = {_remap_pair(key): val for key, val in edges.items()} for v in node_list: # Add different energy rewards depending on whether it is a hard or a soft constraint if int(v.split('-')[-1]) < constraints_max_ind: # if hard cons, add -Bh as the reward - Q[_remap_pair((v, v))] = -Bh + q[_remap_pair((v, v))] = -bh else: # for soft constraints, add -Bs - Q[_remap_pair((v, v))] = -Bs + q[_remap_pair((v, v))] = -bs - logging.info(f"Converted to Choi Qubo with {len(node_list)} binary variables. Bh={config['hard_reward']}," - f" Bs={Bs}.") - return {'Q': Q}, end_time_measurement(start) + logging.info(f"Converted to Choi QUBO with {len(node_list)} binary variables. Bh={config['hard_reward']}," + f" Bs={bs}.") + return {'Q': q}, end_time_measurement(start) def reverse_map(self, solution: dict) -> tuple[dict, float]: """ @@ -193,28 +193,28 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() - # we define the literals list, so that we can check the self-consistency of the solution. That is, we save all + # We define the literals list, so that we can check the self-consistency of the solution. That is, we save all # assignments proposed by the annealer, and see if there is no contradiction. (In principle a solver # could mandate L3 = True and L3 = False, resulting in a contradiction.) literals = [] - # assignments saves the actual solution + # Assignments saves the actual solution assignments = [] for node, tf in solution.items(): # Check if node is included in the set (i.e. if tf is True (1)) if tf: - # convert back to the language of literals + # Convert back to the language of literals lit_str = self.reverse_dict[node] # Check if the literal is negated: if lit_str.startswith('~'): - # remove the negation symbol + # Remove the negation symbol lit_str = lit_str.replace('~', '') - # save a negated literal object, will be used for self-consistency check + # Save a negated literal object, will be used for self-consistency check lit = Var(lit_str).negate() - # add the negated literal to the assignments, removing the (irrelevant) position part + # Add the negated literal to the assignments, removing the (irrelevant) position part assignments.append(Var(lit_str.split('-')[0]).negate()) else: - # if literal is true, no ~ symbol needs to be removed: + # If literal is true, no ~ symbol needs to be removed: lit = Var(lit_str) assignments.append(Var(lit_str.split('-')[0])) literals.append(lit) @@ -226,10 +226,10 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: # If the solution is consistent, find and add potentially missing variables: assignments = sorted(set(assignments)) - # find missing vars, or more precisely, their labels: + # Find missing vars, or more precisely, their labels: missing_vars = set(range(self.nr_vars)) - {int(str(a).replace('L', '').replace('~', '')) for a in assignments} - # add the variables that found were missing: + # Add the variables that found were missing: for nr in missing_vars: assignments.append(Var(f'L{nr}')) @@ -237,11 +237,11 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py index f6a97ba6..5cc52451 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py @@ -61,7 +61,7 @@ def get_parameter_options(self) -> dict: return { "lagrange": { "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) " + "description": "What Lagrange parameter to multiply with the number of (hard) " "constraints?" } } @@ -69,7 +69,7 @@ def get_parameter_options(self) -> dict: return { "lagrange": { "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) constraints?" + "description": "What Lagrange parameter to multiply with the number of (hard) constraints?" } } @@ -88,7 +88,7 @@ def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Uses the DinneenQUBO formulation and converts it to an Ising. - :param problem: the SAT problem + :param problem: SAT problem :param config: Dictionary with the mapping config :return: Dict with the ising, time it took to map it """ @@ -132,11 +132,11 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Returns the default submodule for the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option - :return: The default submodule for the given option - :return NotImplementedError: If the submodule option is not implemented + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py index 89bf8863..5a700f29 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py @@ -54,14 +54,14 @@ def get_parameter_options(self) -> dict: return { "lagrange": { "values": [0.1, 1, 2], - "description": "What lagrange param. to multiply with the number of (hard) constr.?" + "description": "What Lagrange param. to multiply with the number of (hard) constr.?" } } """ return { "lagrange": { "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) constraints?" + "description": "What Lagrange parameter to multiply with the number of (hard) constraints?" } } @@ -185,10 +185,11 @@ def reverse_map(self, solution: dict) -> tuple[dict, float]: def get_default_submodule(self, option: str) -> Core: """ - Return the default submodule based on the given option. + Returns the default submodule based on the provided option. - :param option: The submodule option - :return: The default submodule + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py index 745e3dee..fee12035 100644 --- a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py @@ -58,14 +58,14 @@ def get_parameter_options(self) -> dict: return { "lagrange": { "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], - "description": "What lagrange for the qubo mapping? 1 the number of tests." + "description": "By which factor would you like to multiply your Lagrange?" } } """ return { "lagrange": { "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], - "description": "What lagrange for the qubo mapping? 1 the number of tests." + "description": "By which factor would you like to multiply your Lagrange?" } } @@ -81,9 +81,9 @@ class Config(TypedDict): lagrange: float @staticmethod - def _constraints2qubovert(constraints: dict) -> AND: + def _constraints2qubovert(constraints: any) -> AND: """ - Converts the constraints nnf to a pubo in the qubovert library. + Converts the constraints nnf to a PUBO in the qubovert library. :param constraints: Constraints in nnf format :return: Constraints in qubovert format @@ -97,7 +97,7 @@ def _constraints2qubovert(constraints: dict) -> AND: @staticmethod def _tests2qubovert(test_clauses: dict) -> sum: """ - Converts the list of test clauses in the nnf format to a pubo. + Converts the list of test clauses in the nnf format to a PUBO. :param test_clauses: Test clauses in nnf format :return: Sum of mapped test clauses @@ -111,7 +111,7 @@ def _tests2qubovert(test_clauses: dict) -> sum: def map(self, problem: any, config: Config) -> tuple[dict, float]: """ - Converts the problem to a Qubo in dictionary format. Problem is a CNF formula from the nnf library. + Converts the problem to a QUBO in dictionary format. Problem is a CNF formula from the nnf library. :param problem: SAT problem :param config: Config with the parameters specified in Config class @@ -133,7 +133,7 @@ def map(self, problem: any, config: Config) -> tuple[dict, float]: logging.info(f'{tests_pubo.to_qubo().num_terms} number of terms in tests qubo') lagrange *= len(test_clauses) - # Define the total pubo problem: + # Define the total PUBO problem: self.pubo_problem = -(tests_pubo + lagrange * constraints_pubo) # Convert to qubo: diff --git a/src/modules/applications/optimization/SCP/SCP.py b/src/modules/applications/optimization/SCP/SCP.py index 04c6f337..7ac11ada 100644 --- a/src/modules/applications/optimization/SCP/SCP.py +++ b/src/modules/applications/optimization/SCP/SCP.py @@ -26,9 +26,7 @@ class SCP(Optimization): The set cover problem (SCP) is a classical combinatorial optimization problem where the objective is to find the smallest subset of given elements that covers all required elements in a collection. This can be formulated as selecting the minimum number of sets from a collection such that the union of the selected sets contains all - elements from the universe of the problem instance. SCP is known for being computationally challenging due to its - NP-hard nature, which means finding an optimal solution becomes exponentially more difficult as the size of the - problem grows. + elements from the universe of the problem instance. SCP has widespread applications in various fields, including sensor positioning, resource allocation, and network design. For example, in sensor positioning, SCP can help determine the fewest number of sensors required to cover diff --git a/src/modules/applications/optimization/TSP/TSP.py b/src/modules/applications/optimization/TSP/TSP.py index f104a26f..4f16c0df 100644 --- a/src/modules/applications/optimization/TSP/TSP.py +++ b/src/modules/applications/optimization/TSP/TSP.py @@ -35,9 +35,7 @@ class TSP(Optimization): TSP as graph problem: The solution to the TSP can be viewed as a specific ordering of the vertices in a weighted graph. Taking an undirected weighted graph, nodes correspond to the graph's nodes, with paths corresponding to the - graph's edges, and a path's distance is the edge's weight. Typically, the graph is complete where each pair of nodes - is connected by an edge. If no connection exists between two nodes, one can add an arbitrarily long edge to complete - the graph without affecting the optimal tour." + graph's edges, and a path's distance is the edge's weight." (source: https://github.com/aws/amazon-braket-examples/tree/main/examples) """ @@ -66,7 +64,7 @@ def get_solution_quality_unit(self) -> str: """ Returns the unit of measurement for the solution quality. - :return: The unit of measurement for the solution quality. + :return: Unit of measurement for the solution quality """ return "Tour cost" @@ -74,8 +72,9 @@ def get_default_submodule(self, option: str) -> Core: """ Returns the default submodule based on the given option. - :param option: The chosen submodule option. - :return: The corresponding submodule instance. + :param option: The chosen submodule option + :return: The corresponding submodule instance + :raises NotImplemented: If the provided option is not implemented """ if option == "Ising": from modules.applications.optimization.TSP.mappings.ISING import Ising # pylint: disable=C0415 @@ -214,7 +213,7 @@ def process_solution(self, solution: dict) -> tuple[list, float]: if val and (node not in route): route[timestep] = node - # check whether every timestep has only 1 node flagged + # Check whether every timestep has only 1 node flagged for i in nodes: relevant_nodes = [] relevant_timesteps = [] @@ -227,12 +226,12 @@ def process_solution(self, solution: dict) -> tuple[list, float]: # timestep or nodes have more than 1 or 0 flags return None, end_time_measurement(start_time) - # check validity of solution + # Check validity of solution if sum(value == 1 for value in solution.values()) > len(route): logging.warning("Result is longer than route! This might be problematic!") return None, end_time_measurement(start_time) - # run heuristic replacing None values + # Run heuristic replacing None values if None in route: # get not assigned nodes nodes_unassigned = [node for node in list(nodes) if node not in route] @@ -242,13 +241,13 @@ def process_solution(self, solution: dict) -> tuple[list, float]: route[idx] = nodes_unassigned[0] nodes_unassigned.remove(route[idx]) - # cycle solution to start at provided start location + # Cycle solution to start at provided start location if start is not None and route[0] != start: - # rotate to put the start in front + # Rotate to put the start in front idx = route.index(start) route = route[idx:] + route[:idx] - # print route + # Log route parsed_route = ' ->\n'.join([f' Node {visit}' for visit in route]) logging.info(f"Route found:\n{parsed_route}") @@ -275,7 +274,7 @@ def validate(self, solution: list) -> tuple[bool, float]: def evaluate(self, solution: list) -> tuple[float, float]: """ - Find distance for given route e.g. [0, 4, 3, 1, 2] and original data. + Find distance for given route and original data. :param solution: List containing the nodes of the solution :return: Tour cost and the time it took to calculate it diff --git a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py index 9049dae8..96214f2c 100644 --- a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py @@ -19,6 +19,7 @@ # Source http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/tsp/ filename = "dsj1000.tsp" + def main(): """ Load a TSP problem, remove unnecessary edges, and save the reference graph. @@ -42,5 +43,6 @@ def main(): print("Saved graph as reference_graph.gpickle") + if __name__ == '__main__': main() diff --git a/src/modules/applications/optimization/TSP/mappings/ISING.py b/src/modules/applications/optimization/TSP/mappings/ISING.py index 29760960..131ea279 100644 --- a/src/modules/applications/optimization/TSP/mappings/ISING.py +++ b/src/modules/applications/optimization/TSP/mappings/ISING.py @@ -167,13 +167,7 @@ def _create_pyqubo_model(cost_matrix: list) -> any: @staticmethod def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: """ - Converts dictionary index (e.g. 'c[0][2]') in PyQubo to matrix index. - - (e.g. 2 - {('c[0][2]', 'c[2][1]'): 0.06161479507592913, - ('c[0][0]', 'c[0][1]'): 20.0, - ('c[1][0]', 'c[2][1]'): 0.720033199087941, - ... } + Converts dictionary index in PyQubo to matrix index. :param ising_index_string: Index string from PyQubo :param number_nodes: Number of nodes in the graph @@ -228,7 +222,6 @@ def _map_pyqubo(self, graph: nx.Graph, config: Config) -> tuple[dict, float]: def _map_ocean(self, graph: nx.Graph, config: Config) -> tuple[dict, float]: """ Use D-Wave/Ocean TSP QUBO/Ising model. - https://docs.ocean.dwavesys.com/en/stable/docs_dnx/reference/algorithms/generated/dwave_networkx.algorithms.tsp.traveling_salesperson_qubo.html#dwave_networkx.algorithms.tsp.traveling_salesperson_qubo :param graph: Networkx graph :param config: Config with the parameters specified in Config class @@ -363,6 +356,7 @@ def get_default_submodule(self, option: str) -> Core: :param option: Submodule option :return: Corresponding submodule + :raises NotImplemented: If the provided option is not implemented """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/TSP/mappings/QUBO.py b/src/modules/applications/optimization/TSP/mappings/QUBO.py index 8215ba94..ca22fce1 100644 --- a/src/modules/applications/optimization/TSP/mappings/QUBO.py +++ b/src/modules/applications/optimization/TSP/mappings/QUBO.py @@ -57,7 +57,7 @@ def get_parameter_options(self) -> dict: "lagrange_factor": { "values": [0.75, 1.0, 1.25], "description": "By which factor would you like to multiply your " - "lagrange?", + "Lagrange?", "custom_input": True, "postproc": float } @@ -66,7 +66,7 @@ def get_parameter_options(self) -> dict: return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?", + "description": "By which factor would you like to multiply your Lagrange?", "custom_input": True, "allow_ranges": True, "postproc": float # Since we allow custom input here we need to parse it to float (input is str) @@ -122,6 +122,7 @@ def get_default_submodule(self, option: str) -> Core: :param option: Submodule option :return: Corresponding submodule + :raises NotImplemented: If the provided option is not implemented """ if option == "Annealer": diff --git a/src/modules/circuits/Circuit.py b/src/modules/circuits/Circuit.py index d815318f..8e5b339f 100644 --- a/src/modules/circuits/Circuit.py +++ b/src/modules/circuits/Circuit.py @@ -26,7 +26,7 @@ def __init__(self, name: str): """ Constructor method. - :param name : The name of the circuit architecture + :param name: The name of the circuit architecture """ super().__init__() self.architecture_name = name @@ -68,7 +68,7 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f :param input_data: Collected information of the benchmarking process :param config: Config specifying the number of qubits of the circuit :param kwargs: Optional keyword arguments - :return: Same dictionary like input_data with architecture_name + :return: Same dictionary like input_data with architecture_name and execution time """ start = start_time_measurement() input_data["architecture_name"] = self.architecture_name diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/circuits/CircuitCardinality.py index 37a78de8..f2cbe18c 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/circuits/CircuitCardinality.py @@ -62,9 +62,15 @@ def get_parameter_options(self) -> dict: }, } - def get_default_submodule( - self, option: str - ) ->Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule(self, option: str) \ + -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/circuits/CircuitCopula.py index efdbe2ea..0683f394 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/circuits/CircuitCopula.py @@ -26,7 +26,7 @@ class CircuitCopula(Circuit): """ This class generates a library-agnostic gate sequence, i.e. a list containing information - about the gates and the wires they act on. The marginal ditribtions generated by the copula + about the gates and the wires they act on. The marginal distributions generated by the copula are uniformaly distributed. """ @@ -73,14 +73,13 @@ def get_parameter_options(self) -> dict: }, } - def get_default_submodule( - self, option: str - ) -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule(self, option: str) \ + -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: """ Returns the default submodule based on the given option. :param option: The submodule option to select - :return: Instance of the selected submodule. + :return: Instance of the selected submodule :raises NotImplemented: If the provided option is not implemented """ if option == "LibraryQiskit": diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/circuits/CircuitStandard.py index 876f39a4..0c7afd8d 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/circuits/CircuitStandard.py @@ -71,14 +71,14 @@ def get_parameter_options(self) -> dict: } } - def get_default_submodule( - self, option: str - ) -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule(self, option: str) \ + -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: """ Returns the default submodule based on the given option. :param option: The submodule option to select :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented """ if option == "LibraryQiskit": return LibraryQiskit() diff --git a/src/modules/devices/HelperClass.py b/src/modules/devices/HelperClass.py index 7cd536f0..ae4cf744 100644 --- a/src/modules/devices/HelperClass.py +++ b/src/modules/devices/HelperClass.py @@ -18,7 +18,7 @@ class HelperClass(Device): """ - Some Solvers like Pennylane, only needs strings for setting up the device and not a standalone class. + Some solvers like Pennylane, only needs strings for setting up the device and not a standalone class. TODO: Maybe refactor this once we think of a better structure for this """ @@ -41,11 +41,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/Local.py b/src/modules/devices/Local.py index 1f4ccd4a..ab2101ce 100644 --- a/src/modules/devices/Local.py +++ b/src/modules/devices/Local.py @@ -18,8 +18,7 @@ class Local(Device): """ - Some Solvers (often classical) also can run on a normal local environment without any specific device or - setting needed. + Some solvers (often classical) run on a local environment without any specific device or setting needed. """ def __init__(self): @@ -38,11 +37,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/SimulatedAnnealingSampler.py b/src/modules/devices/SimulatedAnnealingSampler.py index 95091eb4..79e929a3 100644 --- a/src/modules/devices/SimulatedAnnealingSampler.py +++ b/src/modules/devices/SimulatedAnnealingSampler.py @@ -48,11 +48,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index 84803295..66084fdd 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -113,7 +113,7 @@ def _set_profile() -> str: logging.info(f"No AWS_PROFILE specified, using default profile: {profile_name}") return profile_name - def _initialize_aws_session(self, profile_name: str, region: str, my_config: Config): + def _initialize_aws_session(self, profile_name: str, region: str, my_config: Config) -> None: """ Initializes the AWS session for interacting with Amazon Braket. diff --git a/src/modules/devices/braket/Ionq.py b/src/modules/devices/braket/Ionq.py index cab9f6a6..374f64d4 100644 --- a/src/modules/devices/braket/Ionq.py +++ b/src/modules/devices/braket/Ionq.py @@ -27,6 +27,9 @@ class Ionq(Braket): def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-east-1::device/qpu/ionq/Harmony'): """ Constructor method for initializing IonQ device on Amazon Braket. + + :param device_name: Name of the device + :param arn: Amazon resource name for the IonQ device """ super().__init__(region="us-east-1", device_name=device_name, arn=arn) self.submodule_options = [] @@ -47,11 +50,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/LocalSimulator.py b/src/modules/devices/braket/LocalSimulator.py index 741d6d5a..12fe5a15 100644 --- a/src/modules/devices/braket/LocalSimulator.py +++ b/src/modules/devices/braket/LocalSimulator.py @@ -41,11 +41,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/OQC.py b/src/modules/devices/braket/OQC.py index 1aeb3879..59e475e2 100644 --- a/src/modules/devices/braket/OQC.py +++ b/src/modules/devices/braket/OQC.py @@ -27,6 +27,9 @@ class OQC(Braket): def __init__(self, device_name: str, arn: str = 'arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy'): """ Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the OQC device """ super().__init__(region="eu-west-2", device_name=device_name, arn=arn) self.submodule_options = [] @@ -47,11 +50,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/Rigetti.py b/src/modules/devices/braket/Rigetti.py index f1276a30..5566ea6f 100644 --- a/src/modules/devices/braket/Rigetti.py +++ b/src/modules/devices/braket/Rigetti.py @@ -27,6 +27,9 @@ class Rigetti(Braket): def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3'): """ Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the Rigetti device """ super().__init__(region="us-west-1", device_name=device_name, arn=arn) self.submodule_options = [] @@ -47,11 +50,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/SV1.py b/src/modules/devices/braket/SV1.py index ff6b0e60..ee54b16c 100644 --- a/src/modules/devices/braket/SV1.py +++ b/src/modules/devices/braket/SV1.py @@ -27,6 +27,9 @@ class SV1(Braket): def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum-simulator/amazon/sv1'): """ Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the SV1 simulator """ super().__init__(device_name=device_name, arn=arn) self.submodule_options = [] @@ -41,18 +44,17 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum def get_parameter_options(self) -> dict: """ - Returns empty dicionary as this solver has no configurable settings. + Returns empty dictionary as this solver has no configurable settings. :return: Empty dict """ - return { -} + return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/TN1.py b/src/modules/devices/braket/TN1.py index 2877d5ef..c346b4d8 100644 --- a/src/modules/devices/braket/TN1.py +++ b/src/modules/devices/braket/TN1.py @@ -29,7 +29,7 @@ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum Constructor method. :param device_name: Name of the device - :param arn: Amazon Resource Name for the TN1 simulator. + :param arn: Amazon resource name for the TN1 simulator """ super().__init__(device_name=device_name, arn=arn) self.submodule_options = [] @@ -50,11 +50,11 @@ def get_parameter_options(self) -> dict: """ return {} - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/pulser/MockNeutralAtomDevice.py b/src/modules/devices/pulser/MockNeutralAtomDevice.py index b89cae0b..0397a796 100644 --- a/src/modules/devices/pulser/MockNeutralAtomDevice.py +++ b/src/modules/devices/pulser/MockNeutralAtomDevice.py @@ -81,11 +81,11 @@ def get_backend_config(self) -> pulser.backend.config.EmulatorConfig: emulator_config = pulser.backend.config.EmulatorConfig(noise_model=noise_model) return emulator_config - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: """ Raises ValueError as this module has no submodules. :param option: Option name - :raises ValueError: If called, since this module has no submodules. + :raises ValueError: If called, since this module has no submodules """ raise ValueError("This module has no submodules.") diff --git a/src/modules/solvers/Annealer.py b/src/modules/solvers/Annealer.py index 6ab5a5e4..ac08e540 100644 --- a/src/modules/solvers/Annealer.py +++ b/src/modules/solvers/Annealer.py @@ -78,18 +78,19 @@ class Config(TypedDict): """ number_of_reads: int - def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ - Annealing Solver. + Run the annealing solver. - :param mapped_problem: Dictionary with the key 'Q' where its value should be the QUBO + :param mapped_problem: Dict with the key 'Q' where its value should be the QUBO :param device_wrapper: Annealing device :param config: Annealing settings :param kwargs: Additional keyword arguments :return: Solution, the time it took to compute it and optional additional information """ - Q = mapped_problem['Q'] + q = mapped_problem['Q'] additional_solver_information = {} device = device_wrapper.get_device() start = start_time_measurement() @@ -100,7 +101,7 @@ def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwarg logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") - response = device.sample_qubo(Q, num_reads=config['number_of_reads']) + response = device.sample_qubo(q, num_reads=config['number_of_reads']) time_to_solve = end_time_measurement(start) # Take the result with the lowest energy: diff --git a/src/modules/solvers/ClassicalSAT.py b/src/modules/solvers/ClassicalSAT.py index 91ffa7b2..a0308b13 100644 --- a/src/modules/solvers/ClassicalSAT.py +++ b/src/modules/solvers/ClassicalSAT.py @@ -71,7 +71,7 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: dict) -> tuple[list, float]: + def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: dict) -> tuple[list, float, dict]: """ The given application is a problem instance from the pysat library. This uses the rc2 maxsat solver given in that library to return a solution. @@ -89,7 +89,7 @@ def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: ) start = start_time_measurement() - # we use rc2 solver to compute the optimal solution + # We use rc2 solver to compute the optimal solution with RC2(mapped_problem) as rc2: sol = rc2.compute() diff --git a/src/modules/solvers/GreedyClassicalPVC.py b/src/modules/solvers/GreedyClassicalPVC.py index 7446babc..2520f1ab 100644 --- a/src/modules/solvers/GreedyClassicalPVC.py +++ b/src/modules/solvers/GreedyClassicalPVC.py @@ -69,7 +69,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) -> tuple[dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the PVC graph in a greedy fashion. @@ -82,7 +83,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwar # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() - #Start at the base node + # Start at the base node current_node = ((0, 0), 1, 1) idx = 1 diff --git a/src/modules/solvers/GreedyClassicalTSP.py b/src/modules/solvers/GreedyClassicalTSP.py index 0e64d47e..42ffd0e8 100644 --- a/src/modules/solvers/GreedyClassicalTSP.py +++ b/src/modules/solvers/GreedyClassicalTSP.py @@ -37,7 +37,7 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module. + Returns requirements of this module. :return: List of dict with requirements of this module """ @@ -70,7 +70,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) ->tuple[dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the TSP graph in a greedy fashion. @@ -84,11 +85,10 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwar mapped_problem = mapped_problem.copy() start = start_time_measurement() - #Use NetworkX approximation for a greedy TSP solution + # Use NetworkX approximation for a greedy TSP solution tour = approx.greedy_tsp(mapped_problem) # Remove the duplicate node as we don't want a cycle - # Reference: https://stackoverflow.com/a/7961390/10456906 tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later diff --git a/src/modules/solvers/MIPsolverACL.py b/src/modules/solvers/MIPsolverACL.py index 54dccf9f..b6b93493 100644 --- a/src/modules/solvers/MIPsolverACL.py +++ b/src/modules/solvers/MIPsolverACL.py @@ -83,7 +83,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the ACL problem as a mixed integer problem (MIP). diff --git a/src/modules/solvers/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py index eb88f639..9fe81942 100644 --- a/src/modules/solvers/NeutralAtomMIS.py +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -121,8 +121,8 @@ def run(self, mapped_problem: dict, device_wrapper: any, config: any, **kwargs: return state_nodes, end_time_measurement(start), {} - def _create_sequence(self, register:pulser.Register, device:pulser.devices._device_datacls.Device) -> ( - pulser.Sequence): + def _create_sequence(self, register: pulser.Register, device: pulser.devices._device_datacls.Device) \ + -> pulser.Sequence: """ Creates a pulser sequence from a register and a device. @@ -137,7 +137,7 @@ def _create_sequence(self, register:pulser.Register, device:pulser.devices._devi sequence.add(pulse, "Rydberg global") return sequence - def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: + def _create_pulses(self, device: pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: """ Creates pulses tuned to MIS problem. @@ -150,13 +150,13 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p :param device: The device being used :return: List of pulses """ - Omega_max = 2.3 * 2 * np.pi + omega_max = 2.3 * 2 * np.pi delta_factor = 2 * np.pi channel = device.channels['rydberg_global'] max_amp = channel.max_amp - if max_amp is not None and max_amp < Omega_max: - Omega_max = max_amp + if max_amp is not None and max_amp < omega_max: + omega_max = max_amp delta_0 = -3 * delta_factor delta_f = 1 * delta_factor @@ -166,13 +166,13 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p t_sweep = (delta_f - delta_0) / (2 * np.pi * 10) * 5000 rise = pulser.Pulse.ConstantDetuning( - pulser.waveforms.RampWaveform(t_rise, 0.0, Omega_max), delta_0, 0.0 + pulser.waveforms.RampWaveform(t_rise, 0.0, omega_max), delta_0, 0.0 ) sweep = pulser.Pulse.ConstantAmplitude( - Omega_max, pulser.waveforms.RampWaveform(t_sweep, delta_0, delta_f), 0.0 + omega_max, pulser.waveforms.RampWaveform(t_sweep, delta_0, delta_f), 0.0 ) fall = pulser.Pulse.ConstantDetuning( - pulser.waveforms.RampWaveform(t_fall, Omega_max, 0.0), delta_f, 0.0 + pulser.waveforms.RampWaveform(t_fall, omega_max, 0.0), delta_f, 0.0 ) pulses = [rise, sweep, fall] @@ -204,7 +204,7 @@ def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> d return valid_state_counts - def _translate_state_to_nodes(self, state:str, nodes:list) -> list: + def _translate_state_to_nodes(self, state: str, nodes: list) -> list: """ Translates a state string into the corresponding list of nodes. @@ -214,7 +214,7 @@ def _translate_state_to_nodes(self, state:str, nodes:list) -> list: """ return [key for index, key in enumerate(nodes) if state[index] == '1'] - def _select_best_state(self, states:dict, nodes:list) -> str: + def _select_best_state(self, states: dict, nodes: list) -> str: """ Selects the best state from the available valid states. @@ -225,9 +225,9 @@ def _select_best_state(self, states:dict, nodes:list) -> str: # TODO: Implement the samplers try: best_state = max(states, key=lambda k: states[k]) - except: # pylint: disable=W0702 + except Exception: # pylint: disable=W0702 # TODO: Specify error - # TODO: Clean up this monstrocity + # TODO: Clean this up n_nodes = len(nodes) best_state = "0" * n_nodes diff --git a/src/modules/solvers/PennylaneQAOA.py b/src/modules/solvers/PennylaneQAOA.py index d7820061..27f5d311 100644 --- a/src/modules/solvers/PennylaneQAOA.py +++ b/src/modules/solvers/PennylaneQAOA.py @@ -201,51 +201,51 @@ def normalize_data(data: any, scale: float = 1.0) -> any: return scale * data / np.max(np.abs(data)) @staticmethod - def qaoa_operators_from_ising(J: any, t: any, scale: float = 1.0) -> tuple[any, any]: + def qaoa_operators_from_ising(j: any, t: any, scale: float = 1.0) -> tuple[any, any]: """ - Generates pennylane cost and mixer hamiltonians from the Ising matrix J and vector t. + Generates pennylane cost and mixer Hamiltonians from the Ising matrix J and vector t. - :param J: J matrix + :param j: J matrix :param t: t vector :param scale: Scaling factor :return: Cost Hamiltonian and mixer Hamiltonian """ # Define the scaling factor - scaling_factor = scale * max(np.max(np.abs(J.flatten())), np.max(np.abs(t))) + scaling_factor = scale * max(np.max(np.abs(j.flatten())), np.max(np.abs(t))) # Scale the coefficients - J /= scaling_factor + j /= scaling_factor t /= scaling_factor sigzsigz_arr = [ - qml.PauliZ(i) @ qml.PauliZ(j) for i in range(len(J)) for j in range(len(J)) + qml.PauliZ(i) @ qml.PauliZ(k) for i in range(len(j)) for k in range(len(j)) ] sigz_arr = [qml.PauliZ(i) for i in range(len(t))] - J_real = np.real(J.flatten()) + j_real = np.real(j.flatten()) t_real = np.real(t) - h_cost = qml.simplify(qml.Hamiltonian([*t_real, *J_real.flatten()], [*sigz_arr, *sigzsigz_arr])) + h_cost = qml.simplify(qml.Hamiltonian([*t_real, *j_real.flatten()], [*sigz_arr, *sigzsigz_arr])) # Definition of the mixer hamiltonian - h_mixer = -1 * qml.qaoa.mixers.x_mixer(range(len(J))) + h_mixer = -1 * qml.qaoa.mixers.x_mixer(range(len(j))) return h_cost, h_mixer # pylint: disable=R0915 - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, any, float]: + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float, dict]: """ Runs Pennylane QAOA on the Ising problem. - :param mapped_problem: Ising + :param mapped_problem: Dict containing problem parameters mapped to the Ising model :param device_wrapper: Device to run the problem on :param config: QAOA solver settings :param kwargs: Contains store_dir for the plot of the optimization :return: Solution, the time it took to compute it and optional additional information """ - J = mapped_problem['J'] + j = mapped_problem['J'] t = mapped_problem['t'] - wires = J.shape[0] - cost_h, mixer_h = self.qaoa_operators_from_ising(J, t, scale=config['coeff_scale']) + wires = j.shape[0] + cost_h, mixer_h = self.qaoa_operators_from_ising(j, t, scale=config['coeff_scale']) # set up the problem try: @@ -275,13 +275,13 @@ def circuit(params, **kwargs): if device_wrapper.device == 'qulacs.simulator': dev = qml.device(device_wrapper.device, wires=wires, shots=config['shots'], gpu=True) elif device_wrapper.device in ['lightning.qubit', 'lightning.gpu']: - # no number shots as diff method will be adjoint backprop for these devices + # No number shots as diff method will be adjoint backprop for these devices if diff_method in ["adjoint", "backprop"]: dev = qml.device(device_wrapper.device, wires=wires, shots=None, batch_obs=True) else: dev = qml.device(device_wrapper.device, wires=wires, batch_obs=True, shots=config['shots']) elif device_wrapper.device == 'default.qubit': - # no number shots as diff method will be adjoint backprop for these devices + # No number shots as diff method will be adjoint backprop for these devices if diff_method in ["adjoint", "backprop"]: dev = qml.device(device_wrapper.device, shots=None, wires=wires) else: @@ -339,7 +339,7 @@ def cost_function(params): f"Optimization start...") additional_solver_information = {} - min_param, min_cost, cost_pt, params_list, x= None, None, [], [], [] + min_param, min_cost, cost_pt, params_list, x = None, None, [], [], [] run_id = round(time()) start = start_time_measurement() @@ -415,7 +415,6 @@ def evaluate_params_probs(params): best_bitstring = max(probs, key=probs.get) return best_bitstring, probs - best_bitstring, probs = None, None if config['shots'] is None or diff_method in ["backprop", "adjoint"]: best_bitstring, probs = evaluate_params_probs(params) else: @@ -438,7 +437,7 @@ def evaluate_params_probs(params): bitstring_list.append(bitstring) # Save the cost, best bitstring, variational parameters per iteration as well as the final prob. distribution - # TODO: Maybe this can be done more efficient, e.g. only saving the circuit and its weights? + # TODO: Maybe this can be done more efficient, e.g., only saving the circuit and its weights? json_data = { 'cost': cost_pt, 'bitstrings': bitstring_list, @@ -461,18 +460,17 @@ def monkey_init_array(self): def _pseudo_decor(fun, device): """ - Massive shoutout to this guy: https://stackoverflow.com/a/25827070/10456906 We use this decorator for measuring execute and batch_execute. """ - # Magic sauce to lift the name and doc of the function + # Lift the name and doc of the function @wraps(fun) def ret_fun(*args, **kwargs): - # Pre function execution stuff here + # Pre function execution here from time import time # pylint: disable=W0621 disable=C0415 disable=W0404 start_timing = time() * 1000 returned_value = fun(*args, **kwargs) - # Post execution stuff here + # Post execution here device.timings.append(round(time() * 1000 - start_timing, 3)) return returned_value diff --git a/src/modules/solvers/QAOA.py b/src/modules/solvers/QAOA.py index c8a343c4..3016b6ae 100644 --- a/src/modules/solvers/QAOA.py +++ b/src/modules/solvers/QAOA.py @@ -18,6 +18,7 @@ import numpy as np from braket.circuits import Circuit +from braket.aws import AwsDevice from scipy.optimize import minimize from modules.solvers.Solver import Solver @@ -105,7 +106,7 @@ def get_parameter_options(self) -> dict: """ return { - "shots": { # number measurements to make on circuit + "shots": { # number of measurements to make on circuit "values": list(range(10, 500, 30)), "description": "How many shots do you need?" }, @@ -134,11 +135,11 @@ class Config(TypedDict): opt_method: str depth: int - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float]: + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float, dict]: """ Run QAOA algorithm on Ising. - :param mapped_problem: dictionary with the keys 'J' and 't' + :param mapped_problem: Dict containing problem parameters mapped to the Ising model :param device_wrapper: Instance of device :param config: Solver configuration settings :param kwargs: No additionally settings needed @@ -218,9 +219,14 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs # https://github.com/aws/amazon-braket-examples/blob/main/examples/hybrid_quantum_algorithms/QAOA/utils_qaoa.py) # Function to implement ZZ gate using CNOT gates -def ZZgate(q1, q2, gamma): +def zz_gate(q1: any, q2: any, gamma: float) -> Circuit: """ - function that returns a circuit implementing exp(-i \\gamma Z_i Z_j) using CNOT gates if ZZ not supported. + Function that returns a circuit implementing exp(-i \\gamma Z_i Z_j) using CNOT gates if ZZ not supported. + + :param q1: Qubit 1 (control) + :param q2: Qubit 2 (target) + :param gamma: Gamma parameter (angle) + :return: ZZ gate """ # Get a circuit circ_zz = Circuit() @@ -232,9 +238,13 @@ def ZZgate(q1, q2, gamma): # Function to implement evolution with driver Hamiltonian -def driver(beta, n_qubits): +def driver(beta: float, n_qubits: int) -> Circuit: """ Returns circuit for driver Hamiltonian U(Hb, beta). + + :param beta: Beta parameter (angle) + :param n_qubits: Number of qubits + :return: Circuit with rotated qubits """ # Instantiate circuit object circ = Circuit() @@ -248,9 +258,14 @@ def driver(beta, n_qubits): # Helper function for evolution with cost Hamiltonian -def cost_circuit(gamma, n_qubits, ising, device): +def cost_circuit(gamma: float, ising: np.ndarray, device: AwsDevice) -> Circuit: """ - returns circuit for evolution with cost Hamiltonian. + Returns circuit for evolution with cost Hamiltonian. + + :param gamma: Gamma parameter (angle) + :param ising: Ising matrix + :param device: Device to run the circuit on + :return: Circuit representing the cost Hamiltonian """ # Instantiate circuit object circ = Circuit() @@ -265,7 +280,7 @@ def cost_circuit(gamma, n_qubits, ising, device): int_strength = ising[qubit_pair[0], qubit_pair[1]] # For Rigetti we decompose ZZ using CNOT gates if device.name in ["Rigetti", "Aspen-9"]: # TODO make this more flexible - gate = ZZgate(qubit_pair[0], qubit_pair[1], gamma * int_strength) + gate = zz_gate(qubit_pair[0], qubit_pair[1], gamma * int_strength) # Classical simulators and IonQ support ZZ gate else: gate = Circuit().zz(qubit_pair[0], qubit_pair[1], angle=2 * gamma * int_strength) @@ -275,17 +290,23 @@ def cost_circuit(gamma, n_qubits, ising, device): # Function to build the QAOA circuit with depth p -def circuit(params, device, n_qubits, ising): +def circuit(params: np.array, device: AwsDevice, n_qubits: int, ising: np.ndarray) -> Circuit: """ - function to return full QAOA circuit; depends on device as ZZ implementation depends on gate set of backend. + Function to return the full QAOA circuit; depends on device as ZZ implementation depends on gate set of backend. + + :param params: Array containing the beta and gamma parameters + :param device: Device to run the circuit on + :param n_qubits: Number of qubits + :param ising: Ising matrix + :return: QAOA Circuit """ - # Initialize qaoa circuit with first Hadamard layer: for minimization start in |-> + # Initialize QAOA circuit with first Hadamard layer circ = Circuit() - X_on_all = Circuit().x(range(0, n_qubits)) - circ.add(X_on_all) - H_on_all = Circuit().h(range(0, n_qubits)) - circ.add(H_on_all) + x_on_all = Circuit().x(range(0, n_qubits)) + circ.add(x_on_all) + h_on_all = Circuit().h(range(0, n_qubits)) + circ.add(h_on_all) # Setup two parameter families circuit_length = int(len(params) / 2) @@ -294,7 +315,7 @@ def circuit(params, device, n_qubits, ising): # Add QAOA circuit layer blocks for mm in range(circuit_length): - circ.add(cost_circuit(gammas[mm], n_qubits, ising, device)) + circ.add(cost_circuit(gammas[mm], ising, device)) circ.add(driver(betas[mm], n_qubits)) return circ @@ -303,10 +324,21 @@ def circuit(params, device, n_qubits, ising): # Function that computes cost function for given params # pylint: disable=R0917 # pylint: disable=R0913 -def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_folder, verbose): +def objective_function(params: np.array, device: AwsDevice, ising: np.ndarray, n_qubits: int, n_shots: int, + tracker: dict, s3_folder: tuple[str,str], verbose: bool) -> float: """ - objective function takes a list of variational parameters as input, + Objective function takes a list of variational parameters as input, and returns the cost associated with those parameters. + + :param params: Array containing beta and gamma parameters + :param device: Device to run the circuit on + :param ising: Ising matrix + :param n_qubits: Number of qubits + :param n_shots: Number of measurements to make on the circuit + :param tracker: Keeps track of the runs on the circuit + :param s3_folder: AWS S3 bucket + :param verbose: Controls degree of detail in logs + :return: Energy expectation value """ if verbose: @@ -378,9 +410,22 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol # The function to execute the training: run classical minimization. # pylint: disable=R0917 -def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_folder, verbose=True): +def train(device: AwsDevice, options: dict, p: int, ising: np.ndarray, n_qubits: int, n_shots: int, opt_method: str, + tracker: dict, s3_folder: tuple[str,str], verbose: bool = True) -> tuple[float, np.ndarray, dict]: """ - function to run QAOA algorithm for given, fixed circuit depth p. + Function to run QAOA algorithm for given, fixed circuit depth p. + + :param device: Device to run the circuit on + :param options: Dict containing parameters of classical part of the QAOA + :param p: Circuit depth + :param ising: Ising matrix + :param n_qubits: Number of qubits + :param n_shots: Number of measurements to make on the circuit + :param opt_method: Controls degree of detail in logs + :param tracker: Keeps track of the runs on the circuit + :param s3_folder: AWS S3 bucket + :param verbose: Controls degree of detail in logs + :return: Results of the training as a tuple of the energy, the angle and the tracker """ logging.info("Starting the training.") logging.info("==================================" * 2) diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index 5db02676..1d430cb0 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.py @@ -86,7 +86,7 @@ def get_parameter_options(self) -> dict: "description": "How many iterations do you need? Warning: When using\ the IBM Eagle Device you should only choose a lower number of\ iterations, since a high number would lead to a waiting time that\ - could take up to mulitple days!" + could take up to multiple days!" }, "depth": { "values": [2, 3, 4, 5, 10, 20], @@ -112,13 +112,13 @@ def get_parameter_options(self) -> dict: }, "iterations": { # number measurements to make on circuit "values": [1, 5, 10, 20, 50, 75], - "description": "How many iterations do you need? Warning: When using the IBM Eagle Device you\ - should only choose a lower number of iterations, since a high number would lead to a waiting \ - ime that could take up to mulitple days!" + "description": "How many iterations do you need? Warning: When using the IBM Eagle device you\ + should only choose a low number of iterations, since a high number would lead to a waiting \ + time that could take up to multiple days!" }, "depth": { "values": [2, 3, 4, 5, 10, 20], - "description": "How many layers for QAOA (Parameter: p) do you want?" + "description": "How many layers for QAOA (parameter: p) do you want?" }, "method": { "values": ["classic", "vqe", "qaoa"], @@ -126,7 +126,7 @@ def get_parameter_options(self) -> dict: }, "optimizer": { "values": ["POWELL", "SPSA", "COBYLA"], - "description": "Which Qiskit solver should be used? Warning: When using the IBM Eagle Device\ + "description": "Which Qiskit solver should be used? Warning: When using the IBM Eagle device\ you should not use the SPSA optimizer for a low number of iterations!" } } @@ -142,6 +142,7 @@ class Config(TypedDict): iterations: int layers: int method: str + optimizer: str """ shots: int @@ -149,6 +150,7 @@ class Config(TypedDict): iterations: int layers: int method: str + optimizer: str @staticmethod def normalize_data(data: any, scale: float = 1.0) -> any: @@ -161,7 +163,7 @@ def normalize_data(data: any, scale: float = 1.0) -> any: """ return scale * data / np.max(np.abs(data)) - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float]: + def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float, dict]: """ Run Qiskit QAOA algorithm on Ising. @@ -184,7 +186,7 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs optimizer = COBYLA(maxiter=config["iterations"]) elif config["optimizer"] == "POWELL": optimizer = POWELL(maxiter=config["iterations"], maxfev=config["iterations"] if - device_wrapper.device == 'ibm_eagle' else None) + device_wrapper.device == 'ibm_eagle' else None) elif config["optimizer"] == "SPSA": optimizer = SPSA(maxiter=config["iterations"]) if config["method"] == "vqe": @@ -218,7 +220,7 @@ def _get_best_solution(self, result) -> any: """ if self.ry is not None: if hasattr(result, "optimal_point"): - para_dict = dict(zip(self.ry.parameters, result.optimal_point)) + para_dict = dict(zip(self.ry.parameters, result.optimal_point)) unbound_para = set(self.ry.parameters) - set(para_dict.keys()) for param in unbound_para: para_dict[param] = 0.0 @@ -271,5 +273,5 @@ def _get_pauli_op(ising: tuple[np.ndarray, np.ndarray]) -> SparsePauliOp: pauli_str = "".join(pauli_str_list) pauli_list.append((pauli_str, complex(x))) - isingOp =SparsePauliOp.from_list(pauli_list) - return isingOp + ising_op = SparsePauliOp.from_list(pauli_list) + return ising_op diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index 1c1c232f..f388675c 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -69,7 +69,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the PVC graph in a greedy fashion. @@ -79,7 +80,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **k :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ - # Deep copy since we are modifying the graph. This ensures that the original graph remains unchanges + # Deep copy since we are modifying the graph. This ensures that the original graph remains unchanged # with a different graph mapped_problem = mapped_problem.copy() start = start_time_measurement() diff --git a/src/modules/solvers/RandomClassicalSAT.py b/src/modules/solvers/RandomClassicalSAT.py index aa1bcd3e..8f942b92 100644 --- a/src/modules/solvers/RandomClassicalSAT.py +++ b/src/modules/solvers/RandomClassicalSAT.py @@ -69,7 +69,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[list, float]: + def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[list, float, dict]: """ The given application is a problem instance from the pysat library. This generates a random solution to the problem. @@ -81,7 +82,7 @@ def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwarg :return: Solution, the time it took to compute it and optional additional information """ logging.info( - f"Got problem with {mapped_problem.nv} variables, {len(mapped_problem.hard)} constraints and" + f"Got SAT problem with {mapped_problem.nv} variables, {len(mapped_problem.hard)} constraints and" f" {len(mapped_problem.soft)} tests." ) diff --git a/src/modules/solvers/RandomClassicalTSP.py b/src/modules/solvers/RandomClassicalTSP.py index 655d401d..b5dcc7e7 100644 --- a/src/modules/solvers/RandomClassicalTSP.py +++ b/src/modules/solvers/RandomClassicalTSP.py @@ -63,7 +63,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the TSP graph in a greedy fashion. @@ -87,7 +88,6 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **k tour.append(tour[0]) # Remove the duplicate node as we don't want a cycle - # https://stackoverflow.com/a/7961390/10456906 tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later diff --git a/src/modules/solvers/ReverseGreedyClassicalPVC.py b/src/modules/solvers/ReverseGreedyClassicalPVC.py index ac183163..150d2d78 100644 --- a/src/modules/solvers/ReverseGreedyClassicalPVC.py +++ b/src/modules/solvers/ReverseGreedyClassicalPVC.py @@ -69,13 +69,14 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the PVC graph in a greedy fashion. We take the worst choice at each step. :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device - :param config: Empty dicT + :param config: Empty dict :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information """ diff --git a/src/modules/solvers/ReverseGreedyClassicalTSP.py b/src/modules/solvers/ReverseGreedyClassicalTSP.py index bd7fa5f3..d338fbd3 100644 --- a/src/modules/solvers/ReverseGreedyClassicalTSP.py +++ b/src/modules/solvers/ReverseGreedyClassicalTSP.py @@ -71,7 +71,8 @@ class Config(TypedDict): """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: any) -> tuple[dict, float]: + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: any) \ + -> tuple[dict, float, dict]: """ Solve the TSP graph in a greedy fashion. diff --git a/src/modules/training/Inference.py b/src/modules/training/Inference.py index 6016a15e..7d8b1d31 100644 --- a/src/modules/training/Inference.py +++ b/src/modules/training/Inference.py @@ -77,6 +77,12 @@ class Config(TypedDict): pretrained: str def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> dict: diff --git a/src/modules/training/QCBM.py b/src/modules/training/QCBM.py index 64b77631..92e602ce 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/training/QCBM.py @@ -149,6 +149,12 @@ class Config(TypedDict): loss: str def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") def setup_training(self, input_data: dict, config: Config) -> tuple[float, dict]: diff --git a/src/modules/training/QGAN.py b/src/modules/training/QGAN.py index 07449be7..f3016e33 100644 --- a/src/modules/training/QGAN.py +++ b/src/modules/training/QGAN.py @@ -183,6 +183,12 @@ class Config(TypedDict): loss: str def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") def setup_training(self, input_data: dict, config: dict) -> None: diff --git a/src/utils.py b/src/utils.py index 8f94398b..6a041be7 100644 --- a/src/utils.py +++ b/src/utils.py @@ -97,7 +97,7 @@ def _import_class(module_path: str, class_name: str, base_dir: str = None) -> an def checkbox(key: str, message: str, choices: list) -> dict: """ - Wrapper method to avoid empty responses in checkbox. + Wrapper method to avoid empty responses in checkboxes. :param key: Key for response dict :param message: Message for the user @@ -157,12 +157,12 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: :return: The adapted JSON """ assert type(j) in [dict, list], f"unexpected type:{type(j)}" - if type(j) == list: + if type(j) is list: for entry in j: _expand_paths(entry, base_dir) else: for attr in j: - if type(j[attr]) == "submodules": + if type(j[attr]) is "submodules": _expand_paths(j[attr], base_dir) elif attr == "dir": p = j[attr] From 81265cfafef1726474dcfc1ed8c6ae04513236dc Mon Sep 17 00:00:00 2001 From: "Marvin Erdmann (FG-231)" Date: Mon, 14 Oct 2024 16:30:33 +0200 Subject: [PATCH 22/40] Changed QML to qml and some minor adjustments --- docs/tutorial.rst | 4 +- src/Installer.py | 2 +- src/main.py | 2 +- src/modules/applications/{QML => qml}/QML.py | 2 +- .../generative_modeling/GenerativeModeling.py | 10 ++--- .../generative_modeling/__init__.py | 0 .../generative_modeling/data/MG_2D.npy | 0 .../generative_modeling/data/O_2D.npy | 0 .../generative_modeling/data/Stocks_2D.npy | 0 .../generative_modeling/data/X_2D.npy | 0 .../generative_modeling/data/__init__.py | 0 .../data/data_handler/ContinuousData.py | 10 ++--- .../data/data_handler/DataHandler.py | 0 .../data/data_handler/DiscreteData.py | 4 +- .../data_handler/MetricsGeneralization.py | 0 .../data/data_handler/__init__.py | 0 .../mappings/CustomQiskitNoisyBackend.py | 2 +- .../generative_modeling/mappings/Library.py | 0 .../mappings/LibraryPennylane.py | 2 +- .../mappings/LibraryQiskit.py | 2 +- .../mappings/PresetQiskitNoisyBackend.py | 38 ++++++++++++------- .../generative_modeling/mappings/__init__.py | 0 .../transformations/MinMax.py | 2 +- .../transformations/PIT.py | 2 +- .../transformations/Transformation.py | 0 .../transformations/__init__.py | 0 src/modules/circuits/CircuitCardinality.py | 8 ++-- src/modules/circuits/CircuitCopula.py | 8 ++-- src/modules/circuits/CircuitStandard.py | 8 ++-- src/modules/training/Training.py | 2 +- 30 files changed, 60 insertions(+), 48 deletions(-) rename src/modules/applications/{QML => qml}/QML.py (95%) rename src/modules/applications/{QML => qml}/generative_modeling/GenerativeModeling.py (93%) rename src/modules/applications/{QML => qml}/generative_modeling/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/MG_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/O_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/Stocks_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/X_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/ContinuousData.py (94%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/DataHandler.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/DiscreteData.py (98%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/MetricsGeneralization.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/CustomQiskitNoisyBackend.py (99%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/Library.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/LibraryPennylane.py (99%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/LibraryQiskit.py (99%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/PresetQiskitNoisyBackend.py (92%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/MinMax.py (99%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/PIT.py (99%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/Transformation.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/__init__.py (100%) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index a01220b4..f8a7420f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -67,7 +67,7 @@ In case you want to use custom modules files (for example to use external module You can find the documentation in the Dynamic Imports section. Git Large File Storage (LFS) -~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ QUARK stores data and config files using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. Installing Git LFS @@ -81,7 +81,7 @@ Install Git LFS by following the instructions on `Git LFS - On Windows. Download and install Git LFS from the `Official page `_ Running a Benchmark -~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~ .. code:: bash diff --git a/src/Installer.py b/src/Installer.py index 4c0c5c08..95fe0d4f 100644 --- a/src/Installer.py +++ b/src/Installer.py @@ -47,7 +47,7 @@ def __init__(self): {"name": "MIS", "class": "MIS", "module": "modules.applications.optimization.MIS.MIS"}, {"name": "SCP", "class": "SCP", "module": "modules.applications.optimization.SCP.SCP"}, {"name": "GenerativeModeling", "class": "GenerativeModeling", - "module": "modules.applications.QML.generative_modeling.GenerativeModeling"} + "module": "modules.applications.qml.generative_modeling.GenerativeModeling"} ] self.core_requirements = [ diff --git a/src/main.py b/src/main.py index 4194521a..d08b23cd 100644 --- a/src/main.py +++ b/src/main.py @@ -73,7 +73,7 @@ def setup_logging() -> None: logging.info(" ============================================================ ") logging.info(" A Framework for Quantum Computing Application Benchmarking ") logging.info(" ") - logging.info(" Licensed under the Apache License, Version 2.1 ") + logging.info(" Licensed under the Apache License, Version 2.0 ") logging.info(" ============================================================ ") diff --git a/src/modules/applications/QML/QML.py b/src/modules/applications/qml/QML.py similarity index 95% rename from src/modules/applications/QML/QML.py rename to src/modules/applications/qml/QML.py index ef7f6f86..91f34fc5 100644 --- a/src/modules/applications/QML/QML.py +++ b/src/modules/applications/qml/QML.py @@ -19,7 +19,7 @@ class QML(Application, ABC): """ - QML Module for QUARK, is used by all QML applications. + qml Module for QUARK, is used by all qml applications. """ @abstractmethod diff --git a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py b/src/modules/applications/qml/generative_modeling/GenerativeModeling.py similarity index 93% rename from src/modules/applications/QML/generative_modeling/GenerativeModeling.py rename to src/modules/applications/qml/generative_modeling/GenerativeModeling.py index 6947e9c7..0472b1f1 100644 --- a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py +++ b/src/modules/applications/qml/generative_modeling/GenerativeModeling.py @@ -16,9 +16,9 @@ from utils import start_time_measurement, end_time_measurement from modules.applications.Application import Application -from modules.applications.QML.QML import QML -from modules.applications.QML.generative_modeling.data.data_handler.DiscreteData import DiscreteData -from modules.applications.QML.generative_modeling.data.data_handler.ContinuousData import ContinuousData +from modules.applications.qml.QML import QML +from modules.applications.qml.generative_modeling.data.data_handler.DiscreteData import DiscreteData +from modules.applications.qml.generative_modeling.data.data_handler.ContinuousData import ContinuousData class GenerativeModeling(QML): @@ -74,8 +74,8 @@ def get_parameter_options(self) -> dict: return { "n_qubits": { - "values": [4, 6, 8, 10, 12], - "description": "How many qubits do you want to use?" + "values": [4, 6, 8, 10, 12], + "description": "How many qubits do you want to use?" } } """ diff --git a/src/modules/applications/QML/generative_modeling/__init__.py b/src/modules/applications/qml/generative_modeling/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/__init__.py rename to src/modules/applications/qml/generative_modeling/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/data/MG_2D.npy b/src/modules/applications/qml/generative_modeling/data/MG_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/MG_2D.npy rename to src/modules/applications/qml/generative_modeling/data/MG_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/O_2D.npy b/src/modules/applications/qml/generative_modeling/data/O_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/O_2D.npy rename to src/modules/applications/qml/generative_modeling/data/O_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/Stocks_2D.npy b/src/modules/applications/qml/generative_modeling/data/Stocks_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/Stocks_2D.npy rename to src/modules/applications/qml/generative_modeling/data/Stocks_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/X_2D.npy b/src/modules/applications/qml/generative_modeling/data/X_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/X_2D.npy rename to src/modules/applications/qml/generative_modeling/data/X_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/__init__.py b/src/modules/applications/qml/generative_modeling/data/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/__init__.py rename to src/modules/applications/qml/generative_modeling/data/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py b/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py similarity index 94% rename from src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py index 0c3e39dd..5e1d6cf2 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py +++ b/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py @@ -19,9 +19,9 @@ import pkg_resources from utils import start_time_measurement, end_time_measurement -from modules.applications.QML.generative_modeling.transformations.MinMax import MinMax -from modules.applications.QML.generative_modeling.transformations.PIT import PIT -from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import DataHandler +from modules.applications.qml.generative_modeling.transformations.MinMax import MinMax +from modules.applications.qml.generative_modeling.transformations.PIT import PIT +from modules.applications.qml.generative_modeling.data.data_handler.DataHandler import DataHandler class ContinuousData(DataHandler): @@ -34,7 +34,7 @@ class ContinuousData(DataHandler): def __init__(self): """ The continuous data class loads a dataset from the path - src/modules/applications/QML/generative_modeling/data + src/modules/applications/qml/generative_modeling/data """ super().__init__("") self.submodule_options = ["PIT", "MinMax"] @@ -119,7 +119,7 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: self.n_qubits = gen_mod["n_qubits"] filename = pkg_resources.resource_filename( - 'modules.applications.QML.generative_modeling.data', + 'modules.applications.qml.generative_modeling.data', f"{self.dataset_name}.npy" ) self.dataset = np.load(filename) diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py b/src/modules/applications/qml/generative_modeling/data/data_handler/DataHandler.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/DataHandler.py diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py b/src/modules/applications/qml/generative_modeling/data/data_handler/DiscreteData.py similarity index 98% rename from src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/DiscreteData.py index 647a3f0b..a1c75267 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py +++ b/src/modules/applications/qml/generative_modeling/data/data_handler/DiscreteData.py @@ -20,8 +20,8 @@ import numpy as np from modules.circuits.CircuitCardinality import CircuitCardinality -from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import DataHandler -from modules.applications.QML.generative_modeling.data.data_handler.MetricsGeneralization import MetricsGeneralization +from modules.applications.qml.generative_modeling.data.data_handler.DataHandler import DataHandler +from modules.applications.qml.generative_modeling.data.data_handler.MetricsGeneralization import MetricsGeneralization from utils import start_time_measurement, end_time_measurement diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py b/src/modules/applications/qml/generative_modeling/data/data_handler/MetricsGeneralization.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/MetricsGeneralization.py diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/__init__.py b/src/modules/applications/qml/generative_modeling/data/data_handler/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/data_handler/__init__.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py similarity index 99% rename from src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py rename to src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py index df8501e3..d9fda5fb 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -28,7 +28,7 @@ from modules.training.QCBM import QCBM from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.mappings.Library import Library logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/qml/generative_modeling/mappings/Library.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/mappings/Library.py rename to src/modules/applications/qml/generative_modeling/mappings/Library.py diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py b/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py similarity index 99% rename from src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py rename to src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py index 3d2bb899..04edc1fa 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py +++ b/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py @@ -24,7 +24,7 @@ from modules.training.QCBM import QCBM from modules.training.QGAN import QGAN from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.mappings.Library import Library class LibraryPennylane(Library): diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py b/src/modules/applications/qml/generative_modeling/mappings/LibraryQiskit.py similarity index 99% rename from src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py rename to src/modules/applications/qml/generative_modeling/mappings/LibraryQiskit.py index b52e295b..5bc47c6c 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py +++ b/src/modules/applications/qml/generative_modeling/mappings/LibraryQiskit.py @@ -24,7 +24,7 @@ from modules.training.QCBM import QCBM from modules.training.QGAN import QGAN from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.mappings.Library import Library logging.getLogger("qiskit").setLevel(logging.WARNING) diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/qml/generative_modeling/mappings/PresetQiskitNoisyBackend.py similarity index 92% rename from src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py rename to src/modules/applications/qml/generative_modeling/mappings/PresetQiskitNoisyBackend.py index 152bf0f1..91972a22 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py +++ b/src/modules/applications/qml/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -26,7 +26,7 @@ from modules.training.QCBM import QCBM from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.mappings.Library import Library logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) @@ -63,24 +63,36 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: Dictionary with configurable settings. - .. code-block:: python + :return: Dictionary with configurable settings. + .. code-block:: python - return { + { "backend": { - "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", - "cusvaer_simulator (only available in cuQuantum applicance)", - "aer_simulator_gpu", - "aer_simulator_cpu", "ionQ_Harmony", "Amazon_SV1"], - "description": "Which backend do you want to use? (aer_statevector_simulator - uses the measurement probability vector, the others are shot based)" + "values": ["aer_simulator_gpu", "aer_simulator_cpu"], + "description": "Which backend do you want to use? " + "In the NoisyQiskit Module only aer_simulators can be used." + }, + + "simulation_method": { + "values": ["automatic", "statevector", "density_matrix", "cpu_mps"], # TODO Change names + "description": "What simulation methode should be used" }, "n_shots": { "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model? - (If the aer_statevector_simulator selected, - only relevant for studying generalization)" + "description": "How many shots do you want use for estimating the PMF of the model?" + }, + + "transpile_optimization_level": { + "values": [1, 2, 3, 0], + "description": "Switch between different optimization levels in the Qiskit transpile routine. " + "1: light optimization, 2: heavy optimization, 3: even heavier optimization, " + "0: no optimization. Level 1 recommended as standard option." + }, + + "noise_configuration": { + "values": value_list, + "description": "What noise configuration do you want to use?" } } """ diff --git a/src/modules/applications/QML/generative_modeling/mappings/__init__.py b/src/modules/applications/qml/generative_modeling/mappings/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/mappings/__init__.py rename to src/modules/applications/qml/generative_modeling/mappings/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py b/src/modules/applications/qml/generative_modeling/transformations/MinMax.py similarity index 99% rename from src/modules/applications/QML/generative_modeling/transformations/MinMax.py rename to src/modules/applications/qml/generative_modeling/transformations/MinMax.py index 723d403e..7b14f4d5 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/qml/generative_modeling/transformations/MinMax.py @@ -15,7 +15,7 @@ from typing import Union import numpy as np -from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation +from modules.applications.qml.generative_modeling.transformations.Transformation import Transformation from modules.circuits.CircuitStandard import CircuitStandard from modules.circuits.CircuitCardinality import CircuitCardinality diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/qml/generative_modeling/transformations/PIT.py similarity index 99% rename from src/modules/applications/QML/generative_modeling/transformations/PIT.py rename to src/modules/applications/qml/generative_modeling/transformations/PIT.py index 47f94329..42bdc178 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/qml/generative_modeling/transformations/PIT.py @@ -15,7 +15,7 @@ import numpy as np import pandas as pd -from modules.applications.QML.generative_modeling.transformations.Transformation import Transformation +from modules.applications.qml.generative_modeling.transformations.Transformation import Transformation from modules.circuits.CircuitCopula import CircuitCopula diff --git a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py b/src/modules/applications/qml/generative_modeling/transformations/Transformation.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/transformations/Transformation.py rename to src/modules/applications/qml/generative_modeling/transformations/Transformation.py diff --git a/src/modules/applications/QML/generative_modeling/transformations/__init__.py b/src/modules/applications/qml/generative_modeling/transformations/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/transformations/__init__.py rename to src/modules/applications/qml/generative_modeling/transformations/__init__.py diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/circuits/CircuitCardinality.py index f2cbe18c..03fd4c1d 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/circuits/CircuitCardinality.py @@ -15,10 +15,10 @@ from typing import Union, TypedDict from modules.circuits.Circuit import Circuit -from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit -from modules.applications.QML.generative_modeling.mappings.LibraryPennylane import LibraryPennylane -from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend -from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.qml.generative_modeling.mappings.LibraryPennylane import LibraryPennylane +from modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend class CircuitCardinality(Circuit): diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/circuits/CircuitCopula.py index 0683f394..26aba320 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/circuits/CircuitCopula.py @@ -17,10 +17,10 @@ from scipy.special import binom from modules.circuits.Circuit import Circuit -from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit -from modules.applications.QML.generative_modeling.mappings.LibraryPennylane import LibraryPennylane -from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend -from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.qml.generative_modeling.mappings.LibraryPennylane import LibraryPennylane +from modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend class CircuitCopula(Circuit): diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/circuits/CircuitStandard.py index 0c7afd8d..81a6a84f 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/circuits/CircuitStandard.py @@ -15,10 +15,10 @@ from typing import Union, TypedDict from modules.circuits.Circuit import Circuit -from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit -from modules.applications.QML.generative_modeling.mappings.LibraryPennylane import LibraryPennylane -from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend -from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.qml.generative_modeling.mappings.LibraryPennylane import LibraryPennylane +from modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend class CircuitStandard(Circuit): diff --git a/src/modules/training/Training.py b/src/modules/training/Training.py index 9fb2ec4e..20508698 100644 --- a/src/modules/training/Training.py +++ b/src/modules/training/Training.py @@ -78,7 +78,7 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f @abstractmethod def start_training(self, input_data: dict, config: any, **kwargs: dict) -> dict: """ - This function starts the training of QML model or deploys a pretrained model. + This function starts the training of qml model or deploys a pretrained model. :param input_data: A representation of the quantum machine learning model that will be trained :param config: Config specifying the parameters of the training (dict-like Config type defined in children) From 9fdb7a0804df002df4c90592202ab3d4025397e4 Mon Sep 17 00:00:00 2001 From: "Marvin Erdmann (FG-231)" Date: Mon, 14 Oct 2024 16:36:54 +0200 Subject: [PATCH 23/40] Update moduledb --- .settings/module_db.json | 52 ++++++++++++++++++--------------- .settings/requirements_full.txt | 4 +-- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/.settings/module_db.json b/.settings/module_db.json index dd32f798..8b7a55ff 100644 --- a/.settings/module_db.json +++ b/.settings/module_db.json @@ -1,7 +1,7 @@ { - "build_number": 13, - "build_date": "25-09-2024 22:08:09", - "git_revision_number": "150c71668c47ab9c98665d485437f6e9c7bb5e5f", + "build_number": 14, + "build_date": "14-10-2024 14:33:46", + "git_revision_number": "81265cfafef1726474dcfc1ed8c6ae04513236dc", "modules": [ { "name": "PVC", @@ -1841,6 +1841,10 @@ { "name": "numpy", "version": "1.26.4" + }, + { + "name": "qiskit-algorithms", + "version": "0.3.0" } ], "submodules": [ @@ -2169,13 +2173,13 @@ { "name": "GenerativeModeling", "class": "GenerativeModeling", - "module": "modules.applications.QML.generative_modeling.GenerativeModeling", + "module": "modules.applications.qml.generative_modeling.GenerativeModeling", "submodules": [ { "name": "Continuous Data", "class": "ContinuousData", "args": {}, - "module": "modules.applications.QML.generative_modeling.data.data_handler.ContinuousData", + "module": "modules.applications.qml.generative_modeling.data.data_handler.ContinuousData", "requirements": [ { "name": "numpy", @@ -2187,7 +2191,7 @@ "name": "PIT", "class": "PIT", "args": {}, - "module": "modules.applications.QML.generative_modeling.transformations.PIT", + "module": "modules.applications.qml.generative_modeling.transformations.PIT", "requirements": [ { "name": "numpy", @@ -2215,7 +2219,7 @@ "name": "LibraryQiskit", "class": "LibraryQiskit", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", "requirements": [ { "name": "qiskit", @@ -2304,7 +2308,7 @@ "name": "LibraryPennylane", "class": "LibraryPennylane", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", "requirements": [ { "name": "pennylane", @@ -2405,7 +2409,7 @@ "name": "CustomQiskitNoisyBackend", "class": "CustomQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -2469,7 +2473,7 @@ "name": "PresetQiskitNoisyBackend", "class": "PresetQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -2541,7 +2545,7 @@ "name": "MinMax", "class": "MinMax", "args": {}, - "module": "modules.applications.QML.generative_modeling.transformations.MinMax", + "module": "modules.applications.qml.generative_modeling.transformations.MinMax", "requirements": [ { "name": "numpy", @@ -2560,7 +2564,7 @@ "name": "LibraryQiskit", "class": "LibraryQiskit", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", "requirements": [ { "name": "qiskit", @@ -2649,7 +2653,7 @@ "name": "LibraryPennylane", "class": "LibraryPennylane", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", "requirements": [ { "name": "pennylane", @@ -2750,7 +2754,7 @@ "name": "CustomQiskitNoisyBackend", "class": "CustomQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -2814,7 +2818,7 @@ "name": "PresetQiskitNoisyBackend", "class": "PresetQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -2891,7 +2895,7 @@ "name": "LibraryQiskit", "class": "LibraryQiskit", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", "requirements": [ { "name": "qiskit", @@ -2980,7 +2984,7 @@ "name": "LibraryPennylane", "class": "LibraryPennylane", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", "requirements": [ { "name": "pennylane", @@ -3081,7 +3085,7 @@ "name": "CustomQiskitNoisyBackend", "class": "CustomQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -3145,7 +3149,7 @@ "name": "PresetQiskitNoisyBackend", "class": "PresetQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -3219,7 +3223,7 @@ "name": "Discrete Data", "class": "DiscreteData", "args": {}, - "module": "modules.applications.QML.generative_modeling.data.data_handler.DiscreteData", + "module": "modules.applications.qml.generative_modeling.data.data_handler.DiscreteData", "requirements": [ { "name": "numpy", @@ -3238,7 +3242,7 @@ "name": "LibraryQiskit", "class": "LibraryQiskit", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", "requirements": [ { "name": "qiskit", @@ -3327,7 +3331,7 @@ "name": "LibraryPennylane", "class": "LibraryPennylane", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", "requirements": [ { "name": "pennylane", @@ -3428,7 +3432,7 @@ "name": "CustomQiskitNoisyBackend", "class": "CustomQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", "requirements": [ { "name": "qiskit", @@ -3492,7 +3496,7 @@ "name": "PresetQiskitNoisyBackend", "class": "PresetQiskitNoisyBackend", "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", "requirements": [ { "name": "qiskit", diff --git a/.settings/requirements_full.txt b/.settings/requirements_full.txt index a6025883..ca23aca9 100644 --- a/.settings/requirements_full.txt +++ b/.settings/requirements_full.txt @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38b4f8ba527ee7c43b6f5cb7b1bfbbc82e7f5763067d28545b63d53b731fdc9f -size 818 +oid sha256:aff2ebd79e8689ec510cb160da2c5567e898b525459176e54f4a1d28ef6ac8a9 +size 780 From 92040c19d1d57d570d39377f9e3a32a8797b7383 Mon Sep 17 00:00:00 2001 From: "Greshma Shaji (FG-160)" Date: Mon, 14 Oct 2024 19:03:59 +0200 Subject: [PATCH 24/40] Autopep8 lint action added --- .github/workflows/lint.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 69090a1a..b5ea7e39 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -29,8 +29,11 @@ jobs: python-version: '3.9.16' token: ${{ secrets.QUARK_GH_GITHUB_COM_TOKEN }} - - name: Install pylint - run: pip install pylint + - name: Install pylint and autopep8 + run: pip install pylint autopep8 + + - name: Run autopep8 (fix PEP8 issues automatically) + run: autopep8 --in-place --recursive --aggressive --aggressive --max-line-length 120 . - name: Run pylint uses: wearerequired/lint-action@v2 From 2c277ee2afb20ce3789f7f1faef092d7d329f9aa Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 19:56:57 +0200 Subject: [PATCH 25/40] autopep8 lint action added --- .github/workflows/lint.yml | 10 +++++++++- src/BenchmarkManager.py | 8 +++++--- src/utils.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b5ea7e39..3919af4b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,7 +33,15 @@ jobs: run: pip install pylint autopep8 - name: Run autopep8 (fix PEP8 issues automatically) - run: autopep8 --in-place --recursive --aggressive --aggressive --max-line-length 120 . + run: autopep8 --in-place --recursive --aggressive --aggressive --max-line-length 120 -v . + + - name: Commit changes + run: | + git config --global user.name "Github Action" + git config --global user.email "action@github.com" + git add . + git commit -m "Apply autopep8 formatting" + git push - name: Run pylint uses: wearerequired/lint-action@v2 diff --git a/src/BenchmarkManager.py b/src/BenchmarkManager.py index ed71f517..dd9d4d6e 100644 --- a/src/BenchmarkManager.py +++ b/src/BenchmarkManager.py @@ -382,9 +382,11 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i ) output = module_instance.postprocessed_input else: - instruction, processed_input, benchmark_record = self.traverse_config(module["submodule"], - module_instance.preprocessed_input, path, - rep_count, previous_job_info=submodule_job_info) + instruction, processed_input, benchmark_record = self.traverse_config( + module["submodule"], + module_instance.preprocessed_input, path, + rep_count, previous_job_info=submodule_job_info + ) if instruction == Instruction.PROCEED: instruction, module_instance.postprocessed_input, postprocessing_time = postprocess( diff --git a/src/utils.py b/src/utils.py index 6a041be7..d45993cc 100644 --- a/src/utils.py +++ b/src/utils.py @@ -162,7 +162,7 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: _expand_paths(entry, base_dir) else: for attr in j: - if type(j[attr]) is "submodules": + if type(j[attr]) == "submodules": _expand_paths(j[attr], base_dir) elif attr == "dir": p = j[attr] From 2773417778278c29a157ca0eeb87b8cf4b2a3b4e Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 20:05:14 +0200 Subject: [PATCH 26/40] autopep8 lint action added --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3919af4b..eeb96c4e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -41,7 +41,7 @@ jobs: git config --global user.email "action@github.com" git add . git commit -m "Apply autopep8 formatting" - git push + git push origin HEAD:&{{ github.ref }} - name: Run pylint uses: wearerequired/lint-action@v2 From 0ba39ec2da7cdf23936e2da6bbf06617e3a37d32 Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 20:06:37 +0200 Subject: [PATCH 27/40] autopep8 lint action added --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index eeb96c4e..76f7a905 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -41,7 +41,7 @@ jobs: git config --global user.email "action@github.com" git add . git commit -m "Apply autopep8 formatting" - git push origin HEAD:&{{ github.ref }} + git push origin HEAD:${{ github.ref }} - name: Run pylint uses: wearerequired/lint-action@v2 From 012b581b2fb1b1b52730a779aa3d24c6077618dd Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 20:09:58 +0200 Subject: [PATCH 28/40] autopep8 lint action added --- .github/workflows/lint.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 76f7a905..45682509 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,6 +31,9 @@ jobs: - name: Install pylint and autopep8 run: pip install pylint autopep8 + + -name: Disable Git LFS locking + run: git config lfs.https://github.com/QUARK-framework/QUARK.git/info/lfs.locksverify false - name: Run autopep8 (fix PEP8 issues automatically) run: autopep8 --in-place --recursive --aggressive --aggressive --max-line-length 120 -v . From 7a4dfc3b497e7f225100c379de3dda4d5c5528e1 Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 20:11:38 +0200 Subject: [PATCH 29/40] autopep8 lint action added --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 45682509..ada1dfe3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,7 +32,7 @@ jobs: - name: Install pylint and autopep8 run: pip install pylint autopep8 - -name: Disable Git LFS locking + - name: Disable Git LFS locking run: git config lfs.https://github.com/QUARK-framework/QUARK.git/info/lfs.locksverify false - name: Run autopep8 (fix PEP8 issues automatically) From 19c705722010b21a2936209afca0af63fa76b7f1 Mon Sep 17 00:00:00 2001 From: Github Action Date: Mon, 14 Oct 2024 18:12:06 +0000 Subject: [PATCH 30/40] Apply autopep8 formatting --- src/BenchmarkManager.py | 14 ++--- src/ConfigManager.py | 4 +- src/Plotter.py | 2 +- .../applications/optimization/ACL/ACL.py | 54 +++++++++---------- .../applications/optimization/ACL/__init__.py | 2 +- .../optimization/ACL/mappings/QUBO.py | 6 +-- .../applications/optimization/MIS/MIS.py | 2 +- .../applications/optimization/PVC/PVC.py | 8 +-- .../optimization/PVC/mappings/ISING.py | 2 +- .../optimization/SAT/mappings/ChoiQUBO.py | 2 +- .../optimization/SAT/mappings/Direct.py | 2 +- .../optimization/SAT/mappings/QubovertQUBO.py | 2 +- .../TSP/data/createReferenceGraph.py | 2 +- .../applications/optimization/__init__.py | 2 +- .../generative_modeling/GenerativeModeling.py | 1 - .../data/data_handler/ContinuousData.py | 2 +- .../mappings/CustomQiskitNoisyBackend.py | 10 ++-- .../generative_modeling/mappings/Library.py | 2 +- .../mappings/LibraryPennylane.py | 9 ++-- .../transformations/MinMax.py | 2 +- .../transformations/PIT.py | 4 +- .../transformations/Transformation.py | 2 +- src/modules/circuits/Circuit.py | 2 +- src/modules/circuits/CircuitCardinality.py | 6 +-- src/modules/circuits/CircuitCopula.py | 2 +- src/modules/circuits/CircuitStandard.py | 4 +- src/modules/devices/braket/Braket.py | 6 +-- src/modules/devices/pulser/Pulser.py | 2 +- src/modules/solvers/ClassicalSAT.py | 2 +- src/modules/solvers/NeutralAtomMIS.py | 2 +- src/modules/solvers/QAOA.py | 4 +- src/modules/solvers/QiskitQAOA.py | 1 + src/modules/solvers/RandomClassicalPVC.py | 2 +- src/modules/solvers/RandomClassicalSAT.py | 2 +- src/modules/training/Inference.py | 2 +- src/modules/training/QCBM.py | 4 +- src/modules/training/QGAN.py | 10 ++-- src/modules/training/Training.py | 2 +- src/quark2_adapter/adapters.py | 10 ++-- .../legacy_classes/Application.py | 2 +- src/quark2_adapter/legacy_classes/Solver.py | 2 +- src/utils.py | 7 +-- 42 files changed, 105 insertions(+), 105 deletions(-) diff --git a/src/BenchmarkManager.py b/src/BenchmarkManager.py index dd9d4d6e..766f9b5d 100644 --- a/src/BenchmarkManager.py +++ b/src/BenchmarkManager.py @@ -117,7 +117,7 @@ def load_interrupted_results(self) -> Optional[list]: """ if self.interrupted_results_path is None or not os.path.exists(self.interrupted_results_path): return None - with open(self.interrupted_results_path, encoding='utf-8') as results_file : + with open(self.interrupted_results_path, encoding='utf-8') as results_file: results = json.load(results_file) return results @@ -328,7 +328,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # p rel_path = self.store_dir logging.info("====== There are interrupted jobs. You may resume them by running QUARK with") logging.info(f"====== --resume-dir={rel_path}") - logging.info(80*"=") + logging.info(80 * "=") logging.info("") # pylint: disable=R0917 @@ -374,11 +374,11 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i if not module["submodule"]: # If we reach the end of the chain we create the benchmark record, fill it and then pass it up instruction, module_instance.postprocessed_input, postprocessing_time = postprocess( - module_instance, - module_instance.preprocessed_input, - module["config"], store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info + module_instance, + module_instance.preprocessed_input, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info ) output = module_instance.postprocessed_input else: diff --git a/src/ConfigManager.py b/src/ConfigManager.py index 17a8f66e..cec96b93 100644 --- a/src/ConfigManager.py +++ b/src/ConfigManager.py @@ -97,7 +97,7 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: repetitions_answer = inquirer.prompt( [inquirer.Text('repetitions', message="How many repetitions do you want?", - validate=lambda _, x: re.match("\\d", x),default=1)]) + validate=lambda _, x: re.match("\\d", x), default=1)]) self.config["repetitions"] = int(repetitions_answer["repetitions"]) def query_module(self, module: Core, module_friendly_name: str) -> ConfigModule: @@ -359,7 +359,7 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: choices.append("Custom Range") # Add custom_input if it is specified in the parameters - answer = checkbox(key=key,message=f"{prefix} {config_answer['description']}", choices=choices) + answer = checkbox(key=key, message=f"{prefix} {config_answer['description']}", choices=choices) values = answer[key] if "Custom Input" in values: diff --git a/src/Plotter.py b/src/Plotter.py index 0805a051..8ca02ab0 100644 --- a/src/Plotter.py +++ b/src/Plotter.py @@ -222,7 +222,7 @@ def _extract_columns(config: dict, rest_result: dict) -> dict: """ Function to extract and summarize certain data fields like the time spent in every module from the nested module chain. - + :param config: Dictionary containing multiple data fields like the config of a module :param rest_result: Rest of the module chain :return: Extracted data diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index 1bb8f8b5..fabea710 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -43,13 +43,13 @@ class ACL(Optimization): """ The distribution of passenger vehicles is a complex task and a high cost factor for automotive original equipment manufacturers (OEMs). Vehicles travel long distance on different carriers, such as ships, - trains, and trucks, from the production plant to the customer. - - To save costs, OEMs and logistics service providers aim to maximize their loading capacities. - Modern auto carriers are flexible, allowing individual platforms to be rotated, extended, or combined - to accommodate vehicles of different shapes and weights in a space-efficient manner. - - In practice, finding feasible combinations is often based on heuristics or personal experience. + trains, and trucks, from the production plant to the customer. + + To save costs, OEMs and logistics service providers aim to maximize their loading capacities. + Modern auto carriers are flexible, allowing individual platforms to be rotated, extended, or combined + to accommodate vehicles of different shapes and weights in a space-efficient manner. + + In practice, finding feasible combinations is often based on heuristics or personal experience. We formulate the problem as a mixed integer quadratically constrained assignment problem. """ @@ -224,7 +224,7 @@ def _generate_tiny_model(self, df: any, vehicles: list) -> None: # (4) Weight constraint for every level for p_l in plats_l: prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ - wl[p_l] + wl[p_l] # (5) Weight constraint for truck and trailer for t in plats_t: @@ -306,7 +306,7 @@ def _generate_small_model(self, df: any, vehicles: list) -> None: # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used for q in plats_sp: prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ - <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] + <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] # (4) It is always only possible to use a single split-platform for any given p for q in plats_sp: @@ -326,7 +326,7 @@ def _generate_small_model(self, df: any, vehicles: list) -> None: # Truck for h in plats_h1: prob += pulp.lpSum(x[p, v] * height_list[v] for p in platforms_height_array_truck[h] for v in vecs) \ - <= hmax_truck[h] + <= hmax_truck[h] # (7) Linearization constraint -> gamma == 1, if split platform is used for q in plats_sp: @@ -343,12 +343,12 @@ def _generate_small_model(self, df: any, vehicles: list) -> None: for q in plats_sp: for p in split_platforms_array[q]: prob += pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] \ - + (1 - gamma[q]) * wp[p] + + (1 - gamma[q]) * wp[p] # (10) Weight constraint for every level for p_l in plats_l: prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ - wl[p_l] + wl[p_l] # (11) Weight constraint for truck and trailer for p_t in plats_t: @@ -468,7 +468,7 @@ def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disa # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used for q in plats_sp: prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ - <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] + <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] # (3.1) It is always only possible to use a single split-platform for any given p for q in plats_sp: @@ -535,16 +535,16 @@ def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disa * int(v_coef[class_list[v]][3] * length_list[v]) for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) for v in vecs) \ - + pulp.lpSum(x[p, v] * length_list[v] - for p in self.diffset(platforms_level_array[L], platforms_angled_array) - for v in vecs) \ - <= lmax_l[L] + + pulp.lpSum(x[p, v] * length_list[v] + for p in self.diffset(platforms_level_array[L], platforms_angled_array) + for v in vecs) \ + <= lmax_l[L] # (5) Platforms can not be angled, if they are part of a split platform for q in plats_sp: prob += pulp.lpSum(a_p[platforms_angled_array.index(p)] for p in self.intersectset(platforms_angled_array, split_platforms_array[q])) \ - <= len(split_platforms_array[q]) * (1 - sp[q]) + <= len(split_platforms_array[q]) * (1 - sp[q]) # (6) Weight constraint if split platform is used, gamma == 1 for q in plats_sp: @@ -567,7 +567,7 @@ def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disa for p in platforms_angled_array: prob += pulp.lpSum(weight_list[v] * apx[platforms_angled_array.index(p), v] for v in vecs) \ - <= wpa[platforms_angled_array.index(p)] + <= wpa[platforms_angled_array.index(p)] # (8) Weight constraint for every level for p_l in plats_l: @@ -596,10 +596,10 @@ def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disa int(h_coef[class_list[v]][3] * height_list[v]) for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) for v in vecs) \ - + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) - for v in vecs) \ - <= hmax_truck[h] + + pulp.lpSum(x[p, v] * height_list[v] + for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) + for v in vecs) \ + <= hmax_truck[h] # Trailer for h in plats_h2: prob += pulp.lpSum(x[p, v] * height_list[v] @@ -613,10 +613,10 @@ def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disa int(h_coef[class_list[v]][3] * height_list[v]) for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) for v in vecs) \ - + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) - for v in vecs) \ - <= hmax_trailer[h] + + pulp.lpSum(x[p, v] * height_list[v] + for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) + for v in vecs) \ + <= hmax_trailer[h] self.application = prob diff --git a/src/modules/applications/optimization/ACL/__init__.py b/src/modules/applications/optimization/ACL/__init__.py index 2b03c28f..b22c875d 100644 --- a/src/modules/applications/optimization/ACL/__init__.py +++ b/src/modules/applications/optimization/ACL/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" +""" Module containing the ACL This module initializes the ACL application, which is responsible for formulating diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index bc99746c..27adacf1 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -174,9 +174,9 @@ def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarra if isinstance(argument, list): # squared variables in diagonals (x^2 == x) if ( - len(argument) == 2 - and any(isinstance(elem, str) and variable in elem for elem in argument) - and col == row + len(argument) == 2 + and any(isinstance(elem, str) and variable in elem for elem in argument) + and col == row ): parameter += argument[0] # Multiplication of different variables not on diagonal diff --git a/src/modules/applications/optimization/MIS/MIS.py b/src/modules/applications/optimization/MIS/MIS.py index 7037c9ee..7636cef9 100644 --- a/src/modules/applications/optimization/MIS/MIS.py +++ b/src/modules/applications/optimization/MIS/MIS.py @@ -41,7 +41,7 @@ class MIS(Optimization): In the context of QUARK, we employ quantum-inspired approaches and state-of-the-art classical algorithms to tackle the problem. The graph is generated based on user-defined parameters such as size, spacing, and - filling fraction, which affect the complexity and properties of the generated instance. + filling fraction, which affect the complexity and properties of the generated instance. """ def __init__(self): diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index d07c0756..fb04f1a1 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -31,14 +31,14 @@ class PVC(Optimization): In modern vehicle manufacturing, robots take on a significant workload, including performing welding jobs, sealing welding joints, or applying paint to the car body. While the robot’s tasks vary widely, the objective remains the same: Perform a job with the highest possible quality in the shortest amount - of time, optimizing efficiency and productivity on the manufacturing line. - + of time, optimizing efficiency and productivity on the manufacturing line. + For instance, to protect a car’s underbody from corrosion, exposed welding seams are sealed by applying a polyvinyl chloride layer (PVC). The welding seams need to be traversed by a robot to apply the material. It is related to TSP, but different and even more complex in some aspects. The problem of determining the optimal route for robots to traverse all seams shares similarities - with Traveling Salesman Problem (TSP), as it involves finding the shortest possible route to + with Traveling Salesman Problem (TSP), as it involves finding the shortest possible route to visit multiple locations. However, it introduces additional complexities, such as different tool and configuration requirements for each seam, making it an even more challenging problem to solve. """ @@ -147,7 +147,7 @@ def generate_problem(self, config: Config) -> nx.Graph: # Get number of seam in graph seams_in_graph = list({x[0] for x in graph.nodes}) seams_in_graph.sort() - seams_in_graph.remove(0) # Always need the base node 0 (which is not a seam) + seams_in_graph.remove(0) # Always need the base node 0 (which is not a seam) if len(seams_in_graph) < seams: logging.info("Too many seams! The original graph has less seams than that!") diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index dfd45ec5..6c5ded38 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -78,7 +78,7 @@ class Config(TypedDict): Configuration attributes for Ising mapping. Attributes: - lagrange_factor (float): Factor to multiply the Langrange. + lagrange_factor (float): Factor to multiply the Langrange. """ lagrange_factor: float diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py index 5ecf048e..e8a626ee 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py @@ -68,7 +68,7 @@ def get_parameter_options(self) -> dict: "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], "description": ( - "What Bh/A ratio do you want?" + "What Bh/A ratio do you want?" "(How strongly to enforce hard constraints)" ) }, diff --git a/src/modules/applications/optimization/SAT/mappings/Direct.py b/src/modules/applications/optimization/SAT/mappings/Direct.py index 0b57b608..fb889e10 100644 --- a/src/modules/applications/optimization/SAT/mappings/Direct.py +++ b/src/modules/applications/optimization/SAT/mappings/Direct.py @@ -37,7 +37,7 @@ def __init__(self): self.submodule_options = ["ClassicalSAT", "RandomSAT"] @staticmethod - def get_requirements() ->list[dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. diff --git a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py index fee12035..4d763d3b 100644 --- a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py @@ -85,7 +85,7 @@ def _constraints2qubovert(constraints: any) -> AND: """ Converts the constraints nnf to a PUBO in the qubovert library. - :param constraints: Constraints in nnf format + :param constraints: Constraints in nnf format :return: Constraints in qubovert format """ clauses = [] diff --git a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py index 96214f2c..04abb882 100644 --- a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py @@ -34,7 +34,7 @@ def main(): for edge in graph.edges: if edge[0] == edge[1]: graph.remove_edge(edge[0], edge[1]) - + print("Loaded graph:") print(nx.info(graph)) diff --git a/src/modules/applications/optimization/__init__.py b/src/modules/applications/optimization/__init__.py index 2bda246e..658c9cb8 100644 --- a/src/modules/applications/optimization/__init__.py +++ b/src/modules/applications/optimization/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" +""" Module containing all optimization applications """ diff --git a/src/modules/applications/qml/generative_modeling/GenerativeModeling.py b/src/modules/applications/qml/generative_modeling/GenerativeModeling.py index 0472b1f1..b1c2852d 100644 --- a/src/modules/applications/qml/generative_modeling/GenerativeModeling.py +++ b/src/modules/applications/qml/generative_modeling/GenerativeModeling.py @@ -87,7 +87,6 @@ def get_parameter_options(self) -> dict: } def generate_problem(self, config: dict) -> dict: - """ The number of qubits is chosen for this problem. diff --git a/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py b/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py index 5e1d6cf2..37b9fb1f 100644 --- a/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py +++ b/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py @@ -33,7 +33,7 @@ class ContinuousData(DataHandler): def __init__(self): """ - The continuous data class loads a dataset from the path + The continuous data class loads a dataset from the path src/modules/applications/qml/generative_modeling/data """ super().__init__("") diff --git a/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py index d9fda5fb..c937bbe0 100644 --- a/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -264,7 +264,7 @@ def select_backend(config: str, n_qubits: int) -> Backend: return backend - def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict # pylint: disable=W0221 + def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict # pylint: disable=W0221 ) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, @@ -283,7 +283,7 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: backend = self.decompile_noisy_config(config_dict, n_qubits) logging.info(f'Backend in Use: {backend=}') optimization_level = self.get_transpile_routine(config_dict['transpile_optimization_level']) - seed_transp = 42 # Remove seed if wanted + seed_transp = 42 # Remove seed if wanted logging.info(f'Using {optimization_level=} with seed: {seed_transp}') coupling_map = self.get_coupling_map(config_dict, n_qubits) @@ -299,7 +299,7 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: # Now transpile the circuit after running the pass manager circuit_transpiled = transpile(circuit_passed, backend=backend, optimization_level=optimization_level, - seed_transpiler=seed_transp,coupling_map=coupling_map) + seed_transpiler=seed_transp, coupling_map=coupling_map) logging.info(f'Circuit operations before transpilation: {circuit.count_ops()}') logging.info(f'Circuit operations after transpilation: {circuit_transpiled.count_ops()}') logging.info(perf_counter() - start) @@ -363,8 +363,8 @@ def get_simulation_method_and_device(self, device: str, simulation_config: str) """ This method specifies the simulation methode and processing unit. - :param device: Contains information about processing unit - :param simulation_config: Contains information about qiskit simulation method + :param device: Contains information about processing unit + :param simulation_config: Contains information about qiskit simulation method :return: Tuple containing the simulation method and device """ simulation_method = { diff --git a/src/modules/applications/qml/generative_modeling/mappings/Library.py b/src/modules/applications/qml/generative_modeling/mappings/Library.py index 6ea660d9..1882c3b9 100644 --- a/src/modules/applications/qml/generative_modeling/mappings/Library.py +++ b/src/modules/applications/qml/generative_modeling/mappings/Library.py @@ -74,7 +74,7 @@ def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - This method corresponds to the identity and passes the information of the subsequent module + This method corresponds to the identity and passes the information of the subsequent module back to the preceding module in the benchmarking process. :param input_data: Collected information of the benchmarking procesS diff --git a/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py b/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py index 04edc1fa..c87a82df 100644 --- a/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py +++ b/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from modules.applications.qml.generative_modeling.mappings.Library import Library +from modules.training.Inference import Inference +from modules.training.QGAN import QGAN +from modules.training.QCBM import QCBM from typing import Union import numpy as np @@ -21,11 +25,6 @@ jax.config.update("jax_enable_x64", True) -from modules.training.QCBM import QCBM -from modules.training.QGAN import QGAN -from modules.training.Inference import Inference -from modules.applications.qml.generative_modeling.mappings.Library import Library - class LibraryPennylane(Library): diff --git a/src/modules/applications/qml/generative_modeling/transformations/MinMax.py b/src/modules/applications/qml/generative_modeling/transformations/MinMax.py index 7b14f4d5..729eaf2e 100644 --- a/src/modules/applications/qml/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/qml/generative_modeling/transformations/MinMax.py @@ -97,7 +97,7 @@ def transform(self, input_data: dict, config: dict) -> dict: value = 0 for count in histogram_transformed_1d: if count > 0: - solution_space[position:position+int(count)] = value + solution_space[position:position + int(count)] = value position += int(count) value += 1 diff --git a/src/modules/applications/qml/generative_modeling/transformations/PIT.py b/src/modules/applications/qml/generative_modeling/transformations/PIT.py index 42bdc178..959f430f 100644 --- a/src/modules/applications/qml/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/qml/generative_modeling/transformations/PIT.py @@ -21,7 +21,7 @@ class PIT(Transformation): # pylint disable=R0902 """ - The transformation of the original probability distribution to + The transformation of the original probability distribution to the distribution of its uniformly distributed cumulative marginals is known as the copula. """ @@ -100,7 +100,7 @@ def transform(self, input_data: dict, config: dict) -> dict: value = 0 for count in histogram_transformed_1d: if count > 0: - solution_space[position:position+int(count)] = value + solution_space[position:position + int(count)] = value position += int(count) value += 1 diff --git a/src/modules/applications/qml/generative_modeling/transformations/Transformation.py b/src/modules/applications/qml/generative_modeling/transformations/Transformation.py index a52853e0..d76f4a2c 100644 --- a/src/modules/applications/qml/generative_modeling/transformations/Transformation.py +++ b/src/modules/applications/qml/generative_modeling/transformations/Transformation.py @@ -77,7 +77,7 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f @abstractmethod def transform(self, input_data: dict, config: dict) -> dict: """ - Helps to ensure that the model can effectively learn the underlying + Helps to ensure that the model can effectively learn the underlying patterns and structure of the data, and produce high-quality outputs. :param input_data: Input data for transformation diff --git a/src/modules/circuits/Circuit.py b/src/modules/circuits/Circuit.py index 8e5b339f..c3bd1540 100644 --- a/src/modules/circuits/Circuit.py +++ b/src/modules/circuits/Circuit.py @@ -63,7 +63,7 @@ def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, fl def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - Method that passes back information of the subsequent modules to the preceding modules. + Method that passes back information of the subsequent modules to the preceding modules. :param input_data: Collected information of the benchmarking process :param config: Config specifying the number of qubits of the circuit diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/circuits/CircuitCardinality.py index 03fd4c1d..84a05243 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/circuits/CircuitCardinality.py @@ -24,7 +24,7 @@ class CircuitCardinality(Circuit): """ This class generates a library-agnostic gate sequence, i.e. a list containing information - about the gates and the wires they act on. + about the gates and the wires they act on. The circuit follows the implementation by Gili et al. https://arxiv.org/abs/2207.13645 """ @@ -96,7 +96,7 @@ class Config(TypedDict): def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ Returns gate sequence of cardinality circuit architecture. - + :param input_data: Collection of information from the previous modules :param config: Config specifying the number of qubits of the circuit :return: Dictionary including the gate sequence of the Cardinality Circuit @@ -142,7 +142,7 @@ def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: "store_dir_iter": input_data["store_dir_iter"], "train_size": input_data["train_size"], "dataset_name": input_data["dataset_name"], - "binary_train":input_data["binary_train"] + "binary_train": input_data["binary_train"] } return output_dict diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/circuits/CircuitCopula.py index 26aba320..3ebbdbb7 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/circuits/CircuitCopula.py @@ -107,7 +107,7 @@ class Config(TypedDict): def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ Returns gate sequence of copula architecture. - + :param input_data: Collection of information from the previous modules :param config: Config specifying the number of qubits of the circuit :return: Dictionary including the gate sequence of the Copula Circuit diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/circuits/CircuitStandard.py index 81a6a84f..a5ea2455 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/circuits/CircuitStandard.py @@ -105,7 +105,7 @@ class Config(TypedDict): def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ Returns gate sequence of standard architecture. - + :param input_data: Collection of information from the previous modules :param config: Config specifying the number of qubits of the circuit :return: Dictionary including the gate sequence of the Standard Circuit @@ -141,7 +141,7 @@ def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: "store_dir_iter": input_data["store_dir_iter"], "train_size": input_data["train_size"], "dataset_name": input_data["dataset_name"], - "binary_train":input_data["binary_train"] + "binary_train": input_data["binary_train"] } return output_dict diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index 66084fdd..eb068ea4 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -67,7 +67,7 @@ def _configure_aws_session(self, region: str) -> None: def _setup_proxy() -> any: """ Sets up proxy configuration if available in the environment variables. - + :return: Proxy definitions """ if 'HTTP_PROXY' in os.environ: @@ -86,7 +86,7 @@ def _setup_proxy() -> any: def _set_region(region: str) -> str: """ Sets the AWS region from the environment variable or defaults to 'us-east-1'. - + :param region: Provided region :return: Final region to be used """ @@ -99,7 +99,7 @@ def _set_region(region: str) -> str: def _set_profile() -> str: """ Determines the AWS profile to use for the session. - + :return: AWS profile name """ if 'AWS_PROFILE' in os.environ: diff --git a/src/modules/devices/pulser/Pulser.py b/src/modules/devices/pulser/Pulser.py index 46de8f75..5edd43fe 100644 --- a/src/modules/devices/pulser/Pulser.py +++ b/src/modules/devices/pulser/Pulser.py @@ -56,4 +56,4 @@ def get_requirements() -> list[dict]: :return: List of dict with requirements of this module """ - return [{"name": "pulser","version": "0.19.0"}] + return [{"name": "pulser", "version": "0.19.0"}] diff --git a/src/modules/solvers/ClassicalSAT.py b/src/modules/solvers/ClassicalSAT.py index a0308b13..afc70f53 100644 --- a/src/modules/solvers/ClassicalSAT.py +++ b/src/modules/solvers/ClassicalSAT.py @@ -36,7 +36,7 @@ def __init__(self): self.submodule_options = ["Local"] @staticmethod - def get_requirements() -> list[dict]: + def get_requirements() -> list[dict]: """ Return requirements of this module. diff --git a/src/modules/solvers/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py index 9fe81942..b0d99d63 100644 --- a/src/modules/solvers/NeutralAtomMIS.py +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -181,7 +181,7 @@ def _create_pulses(self, device: pulser.devices._device_datacls.Device) -> list[ return pulses - def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> dict: + def _filter_invalid_states(self, state_counts: dict, nodes: list, edges: list) -> dict: """ Filters out invalid states that do not meet the problem constraints. diff --git a/src/modules/solvers/QAOA.py b/src/modules/solvers/QAOA.py index 3016b6ae..d6f44712 100644 --- a/src/modules/solvers/QAOA.py +++ b/src/modules/solvers/QAOA.py @@ -325,7 +325,7 @@ def circuit(params: np.array, device: AwsDevice, n_qubits: int, ising: np.ndarra # pylint: disable=R0917 # pylint: disable=R0913 def objective_function(params: np.array, device: AwsDevice, ising: np.ndarray, n_qubits: int, n_shots: int, - tracker: dict, s3_folder: tuple[str,str], verbose: bool) -> float: + tracker: dict, s3_folder: tuple[str, str], verbose: bool) -> float: """ Objective function takes a list of variational parameters as input, and returns the cost associated with those parameters. @@ -411,7 +411,7 @@ def objective_function(params: np.array, device: AwsDevice, ising: np.ndarray, n # The function to execute the training: run classical minimization. # pylint: disable=R0917 def train(device: AwsDevice, options: dict, p: int, ising: np.ndarray, n_qubits: int, n_shots: int, opt_method: str, - tracker: dict, s3_folder: tuple[str,str], verbose: bool = True) -> tuple[float, np.ndarray, dict]: + tracker: dict, s3_folder: tuple[str, str], verbose: bool = True) -> tuple[float, np.ndarray, dict]: """ Function to run QAOA algorithm for given, fixed circuit depth p. diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index 1d430cb0..5b41a5fc 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.py @@ -29,6 +29,7 @@ from modules.Core import Core from utils import start_time_measurement, end_time_measurement + class QiskitQAOA(Solver): """ Qiskit QAOA. diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index f388675c..12aa6688 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -97,7 +97,7 @@ def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **k # Get the random neighbor edge from the current node next_node = random.choice([ x for x in mapped_problem.edges(current_node[0], data=True) - if x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] and x[2]['t_start'] \ + if x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] ]) next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) diff --git a/src/modules/solvers/RandomClassicalSAT.py b/src/modules/solvers/RandomClassicalSAT.py index 8f942b92..4dc94dea 100644 --- a/src/modules/solvers/RandomClassicalSAT.py +++ b/src/modules/solvers/RandomClassicalSAT.py @@ -72,7 +72,7 @@ class Config(TypedDict): def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) \ -> tuple[list, float, dict]: """ - The given application is a problem instance from the pysat library. + The given application is a problem instance from the pysat library. This generates a random solution to the problem. :param mapped_problem: The WCNF representation of the SAT problem diff --git a/src/modules/training/Inference.py b/src/modules/training/Inference.py index 7d8b1d31..502677af 100644 --- a/src/modules/training/Inference.py +++ b/src/modules/training/Inference.py @@ -19,7 +19,7 @@ class Inference(Training): """ - This module executes a quantum circuit with parameters of a pretrained model. + This module executes a quantum circuit with parameters of a pretrained model. """ def __init__(self): diff --git a/src/modules/training/QCBM.py b/src/modules/training/QCBM.py index 92e602ce..b610b1e7 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/training/QCBM.py @@ -280,9 +280,9 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict self.writer.close() input_data["best_parameter"] = es.result[0] - best_sample = self.sample_from_pmf(best_pmf.get() if GPU else best_pmf, # pylint: disable=E0606 + best_sample = self.sample_from_pmf(best_pmf.get() if GPU else best_pmf, # pylint: disable=E0606 n_shots=input_data["n_shots"]) - input_data["best_sample"] = best_sample.get() if GPU else best_sample # pylint: disable=E1101 + input_data["best_sample"] = best_sample.get() if GPU else best_sample # pylint: disable=E1101 return input_data diff --git a/src/modules/training/QGAN.py b/src/modules/training/QGAN.py index f3016e33..5b0b080a 100644 --- a/src/modules/training/QGAN.py +++ b/src/modules/training/QGAN.py @@ -16,7 +16,7 @@ import logging import torch -from torch.utils.data import DataLoader +from torch.utils.data import DataLoader from torch import nn import torch.nn.functional as funct from tensorboardX import SummaryWriter @@ -223,7 +223,7 @@ def setup_training(self, input_data: dict, config: dict) -> None: self.bins_train = input_data["binary_train"] if input_data["dataset_name"] == "Cardinality_Constraint": new_size = 1000 - self.bins_train = np.repeat(self.bins_train,new_size,axis=0) + self.bins_train = np.repeat(self.bins_train, new_size, axis=0) self.study_generalization = "generalization_metrics" in list(input_data.keys()) if self.study_generalization: @@ -301,8 +301,8 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict out_d_fake = self.discriminator(fake_data).view(-1) err_g = self.criterion(out_d_fake, self.real_labels) - fake_data, _ = self.generator.execute(self.params,self.batch_size) - gradients= self.generator.compute_gradient( + fake_data, _ = self.generator.execute(self.params, self.batch_size) + gradients = self.generator.compute_gradient( self.params, self.discriminator, self.criterion, @@ -321,7 +321,7 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict _, pmfs_model = self.generator.execute(self.params, self.n_shots) pmfs_model = np.asarray(pmfs_model.copy()) - loss= self.loss_func(pmfs_model[None,], self.target) + loss = self.loss_func(pmfs_model[None,], self.target) self.accuracy.append(loss) self.writer.add_scalar("metrics/KL", loss, epoch * n_batches + batch) diff --git a/src/modules/training/Training.py b/src/modules/training/Training.py index 20508698..f65567aa 100644 --- a/src/modules/training/Training.py +++ b/src/modules/training/Training.py @@ -130,7 +130,7 @@ def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: :return: Maximum mean discrepancy """ pmf_model[pmf_model == 0] = 1e-8 - sigma = 1/pmf_model.shape[1] + sigma = 1 / pmf_model.shape[1] kernel_distance = np.exp((-np.square(pmf_model - pmf_target) / (sigma ** 2))) mmd = 2 - 2 * np.mean(kernel_distance, axis=1) return mmd diff --git a/src/quark2_adapter/adapters.py b/src/quark2_adapter/adapters.py index e01bbf5d..6e864738 100644 --- a/src/quark2_adapter/adapters.py +++ b/src/quark2_adapter/adapters.py @@ -52,7 +52,7 @@ def __init__(self, application_name: str, *args, **kwargs): """ Constructor method. """ - logging.warning(WARNING_MSG, self.__class__.__name__) + logging.warning(WARNING_MSG, self.__class__.__name__) Application_NEW.__init__(self, application_name) Application_OLD.__init__(self, application_name) self.args = args @@ -100,8 +100,8 @@ def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, floa rep_count = kwargs["rep_count"] - #create a hash value for identifying the problem configuration - #compare https://stackoverflow.com/questions/5884066/hashing-a-dictionary + # create a hash value for identifying the problem configuration + # compare https://stackoverflow.com/questions/5884066/hashing-a-dictionary problem_conf_hash = json.dumps(config, sort_keys=True) if self.problem_conf_hash != problem_conf_hash: @@ -207,7 +207,7 @@ def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, flo return processed_solution, postprocessing_time -def recursive_replace_dict_keys(obj: any)-> any: +def recursive_replace_dict_keys(obj: any) -> any: """ Replace values used as dictionary keys by their string representation to make the object JSON-compatible. @@ -298,7 +298,7 @@ def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, flo :return: Output and time needed """ run_kwargs = { - "store_dir": kwargs["store_dir"], + "store_dir": kwargs["store_dir"], "repetition": kwargs["rep_count"] } raw_solution, runtime, additional_solver_information = self.run( diff --git a/src/quark2_adapter/legacy_classes/Application.py b/src/quark2_adapter/legacy_classes/Application.py index f151fac7..f3aa8a19 100644 --- a/src/quark2_adapter/legacy_classes/Application.py +++ b/src/quark2_adapter/legacy_classes/Application.py @@ -93,7 +93,7 @@ def regenerate_on_iteration(self, config: dict) -> bool: return False @final - def init_problem(self, config: dict, conf_idx: int, iter_count: int, path: str) ->any: + def init_problem(self, config: dict, conf_idx: int, iter_count: int, path: str) -> any: """ This method is called on every iteration and calls generate_problem if necessary. conf_idx identifies the application configuration. diff --git a/src/quark2_adapter/legacy_classes/Solver.py b/src/quark2_adapter/legacy_classes/Solver.py index d12da120..07ad5ee2 100644 --- a/src/quark2_adapter/legacy_classes/Solver.py +++ b/src/quark2_adapter/legacy_classes/Solver.py @@ -31,7 +31,7 @@ def __init__(self): super().__init__() @abstractmethod - def run(self, mapped_problem: any, device: any , config: dict, **kwargs) -> tuple[any, float, dict]: + def run(self, mapped_problem: any, device: any, config: dict, **kwargs) -> tuple[any, float, dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. diff --git a/src/utils.py b/src/utils.py index d45993cc..407b2f9a 100644 --- a/src/utils.py +++ b/src/utils.py @@ -162,8 +162,9 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: _expand_paths(entry, base_dir) else: for attr in j: - if type(j[attr]) == "submodules": - _expand_paths(j[attr], base_dir) + + +if isinstance(j[attr], if ) _expand_paths(j[attr], base_dir) elif attr == "dir": p = j[attr] if not os.path.isabs(p): @@ -199,7 +200,7 @@ def stop_watch(position: int = None) -> Callable: def run(input_data,...): return processed_data ``` - results in valid: + results in valid: ``` processed_data, time_to_process = run(input,...) ``` From e334c51f80c99b95aa46b62cf1340edf1d58bf76 Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 20:28:40 +0200 Subject: [PATCH 31/40] removed lint issue --- .github/workflows/lint.yml | 2 +- src/utils.py | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ada1dfe3..1d612ec9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,7 +36,7 @@ jobs: run: git config lfs.https://github.com/QUARK-framework/QUARK.git/info/lfs.locksverify false - name: Run autopep8 (fix PEP8 issues automatically) - run: autopep8 --in-place --recursive --aggressive --aggressive --max-line-length 120 -v . + run: autopep8 --in-place --recursive --aggressive --max-line-length 120 -v . - name: Commit changes run: | diff --git a/src/utils.py b/src/utils.py index 407b2f9a..6218b651 100644 --- a/src/utils.py +++ b/src/utils.py @@ -38,13 +38,15 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: clazz = _import_class(opt["module"], class_name, opt.get("dir")) sub_options = opt.get("submodules", None) - # In case the class requires some arguments in its constructor they can be defined in the "args" dict + # In case the class requires some arguments in its constructor + # they can be defined in the "args" dict if "args" in opt and opt["args"]: instance = clazz(**opt["args"]) else: instance = clazz() - # _get_instance_with_sub_options is mostly called when using the --modules option, so it makes sense to also + # _get_instance_with_sub_options is mostly called when using the --modules option, + # so it makes sense to also # save the git revision of the given module, since it can be in a different git # Directory of this file @@ -108,7 +110,8 @@ def checkbox(key: str, message: str, choices: list) -> dict: answer = inquirer.prompt([inquirer.Checkbox(key, message=message, choices=choices)]) else: if len(choices) == 1: - logging.info(f"Skipping asking for submodule, since only 1 option ({choices[0]}) is available.") + logging.info(f"Skipping asking for submodule" + "since only 1 option ({choices[0]}) is available.") return {key: choices} if not answer[key]: @@ -120,17 +123,17 @@ def checkbox(key: str, message: str, choices: list) -> dict: def get_git_revision(git_dir: str) -> tuple[str, str]: """ - Collects git revision number and checks if there are uncommitted changes to allow user to analyze which - codebase was used. + Collects git revision number and checks if there are uncommitted changes + to allow user to analyze which codebase was used. :param git_dir: Directory of the git repository :return: Tuple with git_revision_number, git_uncommitted_changes """ try: - # '-C', git_dir ensures that the following commands also work when QUARK is started from other working - # directories - git_revision_number = subprocess.check_output(['git', '-C', git_dir, 'rev-parse', 'HEAD']).decode( - 'ascii').strip() + # '-C', git_dir ensures that the following commands also work + # when QUARK is started from other working directories + git_revision_number = subprocess.check_output( + ['git', '-C', git_dir, 'rev-parse', 'HEAD']).decode('ascii').strip() git_uncommitted_changes = bool(subprocess.check_output( ['git', '-C', git_dir, 'status', '--porcelain', '--untracked-files=no']).decode( 'ascii').strip()) @@ -162,9 +165,8 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: _expand_paths(entry, base_dir) else: for attr in j: - - -if isinstance(j[attr], if ) _expand_paths(j[attr], base_dir) + if type(j[attr]) == "submodules": + _expand_paths(j[attr], base_dir) elif attr == "dir": p = j[attr] if not os.path.isabs(p): @@ -200,7 +202,7 @@ def stop_watch(position: int = None) -> Callable: def run(input_data,...): return processed_data ``` - results in valid: + results in valid: ``` processed_data, time_to_process = run(input,...) ``` @@ -209,7 +211,7 @@ def run(input_data,...): measured time is to be inserted in the return tuple. :param position: The position at which the measured time gets inserted in the return tuple. - If not specified the measured time will be appended to the original return value. + If not specified the measured time will be appended to the original return value. :return: The wrapper function """ def wrap(func): From 95ee518b1bb683e2b89447648e9be735de0e2f7a Mon Sep 17 00:00:00 2001 From: Github Action Date: Mon, 14 Oct 2024 18:29:21 +0000 Subject: [PATCH 32/40] Apply autopep8 formatting --- src/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils.py b/src/utils.py index 6218b651..29017a32 100644 --- a/src/utils.py +++ b/src/utils.py @@ -165,8 +165,9 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: _expand_paths(entry, base_dir) else: for attr in j: - if type(j[attr]) == "submodules": - _expand_paths(j[attr], base_dir) + + +if isinstance(j[attr], if ) _expand_paths(j[attr], base_dir) elif attr == "dir": p = j[attr] if not os.path.isabs(p): @@ -202,7 +203,7 @@ def stop_watch(position: int = None) -> Callable: def run(input_data,...): return processed_data ``` - results in valid: + results in valid: ``` processed_data, time_to_process = run(input,...) ``` From 89c6eb7b8004936b320cc1954be6069c3a82da12 Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 20:31:51 +0200 Subject: [PATCH 33/40] removed lint issue --- src/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils.py b/src/utils.py index 29017a32..59036a95 100644 --- a/src/utils.py +++ b/src/utils.py @@ -150,6 +150,7 @@ def get_git_revision(git_dir: str) -> tuple[str, str]: return git_revision_number, git_uncommitted_changes +#autopep8: off def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: """ Expands the paths given as value of the 'dir' attribute appearing in the QUARK modules @@ -165,9 +166,8 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: _expand_paths(entry, base_dir) else: for attr in j: - - -if isinstance(j[attr], if ) _expand_paths(j[attr], base_dir) + if type(j[attr]) == "submodules": + _expand_paths(j[attr], base_dir) elif attr == "dir": p = j[attr] if not os.path.isabs(p): @@ -203,7 +203,7 @@ def stop_watch(position: int = None) -> Callable: def run(input_data,...): return processed_data ``` - results in valid: + results in valid: ``` processed_data, time_to_process = run(input,...) ``` From e7acde59030430663fbca337e9819fb8ca344e7c Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 21:24:04 +0200 Subject: [PATCH 34/40] removed lint issue --- .github/workflows/lint.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1d612ec9..8575fcc5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,14 +37,18 @@ jobs: - name: Run autopep8 (fix PEP8 issues automatically) run: autopep8 --in-place --recursive --aggressive --max-line-length 120 -v . - - - name: Commit changes + + - name: Commit changes if any run: | - git config --global user.name "Github Action" + git config --global user.name "GitHub Action" git config --global user.email "action@github.com" git add . - git commit -m "Apply autopep8 formatting" - git push origin HEAD:${{ github.ref }} + if git diff-index --quiet HEAD; then + echo "No changes to commit" + else + git commit -m "Apply autopep8 formatting" + git push origin HEAD:${{ github.ref }} + fi - name: Run pylint uses: wearerequired/lint-action@v2 From fe93088ee2eedc47821fabcd1a630d5005ec867b Mon Sep 17 00:00:00 2001 From: q666911 Date: Mon, 14 Oct 2024 21:31:53 +0200 Subject: [PATCH 35/40] removed lint issue --- src/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.py b/src/utils.py index 59036a95..6058c799 100644 --- a/src/utils.py +++ b/src/utils.py @@ -111,7 +111,7 @@ def checkbox(key: str, message: str, choices: list) -> dict: else: if len(choices) == 1: logging.info(f"Skipping asking for submodule" - "since only 1 option ({choices[0]}) is available.") + f"since only 1 option ({choices[0]}) is available.") return {key: choices} if not answer[key]: From 2220c0d4262ab908558d2650856cadaab68cce50 Mon Sep 17 00:00:00 2001 From: "Marvin Erdmann (FG-231)" Date: Tue, 15 Oct 2024 09:56:11 +0200 Subject: [PATCH 36/40] Make intentional PEP8-violating changes to test automatic corrections. --- src/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.py b/src/main.py index d08b23cd..7d88af2f 100644 --- a/src/main.py +++ b/src/main.py @@ -102,6 +102,11 @@ def start_benchmark_run(config_file: str = None, store_dir: str = None, config_manager = ConfigManager() config_manager.set_config(benchmark_config) + TestVariable = 1 + logging.info(f"TestVariable (={TestVariable}) should be changed to testvariable in main.py. Check and delete!") + logging.info("Blank line should be removed and ") + + benchmark_manager = BenchmarkManager(fail_fast=fail_fast) # Can be overridden by using the -m|--modules option From 7d26d4240327d16a592c6a5869cc466f0ab590a5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 15 Oct 2024 07:57:12 +0000 Subject: [PATCH 37/40] Apply autopep8 formatting --- src/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.py b/src/main.py index 7d88af2f..635e4e74 100644 --- a/src/main.py +++ b/src/main.py @@ -106,7 +106,6 @@ def start_benchmark_run(config_file: str = None, store_dir: str = None, logging.info(f"TestVariable (={TestVariable}) should be changed to testvariable in main.py. Check and delete!") logging.info("Blank line should be removed and ") - benchmark_manager = BenchmarkManager(fail_fast=fail_fast) # Can be overridden by using the -m|--modules option From 02b30b78cbbd2deb9bf720182edbed8bc7b95ca1 Mon Sep 17 00:00:00 2001 From: "Marvin Erdmann (FG-231)" Date: Tue, 15 Oct 2024 10:00:22 +0200 Subject: [PATCH 38/40] Delete intentional PEP8-violating changes that were not caught by autocorrections. --- src/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main.py b/src/main.py index 635e4e74..d08b23cd 100644 --- a/src/main.py +++ b/src/main.py @@ -102,10 +102,6 @@ def start_benchmark_run(config_file: str = None, store_dir: str = None, config_manager = ConfigManager() config_manager.set_config(benchmark_config) - TestVariable = 1 - logging.info(f"TestVariable (={TestVariable}) should be changed to testvariable in main.py. Check and delete!") - logging.info("Blank line should be removed and ") - benchmark_manager = BenchmarkManager(fail_fast=fail_fast) # Can be overridden by using the -m|--modules option From 8052ed16a5938ac49502e998f55ab6a9e96b8597 Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 15 Oct 2024 12:29:31 +0200 Subject: [PATCH 39/40] modified developer guide code examples according with pep8 standard --- docs/developer.rst | 94 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/docs/developer.rst b/docs/developer.rst index f493499d..58e6e3e4 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -85,33 +85,43 @@ Example for an application, which should reside under ``src/modules/applications .. code-block:: python - from modules.applications.Application import * + from modules.applications.Application import Application, Core from utils import start_time_measurement, end_time_measurement - - + from typing import TypedDict class MyApplication(Application): + """ + MyApplication is an example of how to create a new application module in the Quark framework. + """ def __init__(self): + """ + Initializes the MyApplication class. + """ super().__init__("MyApplication") self.submodule_options = ["submodule1"] @staticmethod def get_requirements() -> list: + """ + Returns a list of requirements for the application. + + :returns: A list of dictionaries containing the name and version of required packages + """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: + """ + Given an option string by the user, this returns a submodule. + :param option: String with the chosen submodule + :return: Module of type Core + :raises NotImplementedError: If the option is not recognized + """ if option == "submodule1": return Submodule1() @@ -119,7 +129,9 @@ Example for an application, which should reside under ``src/modules/applications raise NotImplementedError(f"Submodule Option {option} not implemented") def get_parameter_options(self): - + """ + Returns the parameter options for the application. + """ return { "size": { "values": [3, 4, 6, 8, 10, 14, 16], @@ -136,19 +148,34 @@ Example for an application, which should reside under ``src/modules/applications } class Config(TypedDict): + """ + A configuration dictionary for the application. + """ size: int factor: float - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: + """ + Generate data that gets passed to the next submodule. - # Generate data that gets passed to the next submodule + :param input_data: The input data for preprocessing + :param config: The configuration dictionary + :param **kwargs: Additional keyword arguments + :return: A tuple containing the preprocessed output and the time taken for preprocessing + """ start = start_time_measurement() output = self.generate_problem(config) return output, end_time_measurement(start) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: + """ + Processes data passed to this module from the submodule. - # Process data passed to this module from the submodule + :param input_data: The input data for postprocessing + :param config: The configuration dictionary + :param **kwargs: Additional keyword arguments + :returns: A tuple containing the processed solution quality and the time taken for evaluation + """ solution_validity, time_to_validation = self.validate( input_data) if solution_validity and processed_solution: @@ -162,16 +189,26 @@ Example for an application, which should reside under ``src/modules/applications return solution_validity, sum(time_to_validation, time_to_evaluation)) + def generate_problem(self, config: Config, iter_count: int) -> any: + """ + Generates a problem based on the given configuration. - - def generate_problem(self, config: Config, iter_count: int): - + :param config: The configuration dictionary + :param iter_count: The iteration count + :returns: The generated problem. + """ size = config['size'] self.application = create_problem(size) return self.application - def validate(self, solution): + def validate(self, solution) -> tuple[bool, float]: + """ + Validates the solution. + + :param solution: The solution to validate + :return: A tuple containing the validity of the solution and the time taken for validation + """ start = start_time_measurement() # Check if solution is valid @@ -182,14 +219,27 @@ Example for an application, which should reside under ``src/modules/applications logging.info(f"Solution valid") return True, end_time_measurement(start) - def evaluate(self, solution): + def evaluate(self, solution) -> tuple[float, float]: + """ + Evaluates the solution. + + :param solution: The solution to evaluate + :return: A tuple containing the evaluation metric and the time taken for evaluation. + """ start = start_time_measurement() evaluation_metric = calculate_metric(solution) return evaluation_metric, end_time_measurement(start) - def save(self, path, iter_count): + def save(self, path, iter_count) -> None: + """ + Saves the application state. + + :param path: The path where the application state should be saved. + :param iter_count: The iteration count. + :returns:None + """ save_your_application(self.application, f"{path}/application.txt") Writing an asynchronous Module From 447eedfe7413eb6ffca0544f5bb16d4bdb95525c Mon Sep 17 00:00:00 2001 From: q666911 Date: Tue, 15 Oct 2024 12:35:56 +0200 Subject: [PATCH 40/40] modified developer guide code examples according with pep8 standard --- docs/developer.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/developer.rst b/docs/developer.rst index 58e6e3e4..9de0a112 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -195,7 +195,7 @@ Example for an application, which should reside under ``src/modules/applications :param config: The configuration dictionary :param iter_count: The iteration count - :returns: The generated problem. + :returns: The generated problem """ size = config['size'] @@ -224,7 +224,7 @@ Example for an application, which should reside under ``src/modules/applications Evaluates the solution. :param solution: The solution to evaluate - :return: A tuple containing the evaluation metric and the time taken for evaluation. + :return: A tuple containing the evaluation metric and the time taken for evaluation """ start = start_time_measurement() @@ -236,8 +236,8 @@ Example for an application, which should reside under ``src/modules/applications """ Saves the application state. - :param path: The path where the application state should be saved. - :param iter_count: The iteration count. + :param path: The path where the application state should be saved + :param iter_count: The iteration count :returns:None """ save_your_application(self.application, f"{path}/application.txt")