diff --git a/.settings/module_db.json b/.settings/module_db.json index 2fc22e51..18ae1413 100644 --- a/.settings/module_db.json +++ b/.settings/module_db.json @@ -1,7 +1,7 @@ { - "build_number": 7, - "build_date": "20-03-2024 12:51:58", - "git_revision_number": "3be6f3847150d4bb8debee2451522b0b19fa205f", + "build_number": 9, + "build_date": "21-05-2024 10:16:52", + "git_revision_number": "16d427e5b1954cf0855dcea6c8aa5b13ee98a1f9", "modules": [ { "name": "PVC", @@ -1832,7 +1832,7 @@ "requirements": [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" }, { "name": "qiskit-optimization", @@ -1863,16 +1863,6 @@ "module": "modules.devices.HelperClass", "requirements": [], "submodules": [] - }, - { - "name": "ibm_eagle", - "class": "HelperClass", - "args": { - "device_name": "ibm_eagle" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] } ] } @@ -2049,11 +2039,143 @@ "requirements": [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" }, { "name": "numpy", "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" } ], "submodules": [ @@ -2133,11 +2255,143 @@ "requirements": [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" }, { "name": "numpy", "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" } ], "submodules": [ @@ -2198,11 +2452,143 @@ "requirements": [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" }, { "name": "numpy", "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" } ], "submodules": [ @@ -2279,11 +2665,15 @@ "requirements": [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" }, { "name": "numpy", "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" } ], "submodules": [ @@ -2326,6 +2716,183 @@ "submodules": [] } ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "0.45.0" + }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "cma", + "version": "3.3.0" + }, + { + "name": "tensorboard", + "version": "2.13.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.23.5" + } + ], + "submodules": [] + } + ] + } + ] + } + ] + } + ], + "requirements": [] + }, + { + "name": "MIS", + "class": "MIS", + "module": "modules.applications.optimization.MIS.MIS", + "submodules": [ + { + "name": "NeutralAtom", + "class": "NeutralAtom", + "args": {}, + "module": "modules.applications.optimization.MIS.mappings.NeutralAtom", + "requirements": [ + { + "name": "pulser", + "version": "0.16.0" + } + ], + "submodules": [ + { + "name": "NeutralAtomMIS", + "class": "NeutralAtomMIS", + "args": {}, + "module": "modules.solvers.NeutralAtomMIS", + "requirements": [ + { + "name": "pulser", + "version": "0.16.0" + } + ], + "submodules": [ + { + "name": "MockNeutralAtomDevice", + "class": "MockNeutralAtomDevice", + "args": {}, + "module": "modules.devices.pulser.MockNeutralAtomDevice", + "requirements": [ + { + "name": "pulser", + "version": "0.16.0" + } + ], + "submodules": [] } ] } diff --git a/.settings/requirements_full.txt b/.settings/requirements_full.txt index 4168d82b..fb1ea82c 100644 --- a/.settings/requirements_full.txt +++ b/.settings/requirements_full.txt @@ -23,8 +23,10 @@ more-itertools==9.0.0 qiskit-optimization==0.5.0 pyqubo==1.4.0 dwave_networkx==0.8.13 -qiskit==0.40.0 +qiskit==0.45.0 pandas==1.5.2 cma==3.3.0 tensorboard==2.13.0 tensorboardX==2.6.2 +pulser==0.16.0 +qiskit-aer==0.11.2 diff --git a/src/Installer.py b/src/Installer.py index b41f30e9..eb2ccbd6 100644 --- a/src/Installer.py +++ b/src/Installer.py @@ -44,7 +44,8 @@ def __init__(self): {"name": "SAT", "class": "SAT", "module": "modules.applications.optimization.SAT.SAT"}, {"name": "TSP", "class": "TSP", "module": "modules.applications.optimization.TSP.TSP"}, {"name": "GenerativeModeling", "class": "GenerativeModeling", - "module": "modules.applications.QML.generative_modeling.GenerativeModeling"} + "module": "modules.applications.QML.generative_modeling.GenerativeModeling"}, + {"name": "MIS", "class": "MIS", "module": "modules.applications.optimization.MIS.MIS"}, ] self.core_requirements = [ diff --git a/src/main.py b/src/main.py index de5445a4..74098203 100644 --- a/src/main.py +++ b/src/main.py @@ -21,7 +21,6 @@ import yaml from Installer import Installer -from Plotter import Plotter from utils import _expand_paths from utils_mpi import MPIStreamHandler, MPIFileHandler, get_comm @@ -157,6 +156,8 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: :rtype: None """ from BenchmarkManager import BenchmarkManager # pylint: disable=C0415 + from Plotter import Plotter # pylint: disable=C0415 + benchmark_manager = BenchmarkManager(fail_fast=args.failfast) if args.summarize: 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 15492edd..b46b1603 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,6 +14,7 @@ import pickle import os +from qiskit import qpy import numpy as np import pandas as pd @@ -25,7 +26,7 @@ class DataHandler(Core, ABC): """ - The task of the DataHandler module is to translate the application’s data + The task of the DataHandler module is to translate the application’s data and problem specification into preproccesed format. """ @@ -150,6 +151,10 @@ def postprocess(self, input_data: dict, config: dict, **kwargs): with open(f"{store_dir_iter}/training_results-{kwargs['rep_count']}.pkl", 'wb') as f: pickle.dump(input_data, f) + if "circuit_transpiled" in list(input_data.keys()): + with open(f"{store_dir_iter}/transpiled_circuit_{kwargs['rep_count']}.qpy", 'wb') as f: + qpy.dump(input_data.pop("circuit_transpiled"), f) + # Save variables transformed space if "Transformation" in list(input_data.keys()): self.metrics.add_metric_batch({"KL_best": input_data["KL_best_transformed"]}) @@ -164,7 +169,7 @@ def postprocess(self, input_data: dict, config: dict, **kwargs): @abstractmethod def data_load(self, gen_mod: 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 gen_mod: dictionary with collected information of the previous modules @@ -207,12 +212,12 @@ def evaluate(self, solution: any) -> (dict, float): @staticmethod def tb_to_pd(logdir: str, rep: str) -> None: """ - Converts TensorBoard event files in the specified log directory + 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 - :type logdir: str - + :type logdir: str + """ event_acc = EventAccumulator(logdir) event_acc.Reload() diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py new file mode 100644 index 00000000..c16a10d7 --- /dev/null +++ b/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -0,0 +1,398 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.compiler import transpile, assemble +from qiskit.transpiler import CouplingMap +from qiskit.providers import Backend +from qiskit import Aer +from qiskit_aer import AerSimulator +from qiskit_aer import noise +from qiskit_aer.noise import NoiseModel +# from qiskit_ibm_runtime import QiskitRuntimeService +import numpy as np + +from modules.training.QCBM import QCBM +from modules.training.Inference import Inference +from modules.applications.QML.generative_modeling.mappings.Library import Library + +logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) + + +def split_string(s): + return s.split(' ', 1)[0] + + +class CustomQiskitNoisyBackend(Library): + """ + This module maps a library-agnostic gate sequence to a qiskit circuit + """ + + def __init__(self): + """ + Constructor method + """ + super().__init__("NoisyQiskit") + self.submodule_options = ["QCBM", "Inference"] + + circuit_transpiled = None + + @staticmethod + 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": "0.45.0" + }, + # { + # "name": "qiskit_ibm_runtime", + # "version": "0.10.0" + # }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ] + + 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)" + } + } + + """ + value_list = [] + value_list.append('Custom configurations') + value_list.append('No noise') + 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 Change 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 the aer_statevector_simulator selected, 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": value_list, + "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 " + } + } + + def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: + + if option == "QCBM": + return QCBM() + elif option == "Inference": + return Inference() + else: + raise NotImplementedError(f"Option {option} not implemented") + + 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"] + circuit = QuantumCircuit(n_qubits, n_qubits) + param_counter = 0 + for gate, wires in gate_sequence: + if gate == "Hadamard": + circuit.h(wires[0]) + elif gate == "X": + circuit.x(wires[0]) + elif gate == "SX": + circuit.sx(wires[0]) + elif gate == "RZ_PI/2": + circuit.rz(np.pi / 2, wires[0]) + elif gate == "CNOT": + circuit.cnot(wires[0], wires[1]) + elif gate == "ECR": + circuit.ecr(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") + + input_data["circuit"] = circuit + input_data.pop("gate_sequence") + logging.info(param_counter) + return input_data + + @staticmethod + def select_backend(config: str) -> dict: + """ + This method configures the backend + + :param config: Name of a backend + :type config: str + :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, config: str, config_dict: dict) \ + -> callable: # pylint: disable=W0221 + """ + 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: Method that executes the quantum circuit for a given set of parameters + :rtype: callable + """ + n_shots = config_dict["n_shots"] + n_qubits = circuit.num_qubits + start = perf_counter() + + 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 + logging.info(f'Using {optimization_level=} with seed: {seed_transp}') + 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(perf_counter() - start) + + + if config in ["aer_simulator_cpu", "aer_simulator_gpu"]: + def execute_circuit(solutions): + + all_circuits = [circuit_transpiled.bind_parameters(solution) for solution in solutions] + qobjs = assemble(all_circuits, backend=backend) + jobs = backend.run(qobjs, shots=n_shots) + samples_dictionary = [jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits] + samples = [] + for result in samples_dictionary: + target_iter = np.zeros(2 ** n_qubits) + result_keys = list(result.keys()) + result_vals = list(result.values()) + target_iter[result_keys] = result_vals + target_iter = np.asarray(target_iter) + samples.append(target_iter) + samples = np.asarray(samples) + pmfs = samples / n_shots + + return pmfs, samples + + return execute_circuit, circuit_transpiled + + + @staticmethod + def split_string(s): + return s.split(' ', 1)[0] + + def decompile_noisy_config(self, config_dict, num_qubits): + 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']) + + backend = self.get_custom_config(config_dict, num_qubits) \ + 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) + + return backend + + def get_simulation_method_and_device(self, device, simulation_config): + simulation_method = { + "statevector": "statevector", + "density_matrix": "density_matrix", + "cpu_mps": "matrix_product_state" + }.get(simulation_config, 'automatic') + + if simulation_config == "cpu_mps": + device = 'CPU' + + return simulation_method, device + + def get_transpile_routine(self, transpile_config): + return transpile_config if transpile_config in [0, 1, 2, 3] else 1 + + def get_custom_config(self, config_dict, num_qubits): + noise_model = self.build_noise_model(config_dict) + coupling_map = self.get_coupling_map(config_dict, num_qubits) + backend = AerSimulator(noise_model=noise_model, + coupling_map=coupling_map) if coupling_map is not None else AerSimulator( + noise_model=noise_model) + return backend + + def build_noise_model(self, config_dict): + noise_model = 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]]) + + self.add_quantum_errors(noise_model, config_dict) + return noise_model + + def add_quantum_errors(self, noise_model, config_dict): + if config_dict['two_qubit_depolarizing_errors'] is not None: + two_qubit_error = noise.depolarizing_error(config_dict['two_qubit_depolarizing_errors'], 2) + for gate in ['cx', 'ecr', 'rxx']: + noise_model.add_all_qubit_quantum_error(two_qubit_error, gate) + + if config_dict['one_qubit_depolarizing_errors'] is not None: + one_qubit_error = noise.depolarizing_error(config_dict['one_qubit_depolarizing_errors'], 1) + for gate in ['sx', 'x', 'rx', 'ry', 'rz', 'h', 's']: + noise_model.add_all_qubit_quantum_error(one_qubit_error, gate) + + def get_coupling_map(self, config_dict, num_qubits): + layout = config_dict['qubit_layout'] + if layout == 'linear': + return CouplingMap.from_line(num_qubits) + elif layout == 'circle': + return CouplingMap.from_ring(num_qubits) + elif layout == 'fully_connected': + return CouplingMap.from_full(num_qubits) + # 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 + else: + raise ValueError(f"Unknown qubit layout: {layout}") + + def log_backend_options(self, backend): + logging.info(f'Backend configuration: {backend.configuration()}') + logging.info(f'Simulation method: {backend.options.method}') diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/QML/generative_modeling/mappings/Library.py index 35b23e9a..14bf3f2f 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/QML/generative_modeling/mappings/Library.py @@ -61,16 +61,15 @@ def preprocess(self, input_data: dict, config: Config, **kwargs): :return: Dictionary including the function to execute the quantum cicrcuit on a simulator or on quantum hardware :rtype: (dict, float) """ - start = start_time_measurement() output = self.sequence_to_circuit(input_data) backend = self.select_backend(config["backend"]) - output["execute_circuit"] = self.get_execute_circuit( + output["execute_circuit"], output['circuit_transpiled'] = self.get_execute_circuit( output["circuit"], backend, config["backend"], - config["n_shots"]) + config_dict=config) output["backend"] = config["backend"] output["n_shots"] = config["n_shots"] logging.info("Library created") @@ -101,7 +100,7 @@ def sequence_to_circuit(self, input_data): @staticmethod @abstractmethod - def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, n_shots: int): + def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict): pass @staticmethod diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py index c10fea18..8149496d 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py +++ b/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py @@ -14,7 +14,7 @@ from typing import Union import logging - +# from qiskit_ibm_runtime import QiskitRuntimeService, Sampler from qiskit import QuantumCircuit from qiskit.circuit import Parameter from qiskit.compiler import transpile, assemble @@ -51,12 +51,20 @@ def get_requirements() -> list[dict]: return [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" }, + # { + # "name": "qiskit_ibm_runtime", + # "version": "0.10.0" + # }, { "name": "numpy", "version": "1.23.5" }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } ] def get_parameter_options(self) -> dict: @@ -67,32 +75,32 @@ def get_parameter_options(self) -> dict: .. 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)" - } - } + "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", + "simulator_statevector IBM Quantum Platform", "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)" + }, + + "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 { "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"], + "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)" }, + #TODO Discuss: Use IBM Eagle (so 1 of 3) or IBM Brisbane so one specific? "n_shots": { "values": [100, 1000, 10000, 1000000], @@ -217,6 +225,18 @@ def select_backend(config: str) -> dict: backend = Aer.get_backend('statevector_simulator') backend.set_options(device="CPU") + # elif config == "simulator_statevector IBM Quantum Platform": + # define token here once + # service = QiskitRuntimeService(token='TOKEN') + # service = QiskitRuntimeService() + # backend = service.get_backend('simulator_statevector') + + # elif config == "ibm_brisbane IBM Quantum Platform": + #define token here once + #service = QiskitRuntimeService(token='TOKEN') + # service = QiskitRuntimeService() + # backend = service.get_backend('ibm_brisbane') + elif config == "ionQ_Harmony": from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 @@ -249,22 +269,24 @@ def select_backend(config: str) -> dict: return backend @staticmethod - def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, n_shots: int) -> callable: + def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict) \ + -> callable: # pylint: disable=W0221,R0915 """ - This method combnines the qiskit circuit implemenation and the selected backend and returns a function, - that will be called during training. + 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 + :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 n_shots: The number of times the circuit is run - :type n_shots: int + :param config_dict: Contains information about config + :type config_dict: dict :return: Method that executes the quantum circuit for a given set of parameters :rtype: callable """ + n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits circuit_transpiled = transpile(circuit, backend=backend) @@ -324,4 +346,27 @@ def execute_circuit(solutions): pmfs = samples / n_shots return pmfs, samples - return execute_circuit + elif config in ["simulator_statevector IBM Quantum Platform", "ibm_brisbane IBM Quantum Platform"]: + def execute_circuit(solutions, *kwargs): + + """ + This function will submit the circuits with different parameter individually. + If you want to deploy a circuit to ibm for inference, we recommend using a Jupyter Notebook. + """ + # all_circuits = [circuit_transpiled.bind_parameters(solution) for solution in solutions] + # service = QiskitRuntimeService() + samples_dictionary = [] + samples = [] + for result in samples_dictionary: + target_iter = np.zeros(2 ** n_qubits) + result_keys = list(result.keys()) + result_vals = np.abs(list(result.values())) + # To ensure no negative values from the quasi ditribution are taken. + target_iter[result_keys] = result_vals + target_iter = np.asarray(target_iter) + samples.append(target_iter) + samples = np.asarray(samples) + pmfs = samples + return pmfs, samples + + return execute_circuit, circuit_transpiled diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py new file mode 100644 index 00000000..d8f26b8e --- /dev/null +++ b/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -0,0 +1,380 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 + +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit.providers.fake_provider import FakeProviderForBackendV2 +from qiskit.providers.fake_provider import * +from qiskit.compiler import transpile, assemble +from qiskit.providers import Backend +from qiskit import Aer +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel +# from qiskit_ibm_runtime import QiskitRuntimeService +import numpy as np + +from modules.training.QCBM import QCBM +from modules.training.Inference import Inference +from modules.applications.QML.generative_modeling.mappings.Library import Library + +logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) + + +class PresetQiskitNoisyBackend(Library): + """ + This module maps a library-agnostic gate sequence to a qiskit circuit. + """ + + def __init__(self): + """ + Constructor method + """ + super().__init__("PresetQiskitNoisyBackend") + self.submodule_options = ["QCBM", "Inference"] + + circuit_transpiled = None + + @staticmethod + 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": "0.45.0" + }, + # { + # "name": "qiskit_ibm_runtime", + # "version": "0.10.0" + # }, + { + "name": "qiskit_aer", + "version": "0.11.2" + }, + { + "name": "numpy", + "version": "1.23.5" + }, + { + "name": "qiskit-ibmq-provider", + "version": "0.19.2" + } + ] + + 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)" + } + } + + """ + + 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') + + 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 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)" + }, + + "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?" + } + } + + def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: + + if option == "QCBM": + return QCBM() + elif option == "Inference": + return Inference() + else: + raise NotImplementedError(f"Option {option} not implemented") + + 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"] + circuit = QuantumCircuit(n_qubits, n_qubits) + param_counter = 0 + for gate, wires in gate_sequence: + if gate == "Hadamard": + circuit.h(wires[0]) + elif gate == "X": + circuit.x(wires[0]) + elif gate == "SX": + circuit.sx(wires[0]) + elif gate == "RZ_PI/2": + circuit.rz(np.pi / 2, wires[0]) + elif gate == "CNOT": + circuit.cnot(wires[0], wires[1]) + elif gate == "ECR": + circuit.ecr(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") + + input_data["circuit"] = circuit + input_data.pop("gate_sequence") + logging.info(param_counter) + return input_data + + @staticmethod + def select_backend(config: str) -> dict: + """ + This method configures the backend + + :param config: Name of a backend + :type config: str + :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, config: str, # pylint: disable=W0221 + config_dict: dict) -> callable: + """ + 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: Method that executes the quantum circuit for a given set of parameters + :rtype: callable + """ + n_shots = config_dict["n_shots"] + n_qubits = circuit.num_qubits + start = perf_counter() + + 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 + logging.info(f'Using {optimization_level=} with seed: {seed_transp}') + 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(perf_counter() - start) + + if config in ["aer_simulator_cpu", "aer_simulator_gpu"]: + def execute_circuit(solutions): + + all_circuits = [circuit_transpiled.bind_parameters(solution) for solution in solutions] + qobjs = assemble(all_circuits, backend=backend) + jobs = backend.run(qobjs, shots=n_shots) + samples_dictionary = [jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits] + samples = [] + for result in samples_dictionary: + target_iter = np.zeros(2 ** n_qubits) + result_keys = list(result.keys()) + result_vals = list(result.values()) + target_iter[result_keys] = result_vals + target_iter = np.asarray(target_iter) + samples.append(target_iter) + samples = np.asarray(samples) + pmfs = samples / n_shots + + return pmfs, samples + + return execute_circuit, circuit_transpiled + + @staticmethod + def split_string(s): + return s.split(' ', 1)[0] + + def decompile_noisy_config(self, config_dict, num_qubits): + 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']) + backend = self.select_backend_configuration(config_dict['noise_configuration'], num_qubits) + + self.configure_backend(backend, device, simulation_method) + self.log_backend_info(backend) + + return backend + + def select_backend_configuration(self, noise_configuration, num_qubits): + if "fake" in noise_configuration: + return self.get_FakeBackend(noise_configuration, num_qubits) + elif noise_configuration == "No noise": + return Aer.get_backend("aer_simulator") + 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}") + + # 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, device, simulation_method): + backend.set_options(device=device) + backend.set_options(method=simulation_method) + + def log_backend_info(self, backend): + logging.info(f'Backend configuration: {backend.configuration()}') + logging.info(f'Simulation method: {backend.options.method}') + + def get_simulation_method_and_device(self, device, simulation_config): + simulation_methods = { + "statevector": "statevector", + "density_matrix": "density_matrix", + "cpu_mps": "matrix_product_state" + } + simulation_method = simulation_methods.get(simulation_config, 'automatic') + if simulation_config == "cpu_mps": + device = 'CPU' + return simulation_method, device + + def get_transpile_routine(self, transpile_config): + return transpile_config if transpile_config in [0, 1, 2, 3] else 1 + + def get_FakeBackend(self, noise_configuration, num_qubits): + backend_name = str(self.split_string(noise_configuration)) + provider = FakeProviderForBackendV2() + try: + backend = provider.get_backend(backend_name) + except TypeError: + logging.info("qiskit.providers.fake_provider.FakeProviderForBackendV2.get_backend overwritten. " + "Will be addressed with upcoming qiskit upgrade.") + filtered_backends = [backend for backend in provider._backends if # pylint: disable=W0212 + backend.name == backend_name] + if not filtered_backends: + raise FileNotFoundError() # pylint: disable=W0707 + 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.') + return Aer.get_backend("aer_simulator") + + 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 34e3cd84..0ef1a14a 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/QML/generative_modeling/transformations/MinMax.py @@ -132,6 +132,7 @@ def reverse_transform(self, input_data: dict) -> (any, float): n_qubits = input_data["n_qubits"] n_registers = self.transform_config["n_registers"] KL_best_transformed = min(input_data["KL"]) + circuit_transpiled = input_data['circuit_transpiled'] array_bins = Transformation.compute_discretization_efficient(n_qubits, n_registers) transformed_samples = Transformation.generate_samples_efficient(best_results, array_bins, n_registers, @@ -170,7 +171,8 @@ def reverse_transform(self, input_data: dict) -> (any, float): "histogram_generated_original": histogram_generated_original, "histogram_generated": histogram_generated_transformed, "KL_best_transformed": KL_best_transformed, - "store_dir_iter": input_data["store_dir_iter"] + "store_dir_iter": input_data["store_dir_iter"], + "circuit_transpiled": circuit_transpiled } return reverse_config_trans diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/QML/generative_modeling/transformations/PIT.py index dee21acd..e1a08177 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/QML/generative_modeling/transformations/PIT.py @@ -132,6 +132,7 @@ def reverse_transform(self, input_data: dict) -> (any, float): n_registers = self.transform_config["n_registers"] KL_best_transformed = min(input_data["KL"]) best_results = input_data["best_sample"] + circuit_transpiled = input_data['circuit_transpiled'] array_bins = self.compute_discretization_efficient(n_qubits, n_registers) transformed_samples = self.generate_samples_efficient(best_results, array_bins, n_registers, noisy=True) @@ -169,7 +170,8 @@ def reverse_transform(self, input_data: dict) -> (any, float): "histogram_generated_original": histogram_generated_original, "histogram_generated": histogram_generated_transformed, "KL_best_transformed": KL_best_transformed, - "store_dir_iter": input_data["store_dir_iter"] + "store_dir_iter": input_data["store_dir_iter"], + "circuit_transpiled": circuit_transpiled } return reverse_config_trans diff --git a/src/modules/applications/optimization/MIS/MIS.py b/src/modules/applications/optimization/MIS/MIS.py new file mode 100644 index 00000000..dbdf90e8 --- /dev/null +++ b/src/modules/applications/optimization/MIS/MIS.py @@ -0,0 +1,245 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 TypedDict +import pickle + +import networkx + +from modules.applications.Application import * +from modules.applications.optimization.Optimization import Optimization +from modules.applications.optimization.MIS.data.graph_layouts import \ + generate_hexagonal_graph +from utils import start_time_measurement, end_time_measurement + +# define R_rydberg +R_rydberg = 9.75 + + +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. + """ + + def __init__(self): + """ + Constructor method + """ + super().__init__("MIS") + self.submodule_options = ["NeutralAtom"] + + @staticmethod + def get_requirements() -> list[dict]: + """ + Returns requirements of this module + + :return: list of dict with requirements of this module + :rtype: list[dict] + """ + return [ + ] + + def get_solution_quality_unit(self) -> str: + return "Set size" + + def get_default_submodule(self, option: str) -> Core: + 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: + """ + Returns the configurable settings for this application + + :return: + .. 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": [1, 5, 10, 15], + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "How large should your graph be?" + }, + "spacing": { + "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?" + }, + "filling_fraction": { + "values": [x/10 for x in range(2, 11, 2)], + "custom_input": True, + "allow_ranges": True, + "postproc": float, + "description": "What should the filling fraction be?" + }, + } + + class Config(TypedDict): + """ + Attributes of a valid config + + .. code-block:: python + + size: int + spacing: float + filling_fraction: float + + """ + size: int + spacing: float + filling_fraction: float + + def generate_problem(self, config: Config) -> networkx.Graph: + """ + Generates a graph to solve the MIS 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} + + # check if config has the necessary information + assert all( + x in config.keys() + for x in ['size', 'spacing', 'filling_fraction'] + ) + + size = config.get('size') + spacing = config.get('spacing') * R_rydberg + filling_fraction = config.get('filling_fraction') + + graph = generate_hexagonal_graph( + n_nodes=size, + spacing=spacing, + filling_fraction=filling_fraction, + ) + + 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}") + + self.application = graph + return graph.copy() + + def process_solution(self, solution: list) -> (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): + """ + 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 + + nodes = list(self.application.nodes()) + edges = list(self.application.edges()) + + # TODO: Check if the solution is maximal? + + # Check if the solution is independent + is_independent = all((u, v) not in edges for u, v in edges if u in solution and v in solution) + if is_independent: + logging.info("The solution is independent") + else: + logging.warning("The solution is not independent") + is_valid = False + + # Check if the solution is a set + solution_set = set(solution) + is_set = len(solution_set) == len(solution) + if is_set: + logging.info("The solution is a set") + else: + logging.warning("The solution is not a set") + 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: + logging.info("The solution is a subset of the problem") + else: + logging.warning("The solution is not a subset of the problem") + is_valid = False + + return is_valid, end_time_measurement(start) + + def evaluate(self, solution: list) -> (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) + + logging.info(f"Size of solution: {set_size}") + + return set_size, end_time_measurement(start) + + def save(self, path: str, iter_count: int) -> None: + 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 new file mode 100644 index 00000000..e808d010 --- /dev/null +++ b/src/modules/applications/optimization/MIS/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +"""Module for MIS""" diff --git a/src/modules/applications/optimization/MIS/data/__init__.py b/src/modules/applications/optimization/MIS/data/__init__.py new file mode 100644 index 00000000..310100cb --- /dev/null +++ b/src/modules/applications/optimization/MIS/data/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +"""Module for MIS data""" diff --git a/src/modules/applications/optimization/MIS/data/graph_layouts.py b/src/modules/applications/optimization/MIS/data/graph_layouts.py new file mode 100644 index 00000000..44695408 --- /dev/null +++ b/src/modules/applications/optimization/MIS/data/graph_layouts.py @@ -0,0 +1,122 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 math +import random + +import networkx +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: + """ + Generate a hexagonal graph layout based on the number of atoms and spacing. + + Args: + n (int): The number of nodes in the graph. + spacing (float): The spacing between atoms. + filling_fraction (float): The fraction of available places in the + lattice to be filled with atoms. (default: 1.0) + + Returns: + 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]." + ) + + # 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) + + # 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] + 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 + + # Create the graph + hexagonal_graph = networkx.Graph() + + # Add the nodes + for ID, coord in node_positions.items(): + hexagonal_graph.add_node(ID, pos=coord) + + # Generate the edges and add them to the graph + edges = _generate_edges(node_positions=node_positions) + hexagonal_graph.add_edges_from(edges) + + return hexagonal_graph + +def _generate_edges( + node_positions: dict, + 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 + value. + 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. + """ + 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]) + if distance <= radius: + edges.append((vertex_key, neighbor_key)) + return edges + +def _vertex_distance(v0: tuple, v1: tuple) -> float: + """ + Calculates distance between two n-dimensional vertices. + For 2 dimensions: distance = sqrt((x0-x1)**2 + (y0-y1)**2) + """ + squared_difference = 0 + for coordinate0, coordinate1 in zip(v0, v1): + squared_difference += (coordinate0 -coordinate1)**2 + 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 new file mode 100644 index 00000000..dc721743 --- /dev/null +++ b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py @@ -0,0 +1,101 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 TypedDict + +import networkx +import numpy as np +import pulser + +from modules.applications.Mapping import * +from utils import start_time_measurement, end_time_measurement + + +class NeutralAtom(Mapping): + """ + Neutral atom formulation for MIS. + """ + + def __init__(self): + """ + Constructor method + """ + super().__init__() + self.submodule_options = ["NeutralAtomMIS"] + + @staticmethod + def get_requirements() -> list[dict]: + """ + Return requirements of this module + + :return: list of dict with requirements of this module + :rtype: list[dict] + """ + return [ + { + "name": "pulser", + "version": "0.16.0" + } + ] + + def get_parameter_options(self) -> dict: + """ + Returns the configurable settings for this mapping + + :return: + .. code-block:: python + + return {} + + """ + return {} + + class Config(TypedDict): + """ + Attributes of a valid config + + .. code-block:: python + pass + """ + pass + + def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + """ + Maps the networkx graph to a neutral atom MIS problem. + + :param problem: networkx graph + :type problem: networkx.Graph + :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) + """ + start = start_time_measurement() + + pos = networkx.get_node_attributes(problem, 'pos') + register = pulser.Register(pos) + + neutral_atom_problem = { + 'graph': problem, + 'register': register + } + return neutral_atom_problem, end_time_measurement(start) + + def get_default_submodule(self, option: str) -> Core: + + if option == "NeutralAtomMIS": + from modules.solvers.NeutralAtomMIS import NeutralAtomMIS # pylint: disable=C0415 + return NeutralAtomMIS() + else: + raise NotImplementedError(f"Solver Option {option} not implemented") diff --git a/src/modules/applications/optimization/MIS/mappings/__init__.py b/src/modules/applications/optimization/MIS/mappings/__init__.py new file mode 100644 index 00000000..a701f7ff --- /dev/null +++ b/src/modules/applications/optimization/MIS/mappings/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +"""Module for MIS mappings""" diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index f9138a3e..7131739e 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -225,7 +225,7 @@ def process_solution(self, solution: dict) -> (list, bool): # 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)) + nodes_unassigned = list(np.random.permutation(nodes_unassigned, dtype=object)) logging.info(nodes_unassigned) logging.info(visited_seams) logging.info(nodes) diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/circuits/CircuitCardinality.py index a533adcf..c594c64a 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/circuits/CircuitCardinality.py @@ -16,6 +16,9 @@ from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend + class CircuitCardinality(Circuit): @@ -30,7 +33,7 @@ def __init__(self): Constructor method """ super().__init__("CircuitCardinality") - self.submodule_options = ["LibraryQiskit"] + self.submodule_options = ["LibraryQiskit", "CustomQiskitNoisyBackend", "PresetQiskitNoisyBackend"] def get_parameter_options(self) -> dict: """ @@ -58,6 +61,10 @@ def get_parameter_options(self) -> dict: def get_default_submodule(self, option: str) -> LibraryQiskit: if option == "LibraryQiskit": return LibraryQiskit() + elif option == "PresetQiskitNoisyBackend": + return PresetQiskitNoisyBackend() + elif option == "CustomQiskitNoisyBackend": + return CustomQiskitNoisyBackend() else: raise NotImplementedError(f"Option {option} not implemented") diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/circuits/CircuitCopula.py index c1f97225..847bbd5b 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/circuits/CircuitCopula.py @@ -19,6 +19,8 @@ from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend class CircuitCopula(Circuit): @@ -33,7 +35,7 @@ def __init__(self): Constructor method """ super().__init__("DiscreteCopula") - self.submodule_options = ["LibraryQiskit"] + self.submodule_options = ["LibraryQiskit", "CustomQiskitNoisyBackend", "PresetQiskitNoisyBackend"] @staticmethod def get_requirements() -> list[dict]: @@ -76,6 +78,10 @@ def get_parameter_options(self) -> dict: def get_default_submodule(self, option: str) -> LibraryQiskit: if option == "LibraryQiskit": return LibraryQiskit() + elif option == "PresetQiskitNoisyBackend": + return PresetQiskitNoisyBackend() + elif option == "CustomQiskitNoisyBackend": + return CustomQiskitNoisyBackend() else: raise NotImplementedError(f"Option {option} not implemented") diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/circuits/CircuitStandard.py index 86903fe1..e80349ef 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/circuits/CircuitStandard.py @@ -16,6 +16,8 @@ from modules.circuits.Circuit import Circuit from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend class CircuitStandard(Circuit): @@ -29,7 +31,7 @@ def __init__(self): Constructor method """ super().__init__("DiscreteStandard") - self.submodule_options = ["LibraryQiskit"] + self.submodule_options = ["LibraryQiskit", "CustomQiskitNoisyBackend", "PresetQiskitNoisyBackend"] @staticmethod def get_requirements() -> list[dict]: @@ -72,6 +74,10 @@ def get_parameter_options(self) -> dict: def get_default_submodule(self, option: str) -> LibraryQiskit: if option == "LibraryQiskit": return LibraryQiskit() + elif option == "PresetQiskitNoisyBackend": + return PresetQiskitNoisyBackend() + elif option == "CustomQiskitNoisyBackend": + return CustomQiskitNoisyBackend() else: raise NotImplementedError(f"Option {option} not implemented") diff --git a/src/modules/devices/Device.py b/src/modules/devices/Device.py index 014958f3..dcf71103 100644 --- a/src/modules/devices/Device.py +++ b/src/modules/devices/Device.py @@ -29,7 +29,6 @@ def __init__(self, device_name: str): self.device = None self.config = None self.device_name = self.name - self.config = None def get_parameter_options(self) -> dict: """ diff --git a/src/modules/devices/pulser/MockNeutralAtomDevice.py b/src/modules/devices/pulser/MockNeutralAtomDevice.py new file mode 100644 index 00000000..97d93cda --- /dev/null +++ b/src/modules/devices/pulser/MockNeutralAtomDevice.py @@ -0,0 +1,84 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 TypedDict + +import pulser +from pulser.devices import MockDevice +from pulser_simulation import QutipBackend + +from modules.devices.pulser.Pulser import Pulser +from modules.Core import Core + + +class MockNeutralAtomDevice(Pulser): + """ + Class for using the local mock Pulser simulator for neutral atom devices + """ + + def __init__(self): + """ + 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: + """ + Returns the configurable settings for this application + """ + return { + "doppler": { + "values": [False, True], + "description": "Simulate doppler noise? Has a large impact on performance!" + }, + "amplitude": { + "values": [False, True], + "description": "Simulate amplitude noise? Has a large impact on performance!" + }, + "SPAM": { + "values": [False, True], + "description": "Simulate SPAM noise? Has a large impact on performance!" + }, + "dephasing": { + "values": [False, True], + "description": "Simulate dephasing noise? Has a large impact on performance!" + }, + } + + class Config(TypedDict): + """ + Attributes of a valid config + """ + doppler: bool + amplitude: bool + SPAM: bool + dephasing: bool + + def get_backend_config(self) -> pulser.backend.config.EmulatorConfig: + """ + 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) + emulator_config = pulser.backend.config.EmulatorConfig(noise_model=noise_model) + return emulator_config + + def get_default_submodule(self, option: str) -> Core: + raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/pulser/Pulser.py b/src/modules/devices/pulser/Pulser.py new file mode 100644 index 00000000..ce1497e6 --- /dev/null +++ b/src/modules/devices/pulser/Pulser.py @@ -0,0 +1,65 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 modules.devices.Device import Device + + +class Pulser(Device, ABC): + """ + Abstract class to use the Pulser devices. + """ + + def __init__(self, device_name: str): + """ + Constructor method + """ + super().__init__(device_name) + self.device = None + self.backend = None + + def get_backend(self) -> any: + """ + Returns backend + + :return: Instance of the backend class + :rtype: any + """ + return self.backend + + @abstractmethod + def get_backend_config(self) -> any: + """ + Returns backend configurations + + :return: Instance of the backend config class + :rtype: any + """ + pass + + @staticmethod + def get_requirements() -> list[dict]: + """ + Return requirements of this module + + :return: list of dict with requirements of this module + :rtype: list[dict] + """ + return [ + { + "name": "pulser", + "version": "0.16.0" + }, + ] diff --git a/src/modules/devices/pulser/__init__.py b/src/modules/devices/pulser/__init__.py new file mode 100644 index 00000000..21f825c9 --- /dev/null +++ b/src/modules/devices/pulser/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +"""Pulser neutral atom devices""" diff --git a/src/modules/solvers/Annealer.py b/src/modules/solvers/Annealer.py index 34753ca6..dbaa226e 100644 --- a/src/modules/solvers/Annealer.py +++ b/src/modules/solvers/Annealer.py @@ -111,9 +111,10 @@ def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwarg # 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"]) - else: + # else: # This is for D-Wave simulated Annealer - response = device.sample_qubo(Q, num_reads=config['number_of_reads']) + # 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/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py new file mode 100644 index 00000000..0ecc3073 --- /dev/null +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -0,0 +1,206 @@ +# Copyright 2021 The QUARK Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 TypedDict + +import numpy as np +import pulser + +from modules.solvers.Solver import * +from utils import start_time_measurement, end_time_measurement + + +class NeutralAtomMIS(Solver): + """ + Neutral atom quantum computer maximum independent sets solver. + """ + + def __init__(self): + """ + Constructor method + """ + super().__init__() + self.submodule_options = ["MockNeutralAtomDevice"] + + @staticmethod + def get_requirements() -> list[dict]: + """ + Return requirements of this module + + :return: list of dict with requirements of this module + :rtype: list[dict] + """ + return [ + { + "name": "pulser", + "version": "0.16.0" + } + ] + + def get_default_submodule(self, option: str) -> Core: + 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: + """ + Returns the configurable settings for this solver + """ + return { + "samples": { + "values": [10, 100, 1000, 10000], + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "How many samples from the quantum computer do you want per measurement?" + }, + } + + class Config(TypedDict): + """ + 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): + """ + 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 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 + :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." + ) + + device = device_wrapper.get_device() + device.validate_register(register) + device_backend = device_wrapper.get_backend() + device_config = device_wrapper.get_backend_config() + + start = start_time_measurement() + + sequence = self._create_sequence(register, device) + + if device_wrapper.device_name == "mock neutral atom device": + backend = device_backend(sequence, device_config) + results = backend.run(progress_bar=False) + sampled_state_counts = results.sample_final_state(N_samples=config['samples']) + else: + raise NotImplementedError(f"Device Option {device_wrapper.device_name} not implemented") + + valid_state_counts = self._filter_invalid_states(sampled_state_counts, nodes, edges) + state = self._select_best_state(valid_state_counts, nodes) + state_nodes = self._translate_state_to_nodes(state, nodes) + + return state_nodes, end_time_measurement(start), {} + + 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. + """ + pulses = self._create_pulses(device) + sequence = pulser.Sequence(register, device) + sequence.declare_channel("Rydberg global", "rydberg_global") + for pulse in pulses: + sequence.add(pulse, "Rydberg global") + return sequence + + def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: + """ + Creates pulses tuned to MIS problem. + + Pulse creation is a whole art/science on its own that we have not delved into yet. + If you shape and finetune your pulses to decrease compute time on your neutral atom device. + 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. + """ + 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 + + delta_0 = -3 * delta_factor + delta_f = 1 * delta_factor + + t_rise = 2000 + t_fall = 2000 + 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 + ) + sweep = pulser.Pulse.ConstantAmplitude( + 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 + ) + pulses = [rise, sweep, fall] + + for pulse in pulses: + channel.validate_pulse(pulse) + + return pulses + + def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> dict: + valid_state_counts = {} + for state, count in state_counts.items(): + selected_nodes = self._translate_state_to_nodes(state, nodes) + + is_valid = True + for edge in edges: + if edge[0] in selected_nodes and edge[1] in selected_nodes: + is_valid = False + break + if is_valid: + valid_state_counts[state] = count + + return valid_state_counts + + 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: + # TODO: Implement the samplers + try: + best_state = max(states, key=lambda k: states[k]) + except: # pylint: disable=W0702 + # TODO: Specify error + # TODO: Clean up this monstrocity + n_nodes = len(nodes) + best_state = "0" * n_nodes + return best_state diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index fb8b1b35..46abdd39 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.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. - +import logging from typing import Tuple from typing import TypedDict @@ -23,7 +23,7 @@ from qiskit.circuit.library import TwoLocal from qiskit.opflow import PauliSumOp from qiskit_optimization.applications import OptimizationApplication -from qiskit_ibm_runtime import QiskitRuntimeService +# from qiskit_ibm_runtime import QiskitRuntimeService from modules.solvers.Solver import * from utils import start_time_measurement, end_time_measurement @@ -39,7 +39,8 @@ def __init__(self): Constructor method """ super().__init__() - self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu", "ibm_eagle"] + # self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu", "ibm_eagle"] + self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu"] @staticmethod def get_requirements() -> list[dict]: @@ -52,7 +53,7 @@ def get_requirements() -> list[dict]: return [ { "name": "qiskit", - "version": "0.40.0" + "version": "0.45.0" }, { "name": "qiskit-optimization", @@ -71,9 +72,9 @@ def get_default_submodule(self, option: str) -> Core: elif option == "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") + # elif option == "ibm_eagle": + # from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 + # return HelperClass("ibm_eagle") else: raise NotImplementedError(f"Device Option {option} not implemented") @@ -209,6 +210,9 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs elif config["method"] == "qaoa": algorithm = QAOA(reps=config["depth"], optimizer=optimizer, quantum_instance=self._get_quantum_instance(device_wrapper)) + else: + logging.warning("No method selected in QiskitQAOA. Continue with NumPyMinimumEigensolver.") + algorithm = NumPyMinimumEigensolver() # run actual optimization algorithm result = algorithm.compute_minimum_eigenvalue(ising_op) @@ -222,11 +226,11 @@ def _get_quantum_instance(device_wrapper: any) -> any: 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) + # 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') diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index 3c76889b..e860751e 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -102,7 +102,8 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi 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[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2]]) + 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, diff --git a/src/modules/training/QCBM.py b/src/modules/training/QCBM.py index 770613b3..4e558c16 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/training/QCBM.py @@ -190,6 +190,8 @@ def setup_training(self, input_data, config) -> tuple: self.loss_func = self.kl_divergence elif config['loss'] == "NLL": self.loss_func = self.nll + elif config['loss'] == "MMD": + self.loss_func = self.mmd else: raise NotImplementedError("Loss function not implemented") @@ -284,9 +286,9 @@ def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> (d input_data["best_parameter"] = es.result[0] best_sample = self.sample_from_pmf(self.n_states_range, - best_pmf.get() if GPU else best_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 @@ -304,14 +306,16 @@ def data_visualization(self, loss_epoch, pmfs_model, samples, epoch): else: counts = samples[int(index)] - metrics = self.generalization_metrics.get_metrics(counts.get() if GPU else counts) + metrics = self.generalization_metrics.get_metrics(counts if GPU else counts) 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( diff --git a/src/modules/training/Training.py b/src/modules/training/Training.py index fbe61a96..5457f702 100644 --- a/src/modules/training/Training.py +++ b/src/modules/training/Training.py @@ -111,6 +111,16 @@ def nll(self, pmf_model, pmf_target): pmf_model[pmf_model == 0] = 1e-8 return -np.sum(pmf_target * np.log(pmf_model), axis=1) + def mmd(self, pmf_model, pmf_target): + pmf_model[pmf_model == 0] = 1e-8 + sigma = 1/pmf_model.shape[1] # TODO Improve scaling sigma and revise Formula + kernel_distance = np.exp((-np.square(pmf_model - pmf_target) / (sigma ** 2))) + mmd = 2 - 2 * np.mean(kernel_distance, axis=1) + # The correct formula would take the transformed distances of both distributions into account. Since we are + # not sampling from the distribution but using the probability mass function we can skip this step since the + # sum of both, a modified version of the Gaussian kernel is used. + return mmd + class Timing: """ This module is an abstraction of time measurement for for both CPU and GPU processes diff --git a/tests/configs/valid/MIS.yml b/tests/configs/valid/MIS.yml new file mode 100644 index 00000000..b4b660a2 --- /dev/null +++ b/tests/configs/valid/MIS.yml @@ -0,0 +1,30 @@ +application: + config: + filling_fraction: + - 0.2 + size: + - 5 + spacing: + - 0.4 + name: MIS + submodules: + - config: {} + name: NeutralAtom + submodules: + - config: + samples: + - 10 + name: NeutralAtomMIS + submodules: + - config: + SPAM: + - false + amplitude: + - false + dephasing: + - false + doppler: + - false + name: MockNeutralAtomDevice + submodules: [] +repetitions: 1