From 3357fce0aabec29235083a4b6bfe272d2c58037e Mon Sep 17 00:00:00 2001 From: Marvin Erdmann <106394656+Marvmann@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:09:17 +0100 Subject: [PATCH] Release 2.1.1 (#146) * Follow PEP 8 standard (#142) * QML Architecture Updates (#145) --------- Co-authored-by: @GreshmaShaji Co-authored-by: Florian Kiwit Co-authored-by: @Marvmann --- .github/CODEOWNERS | 4 +- .github/workflows/lint.yml | 29 +- .settings/module_db.json | 7136 +++++++++-------- .settings/requirements_full.txt | 4 +- AUTHORS | 2 + README.md | 68 +- docs/analysis.rst | 2 +- docs/developer.rst | 94 +- docs/tutorial.rst | 110 +- h origin HEAD | 31 - src/BenchmarkManager.py | 198 +- src/BenchmarkRecord.py | 63 +- src/ConfigManager.py | 147 +- src/Installer.py | 141 +- src/Metrics.py | 53 +- src/Plotter.py | 129 +- src/demo/instruction_demo.py | 59 +- src/main.py | 51 +- src/modules/Core.py | 48 +- src/modules/applications/Application.py | 19 +- src/modules/applications/Mapping.py | 37 +- .../applications/optimization/ACL/ACL.py | 1121 +-- .../applications/optimization/ACL/__init__.py | 7 +- .../optimization/ACL/mappings/ISING.py | 129 +- .../optimization/ACL/mappings/QUBO.py | 188 +- .../optimization/ACL/mappings/__init__.py | 7 +- .../applications/optimization/MIS/MIS.py | 156 +- .../applications/optimization/MIS/__init__.py | 6 +- .../optimization/MIS/data/__init__.py | 6 +- .../optimization/MIS/data/graph_layouts.py | 102 +- .../optimization/MIS/mappings/NeutralAtom.py | 52 +- .../optimization/MIS/mappings/__init__.py | 6 +- .../applications/optimization/Optimization.py | 86 +- .../applications/optimization/PVC/PVC.py | 217 +- .../applications/optimization/PVC/__init__.py | 6 +- .../PVC/data/createReferenceGraph.py | 35 +- .../optimization/PVC/mappings/ISING.py | 104 +- .../optimization/PVC/mappings/QUBO.py | 175 +- .../optimization/PVC/mappings/__init__.py | 6 +- .../applications/optimization/SAT/SAT.py | 239 +- .../applications/optimization/SAT/__init__.py | 6 +- .../optimization/SAT/mappings/ChoiISING.py | 87 +- .../optimization/SAT/mappings/ChoiQUBO.py | 191 +- .../optimization/SAT/mappings/DinneenISING.py | 85 +- .../optimization/SAT/mappings/DinneenQUBO.py | 121 +- .../optimization/SAT/mappings/Direct.py | 89 +- .../optimization/SAT/mappings/QubovertQUBO.py | 110 +- .../optimization/SAT/mappings/__init__.py | 6 +- .../applications/optimization/SCP/SCP.py | 89 +- .../applications/optimization/SCP/__init__.py | 6 +- .../optimization/SCP/data/__init__.py | 6 +- .../optimization/SCP/mappings/__init__.py | 6 +- .../optimization/SCP/mappings/qubovertQUBO.py | 81 +- .../applications/optimization/TSP/TSP.py | 135 +- .../applications/optimization/TSP/__init__.py | 6 +- .../TSP/data/createReferenceGraph.py | 44 +- .../optimization/TSP/mappings/ISING.py | 201 +- .../optimization/TSP/mappings/QUBO.py | 69 +- .../optimization/TSP/mappings/__init__.py | 6 +- .../applications/optimization/__init__.py | 4 +- src/modules/applications/qml/Circuit.py | 33 + src/modules/applications/qml/DataHandler.py | 69 + src/modules/applications/qml/Model.py | 59 + src/modules/applications/{QML => qml}/QML.py | 24 +- src/modules/applications/qml/Training.py | 33 + .../applications/{QML => qml}/__init__.py | 0 .../generative_modeling/GenerativeModeling.py | 71 +- .../generative_modeling/__init__.py | 0 .../circuits/CircuitCardinality.py | 61 +- .../circuits/CircuitCopula.py | 58 +- .../circuits/CircuitGenerative.py} | 38 +- .../circuits/CircuitStandard.py | 63 +- .../generative_modeling}/circuits/__init__.py | 0 .../generative_modeling/data/MG_2D.npy | 0 .../generative_modeling/data/O_2D.npy | 0 .../generative_modeling/data/Stocks_2D.npy | 0 .../generative_modeling/data/X_2D.npy | 0 .../generative_modeling/data/__init__.py | 0 .../data/data_handler/ContinuousData.py | 76 +- .../data_handler/DataHandlerGenerative.py} | 138 +- .../data/data_handler/DiscreteData.py | 75 +- .../data/data_handler/__init__.py | 0 .../mappings/CustomQiskitNoisyBackend.py | 222 +- .../mappings/LibraryGenerative.py} | 81 +- .../mappings/LibraryPennylane.py | 103 +- .../mappings/LibraryQiskit.py | 121 +- .../mappings/PresetQiskitNoisyBackend.py | 159 +- .../generative_modeling/mappings/__init__.py | 0 .../metrics}/MetricsGeneralization.py | 86 +- .../training/Inference.py | 67 +- .../qml/generative_modeling}/training/QCBM.py | 181 +- .../qml/generative_modeling}/training/QGAN.py | 222 +- .../training/TrainingGenerative.py} | 103 +- .../generative_modeling}/training/__init__.py | 0 .../transformations/MinMax.py | 63 +- .../transformations/PIT.py | 74 +- .../transformations/Transformation.py | 110 +- .../transformations/__init__.py | 0 src/modules/devices/Device.py | 40 +- src/modules/devices/HelperClass.py | 22 +- src/modules/devices/Local.py | 20 +- .../devices/SimulatedAnnealingSampler.py | 31 +- src/modules/devices/braket/Braket.py | 175 +- src/modules/devices/braket/Ionq.py | 27 +- src/modules/devices/braket/LocalSimulator.py | 21 +- src/modules/devices/braket/OQC.py | 29 +- src/modules/devices/braket/Rigetti.py | 29 +- src/modules/devices/braket/SV1.py | 29 +- src/modules/devices/braket/TN1.py | 29 +- .../devices/pulser/MockNeutralAtomDevice.py | 23 +- src/modules/devices/pulser/Pulser.py | 22 +- src/modules/solvers/Annealer.py | 72 +- src/modules/solvers/ClassicalSAT.py | 50 +- src/modules/solvers/GreedyClassicalPVC.py | 70 +- src/modules/solvers/GreedyClassicalTSP.py | 64 +- src/modules/solvers/MIPsolverACL.py | 55 +- src/modules/solvers/NeutralAtomMIS.py | 106 +- src/modules/solvers/PennylaneQAOA.py | 214 +- src/modules/solvers/QAOA.py | 306 +- src/modules/solvers/QiskitQAOA.py | 214 +- src/modules/solvers/RandomClassicalPVC.py | 67 +- src/modules/solvers/RandomClassicalSAT.py | 55 +- src/modules/solvers/RandomClassicalTSP.py | 55 +- .../solvers/ReverseGreedyClassicalPVC.py | 70 +- .../solvers/ReverseGreedyClassicalTSP.py | 66 +- src/modules/solvers/Solver.py | 31 +- src/quark2_adapter/adapters.py | 164 +- .../legacy_classes/Application.py | 90 +- src/quark2_adapter/legacy_classes/Device.py | 26 +- src/quark2_adapter/legacy_classes/Mapping.py | 41 +- src/quark2_adapter/legacy_classes/Solver.py | 41 +- src/utils.py | 84 +- src/utils_mpi.py | 67 +- 133 files changed, 8466 insertions(+), 8850 deletions(-) delete mode 100644 h origin HEAD create mode 100644 src/modules/applications/qml/Circuit.py create mode 100644 src/modules/applications/qml/DataHandler.py create mode 100644 src/modules/applications/qml/Model.py rename src/modules/applications/{QML => qml}/QML.py (60%) create mode 100644 src/modules/applications/qml/Training.py rename src/modules/applications/{QML => qml}/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/GenerativeModeling.py (69%) rename src/modules/applications/{QML => qml}/generative_modeling/__init__.py (100%) rename src/modules/{ => applications/qml/generative_modeling}/circuits/CircuitCardinality.py (74%) rename src/modules/{ => applications/qml/generative_modeling}/circuits/CircuitCopula.py (77%) rename src/modules/{circuits/Circuit.py => applications/qml/generative_modeling/circuits/CircuitGenerative.py} (74%) rename src/modules/{ => applications/qml/generative_modeling}/circuits/CircuitStandard.py (70%) rename src/modules/{ => applications/qml/generative_modeling}/circuits/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/MG_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/O_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/Stocks_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/X_2D.npy (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/ContinuousData.py (69%) rename src/modules/applications/{QML/generative_modeling/data/data_handler/DataHandler.py => qml/generative_modeling/data/data_handler/DataHandlerGenerative.py} (60%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/DiscreteData.py (75%) rename src/modules/applications/{QML => qml}/generative_modeling/data/data_handler/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/CustomQiskitNoisyBackend.py (73%) rename src/modules/applications/{QML/generative_modeling/mappings/Library.py => qml/generative_modeling/mappings/LibraryGenerative.py} (54%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/LibraryPennylane.py (74%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/LibraryQiskit.py (81%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/PresetQiskitNoisyBackend.py (77%) rename src/modules/applications/{QML => qml}/generative_modeling/mappings/__init__.py (100%) rename src/modules/applications/{QML/generative_modeling/data/data_handler => qml/generative_modeling/metrics}/MetricsGeneralization.py (60%) rename src/modules/{ => applications/qml/generative_modeling}/training/Inference.py (67%) rename src/modules/{ => applications/qml/generative_modeling}/training/QCBM.py (71%) rename src/modules/{ => applications/qml/generative_modeling}/training/QGAN.py (74%) rename src/modules/{training/Training.py => applications/qml/generative_modeling/training/TrainingGenerative.py} (66%) rename src/modules/{ => applications/qml/generative_modeling}/training/__init__.py (100%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/MinMax.py (84%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/PIT.py (86%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/Transformation.py (69%) rename src/modules/applications/{QML => qml}/generative_modeling/transformations/__init__.py (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa55c2fc..52fd757b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,6 +3,6 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -# @philross, @drelu and @Marvmann will be requested for +# @philross, @drelu, @aluckow and @Marvmann will be requested for # review when someone opens a pull request. -* @philross @drelu @Marvmann +* @philross @drelu @aluckow @Marvmann diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 69090a1a..ee4ed936 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,6 +19,8 @@ jobs: defaults: run: shell: bash -el {0} + permissions: + contents: write steps: - name: Check out Git repository uses: actions/checkout@v4 @@ -29,8 +31,31 @@ jobs: python-version: '3.9.16' token: ${{ secrets.QUARK_GH_GITHUB_COM_TOKEN }} - - name: Install pylint - run: pip install pylint + - name: Install pylint and autopep8 + run: pip install pylint autopep8 + + - name: Disable Git LFS locking + run: git config lfs.https://github.com/QUARK-framework/QUARK.git/info/lfs.locksverify false + + - name: Run autopep8 (fix PEP8 issues automatically) + run: autopep8 --in-place --recursive --aggressive --max-line-length 120 -v . + + - name: Clean the workspace + if: github.event_name == 'pull_request' + run: git reset --hard + + - name: Commit changes if any + if: github.event_name == 'push' + run: | + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git add . + if git diff-index --quiet HEAD; then + echo "No changes to commit" + else + git commit -m "Apply autopep8 formatting" + git push origin HEAD:${{ github.ref }} + fi - name: Run pylint uses: wearerequired/lint-action@v2 diff --git a/.settings/module_db.json b/.settings/module_db.json index 225a6fba..d40604b5 100644 --- a/.settings/module_db.json +++ b/.settings/module_db.json @@ -1,3567 +1,3571 @@ -{ - "build_number": 13, - "build_date": "25-09-2024 22:08:09", - "git_revision_number": "150c71668c47ab9c98665d485437f6e9c7bb5e5f", - "modules": [ - { - "name": "PVC", - "class": "PVC", - "module": "modules.applications.optimization.PVC.PVC", - "submodules": [ - { - "name": "Ising", - "class": "Ising", - "args": {}, - "module": "modules.applications.optimization.PVC.mappings.ISING", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "QAOA", - "class": "QAOA", - "args": {}, - "module": "modules.solvers.QAOA", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "scipy", - "version": "1.12.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "LocalSimulator", - "class": "LocalSimulator", - "args": { - "device_name": "LocalSimulator" - }, - "module": "modules.devices.braket.LocalSimulator", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionQ", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti Ankaa-2", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PennylaneQAOA", - "class": "PennylaneQAOA", - "args": {}, - "module": "modules.solvers.PennylaneQAOA", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "amazon-braket-pennylane-plugin", - "version": "1.30.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionq", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "class": "OQC", - "args": { - "device_name": "OQC", - "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" - }, - "module": "modules.devices.braket.OQC", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "braket.local.qubit", - "class": "HelperClass", - "args": { - "device_name": "braket.local.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit", - "class": "HelperClass", - "args": { - "device_name": "default.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit.autograd", - "class": "HelperClass", - "args": { - "device_name": "default.qubit.autograd" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "qulacs.simulator", - "class": "HelperClass", - "args": { - "device_name": "qulacs.simulator" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.gpu", - "class": "HelperClass", - "args": { - "device_name": "lightning.gpu" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.qubit", - "class": "HelperClass", - "args": { - "device_name": "lightning.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - } - ] - } - ] - }, - { - "name": "QUBO", - "class": "QUBO", - "args": {}, - "module": "modules.applications.optimization.PVC.mappings.QUBO", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - }, - { - "name": "GreedyClassicalPVC", - "class": "GreedyClassicalPVC", - "args": {}, - "module": "modules.solvers.GreedyClassicalPVC", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "ReverseGreedyClassicalPVC", - "class": "ReverseGreedyClassicalPVC", - "args": {}, - "module": "modules.solvers.ReverseGreedyClassicalPVC", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "RandomPVC", - "class": "RandomPVC", - "args": {}, - "module": "modules.solvers.RandomClassicalPVC", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - } - ], - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ] - }, - { - "name": "SAT", - "class": "SAT", - "module": "modules.applications.optimization.SAT.SAT", - "submodules": [ - { - "name": "QubovertQUBO", - "class": "QubovertQUBO", - "args": {}, - "module": "modules.applications.optimization.SAT.mappings.QubovertQUBO", - "requirements": [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "qubovert", - "version": "1.2.5" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - }, - { - "name": "Direct", - "class": "Direct", - "args": {}, - "module": "modules.applications.optimization.SAT.mappings.Direct", - "requirements": [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "python-sat", - "version": "1.8.dev13" - } - ], - "submodules": [ - { - "name": "ClassicalSAT", - "class": "ClassicalSAT", - "args": {}, - "module": "modules.solvers.ClassicalSAT", - "requirements": [ - { - "name": "python-sat", - "version": "1.8.dev13" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "RandomSAT", - "class": "RandomSAT", - "args": {}, - "module": "modules.solvers.RandomClassicalSAT", - "requirements": [ - { - "name": "python-sat", - "version": "1.8.dev13" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - } - ] - }, - { - "name": "ChoiQUBO", - "class": "ChoiQUBO", - "args": {}, - "module": "modules.applications.optimization.SAT.mappings.ChoiQUBO", - "requirements": [ - { - "name": "nnf", - "version": "0.4.1" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - }, - { - "name": "DinneenQUBO", - "class": "DinneenQUBO", - "args": {}, - "module": "modules.applications.optimization.SAT.mappings.DinneenQUBO", - "requirements": [ - { - "name": "nnf", - "version": "0.4.1" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - }, - { - "name": "ChoiIsing", - "class": "ChoiIsing", - "args": {}, - "module": "modules.applications.optimization.SAT.mappings.ChoiISING", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, - { - "name": "nnf", - "version": "0.4.1" - } - ], - "submodules": [ - { - "name": "QAOA", - "class": "QAOA", - "args": {}, - "module": "modules.solvers.QAOA", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "scipy", - "version": "1.12.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "LocalSimulator", - "class": "LocalSimulator", - "args": { - "device_name": "LocalSimulator" - }, - "module": "modules.devices.braket.LocalSimulator", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionQ", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti Ankaa-2", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PennylaneQAOA", - "class": "PennylaneQAOA", - "args": {}, - "module": "modules.solvers.PennylaneQAOA", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "amazon-braket-pennylane-plugin", - "version": "1.30.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionq", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "class": "OQC", - "args": { - "device_name": "OQC", - "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" - }, - "module": "modules.devices.braket.OQC", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "braket.local.qubit", - "class": "HelperClass", - "args": { - "device_name": "braket.local.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit", - "class": "HelperClass", - "args": { - "device_name": "default.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit.autograd", - "class": "HelperClass", - "args": { - "device_name": "default.qubit.autograd" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "qulacs.simulator", - "class": "HelperClass", - "args": { - "device_name": "qulacs.simulator" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.gpu", - "class": "HelperClass", - "args": { - "device_name": "lightning.gpu" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.qubit", - "class": "HelperClass", - "args": { - "device_name": "lightning.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - } - ] - } - ] - }, - { - "name": "DinneenIsing", - "class": "DinneenIsing", - "args": {}, - "module": "modules.applications.optimization.SAT.mappings.DinneenISING", - "requirements": [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, - { - "name": "nnf", - "version": "0.4.1" - } - ], - "submodules": [ - { - "name": "QAOA", - "class": "QAOA", - "args": {}, - "module": "modules.solvers.QAOA", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "scipy", - "version": "1.12.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "LocalSimulator", - "class": "LocalSimulator", - "args": { - "device_name": "LocalSimulator" - }, - "module": "modules.devices.braket.LocalSimulator", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionQ", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti Ankaa-2", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PennylaneQAOA", - "class": "PennylaneQAOA", - "args": {}, - "module": "modules.solvers.PennylaneQAOA", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "amazon-braket-pennylane-plugin", - "version": "1.30.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionq", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "class": "OQC", - "args": { - "device_name": "OQC", - "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" - }, - "module": "modules.devices.braket.OQC", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "braket.local.qubit", - "class": "HelperClass", - "args": { - "device_name": "braket.local.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit", - "class": "HelperClass", - "args": { - "device_name": "default.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit.autograd", - "class": "HelperClass", - "args": { - "device_name": "default.qubit.autograd" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "qulacs.simulator", - "class": "HelperClass", - "args": { - "device_name": "qulacs.simulator" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.gpu", - "class": "HelperClass", - "args": { - "device_name": "lightning.gpu" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.qubit", - "class": "HelperClass", - "args": { - "device_name": "lightning.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - } - ] - } - ] - } - ], - "requirements": [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ] - }, - { - "name": "TSP", - "class": "TSP", - "module": "modules.applications.optimization.TSP.TSP", - "submodules": [ - { - "name": "Ising", - "class": "Ising", - "args": {}, - "module": "modules.applications.optimization.TSP.mappings.ISING", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, - { - "name": "more-itertools", - "version": "10.5.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, - { - "name": "pyqubo", - "version": "1.4.0" - }, - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "dwave_networkx", - "version": "0.8.15" - } - ], - "submodules": [ - { - "name": "QAOA", - "class": "QAOA", - "args": {}, - "module": "modules.solvers.QAOA", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "scipy", - "version": "1.12.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "LocalSimulator", - "class": "LocalSimulator", - "args": { - "device_name": "LocalSimulator" - }, - "module": "modules.devices.braket.LocalSimulator", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionQ", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti Ankaa-2", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PennylaneQAOA", - "class": "PennylaneQAOA", - "args": {}, - "module": "modules.solvers.PennylaneQAOA", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "amazon-braket-pennylane-plugin", - "version": "1.30.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "class": "SV1", - "args": { - "device_name": "SV1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" - }, - "module": "modules.devices.braket.SV1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "class": "TN1", - "args": { - "device_name": "TN1", - "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" - }, - "module": "modules.devices.braket.TN1", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1", - "class": "Ionq", - "args": { - "device_name": "ionq", - "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1" - }, - "module": "modules.devices.braket.Ionq", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2", - "class": "Rigetti", - "args": { - "device_name": "Rigetti", - "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2" - }, - "module": "modules.devices.braket.Rigetti", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "class": "OQC", - "args": { - "device_name": "OQC", - "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" - }, - "module": "modules.devices.braket.OQC", - "requirements": [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } - ], - "submodules": [] - }, - { - "name": "braket.local.qubit", - "class": "HelperClass", - "args": { - "device_name": "braket.local.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit", - "class": "HelperClass", - "args": { - "device_name": "default.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "default.qubit.autograd", - "class": "HelperClass", - "args": { - "device_name": "default.qubit.autograd" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "qulacs.simulator", - "class": "HelperClass", - "args": { - "device_name": "qulacs.simulator" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.gpu", - "class": "HelperClass", - "args": { - "device_name": "lightning.gpu" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "lightning.qubit", - "class": "HelperClass", - "args": { - "device_name": "lightning.qubit" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "QiskitQAOA", - "class": "QiskitQAOA", - "args": {}, - "module": "modules.solvers.QiskitQAOA", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "qasm_simulator", - "class": "HelperClass", - "args": { - "device_name": "qasm_simulator" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - }, - { - "name": "qasm_simulator_gpu", - "class": "HelperClass", - "args": { - "device_name": "qasm_simulator_gpu" - }, - "module": "modules.devices.HelperClass", - "requirements": [], - "submodules": [] - } - ] - } - ] - }, - { - "name": "QUBO", - "class": "QUBO", - "args": {}, - "module": "modules.applications.optimization.TSP.mappings.QUBO", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "dwave_networkx", - "version": "0.8.15" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - }, - { - "name": "GreedyClassicalTSP", - "class": "GreedyClassicalTSP", - "args": {}, - "module": "modules.solvers.GreedyClassicalTSP", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "ReverseGreedyClassicalTSP", - "class": "ReverseGreedyClassicalTSP", - "args": {}, - "module": "modules.solvers.ReverseGreedyClassicalTSP", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "RandomTSP", - "class": "RandomTSP", - "args": {}, - "module": "modules.solvers.RandomClassicalTSP", - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - } - ], - "requirements": [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ] - }, - { - "name": "ACL", - "class": "ACL", - "module": "modules.applications.optimization.ACL.ACL", - "submodules": [ - { - "name": "MIPsolverACL", - "class": "MIPaclp", - "args": {}, - "module": "modules.solvers.MIPsolverACL", - "requirements": [ - { - "name": "pulp", - "version": "2.9.0" - } - ], - "submodules": [ - { - "name": "Local", - "class": "Local", - "args": {}, - "module": "modules.devices.Local", - "requirements": [], - "submodules": [] - } - ] - }, - { - "name": "QUBO", - "class": "Qubo", - "args": {}, - "module": "modules.applications.optimization.ACL.mappings.QUBO", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - } - ], - "requirements": [ - { - "name": "pulp", - "version": "2.9.0" - }, - { - "name": "pandas", - "version": "2.2.2" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "openpyxl", - "version": "3.1.5" - } - ] - }, - { - "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.19.0" - } - ], - "submodules": [ - { - "name": "NeutralAtomMIS", - "class": "NeutralAtomMIS", - "args": {}, - "module": "modules.solvers.NeutralAtomMIS", - "requirements": [ - { - "name": "pulser", - "version": "0.19.0" - } - ], - "submodules": [ - { - "name": "MockNeutralAtomDevice", - "class": "MockNeutralAtomDevice", - "args": {}, - "module": "modules.devices.pulser.MockNeutralAtomDevice", - "requirements": [ - { - "name": "pulser", - "version": "0.19.0" - } - ], - "submodules": [] - } - ] - } - ] - } - ], - "requirements": [] - }, - { - "name": "SCP", - "class": "SCP", - "module": "modules.applications.optimization.SCP.SCP", - "submodules": [ - { - "name": "qubovertQUBO", - "class": "QubovertQUBO", - "args": {}, - "module": "modules.applications.optimization.SCP.mappings.qubovertQUBO", - "requirements": [ - { - "name": "qubovert", - "version": "1.2.5" - } - ], - "submodules": [ - { - "name": "Annealer", - "class": "Annealer", - "args": {}, - "module": "modules.solvers.Annealer", - "requirements": [], - "submodules": [ - { - "name": "Simulated Annealer", - "class": "SimulatedAnnealingSampler", - "args": {}, - "module": "modules.devices.SimulatedAnnealingSampler", - "requirements": [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ], - "submodules": [] - } - ] - } - ] - } - ], - "requirements": [] - }, - { - "name": "GenerativeModeling", - "class": "GenerativeModeling", - "module": "modules.applications.QML.generative_modeling.GenerativeModeling", - "submodules": [ - { - "name": "Continuous Data", - "class": "ContinuousData", - "args": {}, - "module": "modules.applications.QML.generative_modeling.data.data_handler.ContinuousData", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "PIT", - "class": "PIT", - "args": {}, - "module": "modules.applications.QML.generative_modeling.transformations.PIT", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "pandas", - "version": "2.2.2" - } - ], - "submodules": [ - { - "name": "CircuitCopula", - "class": "CircuitCopula", - "args": {}, - "module": "modules.circuits.CircuitCopula", - "requirements": [ - { - "name": "scipy", - "version": "1.12.0" - } - ], - "submodules": [ - { - "name": "LibraryQiskit", - "class": "LibraryQiskit", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "LibraryPennylane", - "class": "LibraryPennylane", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "jax", - "version": "0.4.30" - }, - { - "name": "jaxlib", - "version": "0.4.30" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "CustomQiskitNoisyBackend", - "class": "CustomQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PresetQiskitNoisyBackend", - "class": "PresetQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_ibm_runtime", - "version": "0.29.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - } - ] - } - ] - }, - { - "name": "MinMax", - "class": "MinMax", - "args": {}, - "module": "modules.applications.QML.generative_modeling.transformations.MinMax", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "CircuitStandard", - "class": "CircuitStandard", - "args": {}, - "module": "modules.circuits.CircuitStandard", - "requirements": [], - "submodules": [ - { - "name": "LibraryQiskit", - "class": "LibraryQiskit", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "LibraryPennylane", - "class": "LibraryPennylane", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "jax", - "version": "0.4.30" - }, - { - "name": "jaxlib", - "version": "0.4.30" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "CustomQiskitNoisyBackend", - "class": "CustomQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PresetQiskitNoisyBackend", - "class": "PresetQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_ibm_runtime", - "version": "0.29.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - } - ] - }, - { - "name": "CircuitCardinality", - "class": "CircuitCardinality", - "args": {}, - "module": "modules.circuits.CircuitCardinality", - "requirements": [], - "submodules": [ - { - "name": "LibraryQiskit", - "class": "LibraryQiskit", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "LibraryPennylane", - "class": "LibraryPennylane", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "jax", - "version": "0.4.30" - }, - { - "name": "jaxlib", - "version": "0.4.30" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "CustomQiskitNoisyBackend", - "class": "CustomQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PresetQiskitNoisyBackend", - "class": "PresetQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_ibm_runtime", - "version": "0.29.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - } - ] - } - ] - } - ] - }, - { - "name": "Discrete Data", - "class": "DiscreteData", - "args": {}, - "module": "modules.applications.QML.generative_modeling.data.data_handler.DiscreteData", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "CircuitCardinality", - "class": "CircuitCardinality", - "args": {}, - "module": "modules.circuits.CircuitCardinality", - "requirements": [], - "submodules": [ - { - "name": "LibraryQiskit", - "class": "LibraryQiskit", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryQiskit", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "LibraryPennylane", - "class": "LibraryPennylane", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.LibraryPennylane", - "requirements": [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "jax", - "version": "0.4.30" - }, - { - "name": "jaxlib", - "version": "0.4.30" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "QGAN", - "class": "QGAN", - "args": {}, - "module": "modules.training.QGAN", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "torch", - "version": "2.2.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "CustomQiskitNoisyBackend", - "class": "CustomQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - }, - { - "name": "PresetQiskitNoisyBackend", - "class": "PresetQiskitNoisyBackend", - "args": {}, - "module": "modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend", - "requirements": [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_ibm_runtime", - "version": "0.29.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [ - { - "name": "QCBM", - "class": "QCBM", - "args": {}, - "module": "modules.training.QCBM", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } - ], - "submodules": [] - }, - { - "name": "Inference", - "class": "Inference", - "args": {}, - "module": "modules.training.Inference", - "requirements": [ - { - "name": "numpy", - "version": "1.26.4" - } - ], - "submodules": [] - } - ] - } - ] - } - ] - } - ], - "requirements": [] - } - ] +{ + "build_number": 15, + "build_date": "28-10-2024 09:38:32", + "git_revision_number": "62a87b5b3c81a449424964c1672d9b32bc678c1a", + "modules": [ + { + "name": "PVC", + "class": "PVC", + "module": "modules.applications.optimization.PVC.PVC", + "submodules": [ + { + "name": "Ising", + "class": "Ising", + "args": {}, + "module": "modules.applications.optimization.PVC.mappings.ISING", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "dimod", + "version": "0.12.17" + }, + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "QAOA", + "class": "QAOA", + "args": {}, + "module": "modules.solvers.QAOA", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "scipy", + "version": "1.12.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "LocalSimulator", + "class": "LocalSimulator", + "args": { + "device_name": "LocalSimulator" + }, + "module": "modules.devices.braket.LocalSimulator", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionQ", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti Aspen-9", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PennylaneQAOA", + "class": "PennylaneQAOA", + "args": {}, + "module": "modules.solvers.PennylaneQAOA", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "amazon-braket-pennylane-plugin", + "version": "1.30.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionq", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", + "class": "OQC", + "args": { + "device_name": "OQC", + "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" + }, + "module": "modules.devices.braket.OQC", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "braket.local.qubit", + "class": "HelperClass", + "args": { + "device_name": "braket.local.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit", + "class": "HelperClass", + "args": { + "device_name": "default.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit.autograd", + "class": "HelperClass", + "args": { + "device_name": "default.qubit.autograd" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "qulacs.simulator", + "class": "HelperClass", + "args": { + "device_name": "qulacs.simulator" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.gpu", + "class": "HelperClass", + "args": { + "device_name": "lightning.gpu" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.qubit", + "class": "HelperClass", + "args": { + "device_name": "lightning.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + } + ] + } + ] + }, + { + "name": "QUBO", + "class": "QUBO", + "args": {}, + "module": "modules.applications.optimization.PVC.mappings.QUBO", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + }, + { + "name": "GreedyClassicalPVC", + "class": "GreedyClassicalPVC", + "args": {}, + "module": "modules.solvers.GreedyClassicalPVC", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "ReverseGreedyClassicalPVC", + "class": "ReverseGreedyClassicalPVC", + "args": {}, + "module": "modules.solvers.ReverseGreedyClassicalPVC", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "RandomPVC", + "class": "RandomPVC", + "args": {}, + "module": "modules.solvers.RandomClassicalPVC", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + } + ], + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ] + }, + { + "name": "SAT", + "class": "SAT", + "module": "modules.applications.optimization.SAT.SAT", + "submodules": [ + { + "name": "QubovertQUBO", + "class": "QubovertQUBO", + "args": {}, + "module": "modules.applications.optimization.SAT.mappings.QubovertQUBO", + "requirements": [ + { + "name": "nnf", + "version": "0.4.1" + }, + { + "name": "qubovert", + "version": "1.2.5" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + }, + { + "name": "Direct", + "class": "Direct", + "args": {}, + "module": "modules.applications.optimization.SAT.mappings.Direct", + "requirements": [ + { + "name": "nnf", + "version": "0.4.1" + }, + { + "name": "python-sat", + "version": "1.8.dev13" + } + ], + "submodules": [ + { + "name": "ClassicalSAT", + "class": "ClassicalSAT", + "args": {}, + "module": "modules.solvers.ClassicalSAT", + "requirements": [ + { + "name": "python-sat", + "version": "1.8.dev13" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "RandomSAT", + "class": "RandomSAT", + "args": {}, + "module": "modules.solvers.RandomClassicalSAT", + "requirements": [ + { + "name": "python-sat", + "version": "1.8.dev13" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + } + ] + }, + { + "name": "ChoiQUBO", + "class": "ChoiQUBO", + "args": {}, + "module": "modules.applications.optimization.SAT.mappings.ChoiQUBO", + "requirements": [ + { + "name": "nnf", + "version": "0.4.1" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + }, + { + "name": "DinneenQUBO", + "class": "DinneenQUBO", + "args": {}, + "module": "modules.applications.optimization.SAT.mappings.DinneenQUBO", + "requirements": [ + { + "name": "nnf", + "version": "0.4.1" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + }, + { + "name": "ChoiIsing", + "class": "ChoiIsing", + "args": {}, + "module": "modules.applications.optimization.SAT.mappings.ChoiISING", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "dimod", + "version": "0.12.17" + }, + { + "name": "nnf", + "version": "0.4.1" + } + ], + "submodules": [ + { + "name": "QAOA", + "class": "QAOA", + "args": {}, + "module": "modules.solvers.QAOA", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "scipy", + "version": "1.12.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "LocalSimulator", + "class": "LocalSimulator", + "args": { + "device_name": "LocalSimulator" + }, + "module": "modules.devices.braket.LocalSimulator", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionQ", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti Aspen-9", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PennylaneQAOA", + "class": "PennylaneQAOA", + "args": {}, + "module": "modules.solvers.PennylaneQAOA", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "amazon-braket-pennylane-plugin", + "version": "1.30.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionq", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", + "class": "OQC", + "args": { + "device_name": "OQC", + "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" + }, + "module": "modules.devices.braket.OQC", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "braket.local.qubit", + "class": "HelperClass", + "args": { + "device_name": "braket.local.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit", + "class": "HelperClass", + "args": { + "device_name": "default.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit.autograd", + "class": "HelperClass", + "args": { + "device_name": "default.qubit.autograd" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "qulacs.simulator", + "class": "HelperClass", + "args": { + "device_name": "qulacs.simulator" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.gpu", + "class": "HelperClass", + "args": { + "device_name": "lightning.gpu" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.qubit", + "class": "HelperClass", + "args": { + "device_name": "lightning.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + } + ] + } + ] + }, + { + "name": "DinneenIsing", + "class": "DinneenIsing", + "args": {}, + "module": "modules.applications.optimization.SAT.mappings.DinneenISING", + "requirements": [ + { + "name": "nnf", + "version": "0.4.1" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "dimod", + "version": "0.12.17" + }, + { + "name": "nnf", + "version": "0.4.1" + } + ], + "submodules": [ + { + "name": "QAOA", + "class": "QAOA", + "args": {}, + "module": "modules.solvers.QAOA", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "scipy", + "version": "1.12.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "LocalSimulator", + "class": "LocalSimulator", + "args": { + "device_name": "LocalSimulator" + }, + "module": "modules.devices.braket.LocalSimulator", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionQ", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti Aspen-9", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PennylaneQAOA", + "class": "PennylaneQAOA", + "args": {}, + "module": "modules.solvers.PennylaneQAOA", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "amazon-braket-pennylane-plugin", + "version": "1.30.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionq", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", + "class": "OQC", + "args": { + "device_name": "OQC", + "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" + }, + "module": "modules.devices.braket.OQC", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "braket.local.qubit", + "class": "HelperClass", + "args": { + "device_name": "braket.local.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit", + "class": "HelperClass", + "args": { + "device_name": "default.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit.autograd", + "class": "HelperClass", + "args": { + "device_name": "default.qubit.autograd" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "qulacs.simulator", + "class": "HelperClass", + "args": { + "device_name": "qulacs.simulator" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.gpu", + "class": "HelperClass", + "args": { + "device_name": "lightning.gpu" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.qubit", + "class": "HelperClass", + "args": { + "device_name": "lightning.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + } + ] + } + ] + } + ], + "requirements": [ + { + "name": "nnf", + "version": "0.4.1" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ] + }, + { + "name": "TSP", + "class": "TSP", + "module": "modules.applications.optimization.TSP.TSP", + "submodules": [ + { + "name": "Ising", + "class": "Ising", + "args": {}, + "module": "modules.applications.optimization.TSP.mappings.ISING", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "dimod", + "version": "0.12.17" + }, + { + "name": "more-itertools", + "version": "10.5.0" + }, + { + "name": "qiskit-optimization", + "version": "0.6.1" + }, + { + "name": "pyqubo", + "version": "1.4.0" + }, + { + "name": "networkx", + "version": "3.2.1" + }, + { + "name": "dwave_networkx", + "version": "0.8.15" + } + ], + "submodules": [ + { + "name": "QAOA", + "class": "QAOA", + "args": {}, + "module": "modules.solvers.QAOA", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "scipy", + "version": "1.12.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "LocalSimulator", + "class": "LocalSimulator", + "args": { + "device_name": "LocalSimulator" + }, + "module": "modules.devices.braket.LocalSimulator", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionQ", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti Aspen-9", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PennylaneQAOA", + "class": "PennylaneQAOA", + "args": {}, + "module": "modules.solvers.PennylaneQAOA", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "amazon-braket-pennylane-plugin", + "version": "1.30.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "class": "SV1", + "args": { + "device_name": "SV1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/sv1" + }, + "module": "modules.devices.braket.SV1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "class": "TN1", + "args": { + "device_name": "TN1", + "arn": "arn:aws:braket:::device/quantum-simulator/amazon/tn1" + }, + "module": "modules.devices.braket.TN1", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "class": "Ionq", + "args": { + "device_name": "ionq", + "arn": "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony" + }, + "module": "modules.devices.braket.Ionq", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "class": "Rigetti", + "args": { + "device_name": "Rigetti", + "arn": "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + }, + "module": "modules.devices.braket.Rigetti", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", + "class": "OQC", + "args": { + "device_name": "OQC", + "arn": "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy" + }, + "module": "modules.devices.braket.OQC", + "requirements": [ + { + "name": "amazon-braket-sdk", + "version": "1.87.0" + }, + { + "name": "botocore", + "version": "1.35.20" + }, + { + "name": "boto3", + "version": "1.35.20" + } + ], + "submodules": [] + }, + { + "name": "braket.local.qubit", + "class": "HelperClass", + "args": { + "device_name": "braket.local.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit", + "class": "HelperClass", + "args": { + "device_name": "default.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "default.qubit.autograd", + "class": "HelperClass", + "args": { + "device_name": "default.qubit.autograd" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "qulacs.simulator", + "class": "HelperClass", + "args": { + "device_name": "qulacs.simulator" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.gpu", + "class": "HelperClass", + "args": { + "device_name": "lightning.gpu" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "lightning.qubit", + "class": "HelperClass", + "args": { + "device_name": "lightning.qubit" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "QiskitQAOA", + "class": "QiskitQAOA", + "args": {}, + "module": "modules.solvers.QiskitQAOA", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit-optimization", + "version": "0.6.1" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "qiskit-algorithms", + "version": "0.3.0" + } + ], + "submodules": [ + { + "name": "qasm_simulator", + "class": "HelperClass", + "args": { + "device_name": "qasm_simulator" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + }, + { + "name": "qasm_simulator_gpu", + "class": "HelperClass", + "args": { + "device_name": "qasm_simulator_gpu" + }, + "module": "modules.devices.HelperClass", + "requirements": [], + "submodules": [] + } + ] + } + ] + }, + { + "name": "QUBO", + "class": "QUBO", + "args": {}, + "module": "modules.applications.optimization.TSP.mappings.QUBO", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + }, + { + "name": "dwave_networkx", + "version": "0.8.15" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + }, + { + "name": "GreedyClassicalTSP", + "class": "GreedyClassicalTSP", + "args": {}, + "module": "modules.solvers.GreedyClassicalTSP", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "ReverseGreedyClassicalTSP", + "class": "ReverseGreedyClassicalTSP", + "args": {}, + "module": "modules.solvers.ReverseGreedyClassicalTSP", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "RandomTSP", + "class": "RandomTSP", + "args": {}, + "module": "modules.solvers.RandomClassicalTSP", + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + } + ], + "requirements": [ + { + "name": "networkx", + "version": "3.2.1" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ] + }, + { + "name": "ACL", + "class": "ACL", + "module": "modules.applications.optimization.ACL.ACL", + "submodules": [ + { + "name": "MIPsolverACL", + "class": "MIPaclp", + "args": {}, + "module": "modules.solvers.MIPsolverACL", + "requirements": [ + { + "name": "pulp", + "version": "2.9.0" + } + ], + "submodules": [ + { + "name": "Local", + "class": "Local", + "args": {}, + "module": "modules.devices.Local", + "requirements": [], + "submodules": [] + } + ] + }, + { + "name": "QUBO", + "class": "Qubo", + "args": {}, + "module": "modules.applications.optimization.ACL.mappings.QUBO", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "qiskit-optimization", + "version": "0.6.1" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + } + ], + "requirements": [ + { + "name": "pulp", + "version": "2.9.0" + }, + { + "name": "pandas", + "version": "2.2.2" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "openpyxl", + "version": "3.1.5" + } + ] + }, + { + "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.19.0" + } + ], + "submodules": [ + { + "name": "NeutralAtomMIS", + "class": "NeutralAtomMIS", + "args": {}, + "module": "modules.solvers.NeutralAtomMIS", + "requirements": [ + { + "name": "pulser", + "version": "0.19.0" + } + ], + "submodules": [ + { + "name": "MockNeutralAtomDevice", + "class": "MockNeutralAtomDevice", + "args": {}, + "module": "modules.devices.pulser.MockNeutralAtomDevice", + "requirements": [ + { + "name": "pulser", + "version": "0.19.0" + } + ], + "submodules": [] + } + ] + } + ] + } + ], + "requirements": [] + }, + { + "name": "SCP", + "class": "SCP", + "module": "modules.applications.optimization.SCP.SCP", + "submodules": [ + { + "name": "qubovertQUBO", + "class": "QubovertQUBO", + "args": {}, + "module": "modules.applications.optimization.SCP.mappings.qubovertQUBO", + "requirements": [ + { + "name": "qubovert", + "version": "1.2.5" + } + ], + "submodules": [ + { + "name": "Annealer", + "class": "Annealer", + "args": {}, + "module": "modules.solvers.Annealer", + "requirements": [], + "submodules": [ + { + "name": "Simulated Annealer", + "class": "SimulatedAnnealingSampler", + "args": {}, + "module": "modules.devices.SimulatedAnnealingSampler", + "requirements": [ + { + "name": "dwave-samplers", + "version": "1.3.0" + } + ], + "submodules": [] + } + ] + } + ] + } + ], + "requirements": [] + }, + { + "name": "GenerativeModeling", + "class": "GenerativeModeling", + "module": "modules.applications.qml.generative_modeling.GenerativeModeling", + "submodules": [ + { + "name": "Continuous Data", + "class": "ContinuousData", + "args": {}, + "module": "modules.applications.qml.generative_modeling.data.data_handler.ContinuousData", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "PIT", + "class": "PIT", + "args": {}, + "module": "modules.applications.qml.generative_modeling.transformations.PIT", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "pandas", + "version": "2.2.2" + } + ], + "submodules": [ + { + "name": "CircuitCopula", + "class": "CircuitCopula", + "args": {}, + "module": "modules.applications.qml.generative_modeling.circuits.CircuitCopula", + "requirements": [ + { + "name": "scipy", + "version": "1.12.0" + } + ], + "submodules": [ + { + "name": "LibraryQiskit", + "class": "LibraryQiskit", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "LibraryPennylane", + "class": "LibraryPennylane", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "jax", + "version": "0.4.30" + }, + { + "name": "jaxlib", + "version": "0.4.30" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_ibm_runtime", + "version": "0.29.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + } + ] + } + ] + }, + { + "name": "MinMax", + "class": "MinMax", + "args": {}, + "module": "modules.applications.qml.generative_modeling.transformations.MinMax", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "CircuitStandard", + "class": "CircuitStandard", + "args": {}, + "module": "modules.applications.qml.generative_modeling.circuits.CircuitStandard", + "requirements": [], + "submodules": [ + { + "name": "LibraryQiskit", + "class": "LibraryQiskit", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "LibraryPennylane", + "class": "LibraryPennylane", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "jax", + "version": "0.4.30" + }, + { + "name": "jaxlib", + "version": "0.4.30" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_ibm_runtime", + "version": "0.29.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + } + ] + }, + { + "name": "CircuitCardinality", + "class": "CircuitCardinality", + "args": {}, + "module": "modules.applications.qml.generative_modeling.circuits.CircuitCardinality", + "requirements": [], + "submodules": [ + { + "name": "LibraryQiskit", + "class": "LibraryQiskit", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "LibraryPennylane", + "class": "LibraryPennylane", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "jax", + "version": "0.4.30" + }, + { + "name": "jaxlib", + "version": "0.4.30" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_ibm_runtime", + "version": "0.29.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + } + ] + } + ] + } + ] + }, + { + "name": "Discrete Data", + "class": "DiscreteData", + "args": {}, + "module": "modules.applications.qml.generative_modeling.data.data_handler.DiscreteData", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "CircuitCardinality", + "class": "CircuitCardinality", + "args": {}, + "module": "modules.applications.qml.generative_modeling.circuits.CircuitCardinality", + "requirements": [], + "submodules": [ + { + "name": "LibraryQiskit", + "class": "LibraryQiskit", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryQiskit", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "LibraryPennylane", + "class": "LibraryPennylane", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.LibraryPennylane", + "requirements": [ + { + "name": "pennylane", + "version": "0.37.0" + }, + { + "name": "pennylane-lightning", + "version": "0.38.0" + }, + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "jax", + "version": "0.4.30" + }, + { + "name": "jaxlib", + "version": "0.4.30" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "QGAN", + "class": "QGAN", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QGAN", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "torch", + "version": "2.2.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "CustomQiskitNoisyBackend", + "class": "CustomQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + }, + { + "name": "PresetQiskitNoisyBackend", + "class": "PresetQiskitNoisyBackend", + "args": {}, + "module": "modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend", + "requirements": [ + { + "name": "qiskit", + "version": "1.1.0" + }, + { + "name": "qiskit_ibm_runtime", + "version": "0.29.0" + }, + { + "name": "qiskit_aer", + "version": "0.15.0" + }, + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [ + { + "name": "QCBM", + "class": "QCBM", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.QCBM", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + }, + { + "name": "cma", + "version": "4.0.0" + }, + { + "name": "matplotlib", + "version": "3.7.5" + }, + { + "name": "tensorboard", + "version": "2.17.0" + }, + { + "name": "tensorboardX", + "version": "2.6.2.2" + } + ], + "submodules": [] + }, + { + "name": "Inference", + "class": "Inference", + "args": {}, + "module": "modules.applications.qml.generative_modeling.training.Inference", + "requirements": [ + { + "name": "numpy", + "version": "1.26.4" + } + ], + "submodules": [] + } + ] + } + ] + } + ] + } + ], + "requirements": [] + } + ] } \ No newline at end of file diff --git a/.settings/requirements_full.txt b/.settings/requirements_full.txt index 30c31969..ca23aca9 100644 --- a/.settings/requirements_full.txt +++ b/.settings/requirements_full.txt @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72809f65fb0919ae9ff2dcdc19151cc52a41adf9ee7f18900ce7ddc365197e69 -size 754 +oid sha256:aff2ebd79e8689ec510cb160da2c5567e898b525459176e54f4a1d28ef6ac8a9 +size 780 diff --git a/AUTHORS b/AUTHORS index 0aedfa1c..9397a0e5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,6 +5,8 @@ Philipp Ross (BMW Group) Marvin Erdmann (BMW Group) Andre Luckow (BMW Group) +Greshma Shaji (BMW Group) Florian Kiwit (BMW Group) Jürgen Schwitalla (Eviden) Chris van den Oetelaar (Capgemini) +Niklas Steinmann (Fraunhofer FOKUS) diff --git a/README.md b/README.md index 331af9cc..27698f2e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # QUARK: A Framework for Quantum Computing Application Benchmarking Quantum Computing Application Benchmark (QUARK) is a framework for orchestrating benchmarks of different industry applications on quantum computers. -QUARK supports various applications, like the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, or the robot path optimization in the PVC sealing use case. +QUARK supports various applications such as the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, robot path optimization in the PVC sealing use case +as well as new additions like the Maximum Independent Set (MIS), Set Cover Problem (SCP) and Auto Carrier Loading (ACL). It also features different solvers (e.g., simulated /quantum annealing and the quantum approximate optimization algorithm (QAOA)), quantum devices (e.g., IonQ and Rigetti), and simulators. It is designed to be easily extendable in all of its components: applications, mappings, solvers, devices, and any other custom modules. ## Publications Details about the motivations for the original framework can be found in the [accompanying QUARK paper from Finžgar et al](https://arxiv.org/abs/2202.03028). -Even though the architecture changes significantly from QUARK 1.0 to 2.0, the guiding principles still remain. The most recent publication from [Kiwit et al.](https://arxiv.org/abs/2308.04082) provides an updated overview of the functionalities and quantum machine learning features of QUARK 2.0. +Even though the architecture changes significantly from QUARK 1.0 to 2.0, the guiding principles still remain. The most recent publication from [Kiwit et al.](https://arxiv.org/abs/2308.04082) provides an updated overview of the functionalities and quantum machine learning features of QUARK 2.1. ## Documentation Documentation with a tutorial and developer guidelines can be found here: https://quark-framework.readthedocs.io/en/dev/. @@ -62,6 +63,17 @@ Content of the environment: In case you want to use custom modules files (for example, to use external modules from other repositories), you can still use the ```--modules``` option. You can find the documentation in the respective Read the Docs section. +## Git Large File Storage (LFS) +QUARK stores data and config files using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. + +### Installing Git LFS +Install Git LFS by following the instructions on [Git LFS](https://git-lfs.com/): + - On Linux/macOS + ```bash + git lfs install + ``` + - On Windows. Download and install Git LFS from the [Official page](https://git-lfs.com/) + ## Running a Benchmark ```bash @@ -84,8 +96,12 @@ Example run (You need to check at least one option with an ``X`` for the checkbo PVC SAT > TSP + ACL + MIS + SCP + GenerativeModeling -2023-03-21 09:18:36,440 [INFO] Import module modules.applications.optimization.TSP.TSP +2024-10-09 15:05:52,610 [INFO] Import module modules.applications.optimization.TSP.TSP [?] (Option for TSP) How many nodes does you graph need?: > [X] 3 [ ] 4 @@ -94,6 +110,7 @@ Example run (You need to check at least one option with an ``X`` for the checkbo [ ] 10 [ ] 14 [ ] 16 + [ ] Custom Range [?] What submodule do you want?: [ ] Ising @@ -102,25 +119,38 @@ Example run (You need to check at least one option with an ``X`` for the checkbo [ ] ReverseGreedyClassicalTSP [ ] RandomTSP -2023-03-21 09:18:49,563 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. -2023-03-21 09:18:49,566 [INFO] Submodule configuration finished -[?] How many repetitions do you want?: 1 -2023-03-21 09:18:50,577 [INFO] Import module modules.applications.optimization.TSP.TSP -2023-03-21 09:18:50,948 [INFO] Created Benchmark run directory /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50 -2023-03-21 09:18:51,025 [INFO] Codebase is based on revision 075201825fa71c24b5567e1290966081be7dbdc0 and has some uncommitted changes -2023-03-21 09:18:51,026 [INFO] Running backlog item 1/1, Iteration 1/1: -2023-03-21 09:18:51,388 [INFO] Route found: +2024-10-09 15:06:20,897 [INFO] Import module modules.solvers.GreedyClassicalTSP +2024-10-09 15:06:20,933 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. +2024-10-09 15:06:20,933 [INFO] Import module modules.devices.Local +2024-10-09 15:06:20,946 [INFO] Submodule configuration finished +[?] How many repetitions do you want?: 1P +2024-10-09 15:07:11,573 [INFO] Import module modules.applications.optimization.TSP.TSP +2024-10-09 15:07:11,573 [INFO] Import module modules.solvers.GreedyClassicalTSP +2024-10-09 15:07:11,574 [INFO] Import module modules.devices.Local +2024-10-09 15:07:12,194 [INFO] [INFO] Created Benchmark run directory /Users/user1/quark/benchmark_runs/tsp-2024-10-09-15-07-11 +2024-10-09 15:07:12,194 [INFO] Codebase is based on revision 1d9d17aad7ddff623ff51f62ca3ec2756621c345 and has no uncommitted changes +2024-10-09 15:07:12,195 [INFO] Running backlog item 1/1, Iteration 1/1: +2024-10-09 15:07:12,386 [INFO] Route found: Node 0 -> Node 2 -> Node 1 -2023-03-21 09:18:51,388 [INFO] All 3 nodes got visited -2023-03-21 09:18:51,388 [INFO] Total distance (without return): 727223.0 -2023-03-21 09:18:51,388 [INFO] Total distance (including return): 1436368.0 -2023-03-21 09:18:51,389 [INFO] -2023-03-21 09:18:51,389 [INFO] ============================================================ -2023-03-21 09:18:51,389 [INFO] -2023-03-21 09:18:51,389 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50/results.json -2023-03-21 09:18:51,746 [INFO] Finished creating plots. +2024-10-09 15:07:12,386 [INFO] All 3 nodes got visited +2024-10-09 15:07:12,386 [INFO] Total distance (without return): 727223.0 +2024-10-09 15:07:12,386 [INFO] Total distance (including return): 1436368.0 +2024-10-09 15:07:12,386 [INFO] +2024-10-09 15:07:12,386 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== +2024-10-09 15:07:12,387 [INFO] +2024-10-09 15:07:12,387 [INFO] =============== Run finished =============== +2024-10-09 15:07:12,387 [INFO] +2024-10-09 15:07:12,387 [INFO] ================================================================================ +2024-10-09 15:07:12,387 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 +2024-10-09 15:07:12,387 [INFO] ================================================================================ +2024-10-09 15:07:12,395 [INFO] +2024-10-09 15:07:12,400 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2024-10-09-15-07-11/results.json +2024-10-09 15:07:12,942 [INFO] Finished creating plots. +2024-10-09 15:07:12,943 [INFO] ============================================================ +2024-10-09 15:07:12,944 [INFO] ==================== QUARK finished! ==================== +2024-10-09 15:07:12,944 [INFO] ============================================================ ``` diff --git a/docs/analysis.rst b/docs/analysis.rst index 76090b68..b4f87043 100644 --- a/docs/analysis.rst +++ b/docs/analysis.rst @@ -11,7 +11,7 @@ Python Example import json # Load the results - filename = "benchmark_runs/tsp-2023-03-13-15-31-17/results.json" + filename = "benchmark_runs/tsp-2024-10-09-15-07-11/results.json" with open(filename) as f: results = json.load(f) diff --git a/docs/developer.rst b/docs/developer.rst index f493499d..9de0a112 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -85,33 +85,43 @@ Example for an application, which should reside under ``src/modules/applications .. code-block:: python - from modules.applications.Application import * + from modules.applications.Application import Application, Core from utils import start_time_measurement, end_time_measurement - - + from typing import TypedDict class MyApplication(Application): + """ + MyApplication is an example of how to create a new application module in the Quark framework. + """ def __init__(self): + """ + Initializes the MyApplication class. + """ super().__init__("MyApplication") self.submodule_options = ["submodule1"] @staticmethod def get_requirements() -> list: + """ + Returns a list of requirements for the application. + + :returns: A list of dictionaries containing the name and version of required packages + """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: + """ + Given an option string by the user, this returns a submodule. + :param option: String with the chosen submodule + :return: Module of type Core + :raises NotImplementedError: If the option is not recognized + """ if option == "submodule1": return Submodule1() @@ -119,7 +129,9 @@ Example for an application, which should reside under ``src/modules/applications raise NotImplementedError(f"Submodule Option {option} not implemented") def get_parameter_options(self): - + """ + Returns the parameter options for the application. + """ return { "size": { "values": [3, 4, 6, 8, 10, 14, 16], @@ -136,19 +148,34 @@ Example for an application, which should reside under ``src/modules/applications } class Config(TypedDict): + """ + A configuration dictionary for the application. + """ size: int factor: float - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: + """ + Generate data that gets passed to the next submodule. - # Generate data that gets passed to the next submodule + :param input_data: The input data for preprocessing + :param config: The configuration dictionary + :param **kwargs: Additional keyword arguments + :return: A tuple containing the preprocessed output and the time taken for preprocessing + """ start = start_time_measurement() output = self.generate_problem(config) return output, end_time_measurement(start) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: + """ + Processes data passed to this module from the submodule. - # Process data passed to this module from the submodule + :param input_data: The input data for postprocessing + :param config: The configuration dictionary + :param **kwargs: Additional keyword arguments + :returns: A tuple containing the processed solution quality and the time taken for evaluation + """ solution_validity, time_to_validation = self.validate( input_data) if solution_validity and processed_solution: @@ -162,16 +189,26 @@ Example for an application, which should reside under ``src/modules/applications return solution_validity, sum(time_to_validation, time_to_evaluation)) + def generate_problem(self, config: Config, iter_count: int) -> any: + """ + Generates a problem based on the given configuration. - - def generate_problem(self, config: Config, iter_count: int): - + :param config: The configuration dictionary + :param iter_count: The iteration count + :returns: The generated problem + """ size = config['size'] self.application = create_problem(size) return self.application - def validate(self, solution): + def validate(self, solution) -> tuple[bool, float]: + """ + Validates the solution. + + :param solution: The solution to validate + :return: A tuple containing the validity of the solution and the time taken for validation + """ start = start_time_measurement() # Check if solution is valid @@ -182,14 +219,27 @@ Example for an application, which should reside under ``src/modules/applications logging.info(f"Solution valid") return True, end_time_measurement(start) - def evaluate(self, solution): + def evaluate(self, solution) -> tuple[float, float]: + """ + Evaluates the solution. + + :param solution: The solution to evaluate + :return: A tuple containing the evaluation metric and the time taken for evaluation + """ start = start_time_measurement() evaluation_metric = calculate_metric(solution) return evaluation_metric, end_time_measurement(start) - def save(self, path, iter_count): + def save(self, path, iter_count) -> None: + """ + Saves the application state. + + :param path: The path where the application state should be saved + :param iter_count: The iteration count + :returns:None + """ save_your_application(self.application, f"{path}/application.txt") Writing an asynchronous Module diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 4c80bae5..f8a7420f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -2,7 +2,8 @@ QUARK: A Framework for Quantum Computing Application Benchmarking ================================================================= Quantum Computing Application Benchmark (QUARK) is a framework for orchestrating benchmarks of different industry applications on quantum computers. -QUARK supports various applications, like the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, or the robot path optimization in the PVC sealing use case. +QUARK supports various applications such as the traveling salesperson problem (TSP), the maximum satisfiability (MaxSAT) problem, robot path optimization in the PVC sealing use case +as well as new additions like the Maximum Independent Set (MIS), Set Cover Problem (SCP) and Auto Carrier Loading (ACL). It also features different solvers (e.g., simulated /quantum annealing and the quantum approximate optimization algorithm (QAOA)), quantum devices (e.g., IonQ and Rigetti), and simulators. It is designed to be easily extendable in all of its components: applications, mappings, solvers, devices, and any other custom modules. @@ -65,9 +66,22 @@ You can also visualize the contents of your QUARK environment: In case you want to use custom modules files (for example to use external modules from other repositories), you can still use the ``--modules`` option. You can find the documentation in the Dynamic Imports section. +Git Large File Storage (LFS) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +QUARK stores data and config files using **Git LFS**. If you are contributing to this project or cloning this repository, ensure that you have **Git LFS** installed and configured to manage large files effectively. + +Installing Git LFS +^^^^^^^^^^^^^^^^^^^ +Install Git LFS by following the instructions on `Git LFS `_ + - On Linux/macOS + :: + + git lfs install + + - On Windows. Download and install Git LFS from the `Official page `_ Running a Benchmark -~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~ .. code:: bash @@ -88,46 +102,64 @@ Example run (You need to check at least one option with an ``X`` for the checkbo (quark) % python src/main.py [?] What application do you want?: TSP - PVC - SAT - > TSP - - 2023-03-21 09:18:36,440 [INFO] Import module modules.applications.optimization.TSP.TSP + PVC + SAT + > TSP + ACL + MIS + SCP + GenerativeModeling + + 2024-10-09 15:05:52,610 [INFO] Import module modules.applications.optimization.TSP.TSP [?] (Option for TSP) How many nodes does you graph need?: - > [X] 3 - [ ] 4 - [ ] 6 - [ ] 8 - [ ] 10 - [ ] 14 - [ ] 16 + > [X] 3 + [ ] 4 + [ ] 6 + [ ] 8 + [ ] 10 + [ ] 14 + [ ] 16 + [ ] Custom Range [?] What submodule do you want?: - [ ] Ising - [ ] Qubo - > [X] GreedyClassicalTSP - [ ] ReverseGreedyClassicalTSP - [ ] RandomTSP - - 2023-03-21 09:18:49,563 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. - 2023-03-21 09:18:49,566 [INFO] Submodule configuration finished - [?] How many repetitions do you want?: 1 - 2023-03-21 09:18:50,577 [INFO] Import module modules.applications.optimization.TSP.TSP - 2023-03-21 09:18:50,948 [INFO] Created Benchmark run directory /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50 - 2023-03-21 09:18:51,025 [INFO] Codebase is based on revision 075201825fa71c24b5567e1290966081be7dbdc0 and has some uncommitted changes - 2023-03-21 09:18:51,026 [INFO] Running backlog item 1/1, Iteration 1/1: - 2023-03-21 09:18:51,388 [INFO] Route found: - Node 0 -> - Node 2 -> - Node 1 - 2023-03-21 09:18:51,388 [INFO] All 3 nodes got visited - 2023-03-21 09:18:51,388 [INFO] Total distance (without return): 727223.0 - 2023-03-21 09:18:51,388 [INFO] Total distance (including return): 1436368.0 - 2023-03-21 09:18:51,389 [INFO] - 2023-03-21 09:18:51,389 [INFO] ============================================================ - 2023-03-21 09:18:51,389 [INFO] - 2023-03-21 09:18:51,389 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2023-03-21-09-18-50/results.json - 2023-03-21 09:18:51,746 [INFO] Finished creating plots. + [ ] Ising + [ ] Qubo + > [X] GreedyClassicalTSP + [ ] ReverseGreedyClassicalTSP + [ ] RandomTSP + + 2024-10-09 15:06:20,897 [INFO] Import module modules.solvers.GreedyClassicalTSP + 2024-10-09 15:06:20,933 [INFO] Skipping asking for submodule, since only 1 option (Local) is available. + 2024-10-09 15:06:20,933 [INFO] Import module modules.devices.Local + 2024-10-09 15:06:20,946 [INFO] Submodule configuration finished + [?] How many repetitions do you want?: 1P + 2024-10-09 15:07:11,573 [INFO] Import module modules.applications.optimization.TSP.TSP + 2024-10-09 15:07:11,573 [INFO] Import module modules.solvers.GreedyClassicalTSP + 2024-10-09 15:07:11,574 [INFO] Import module modules.devices.Local + 2024-10-09 15:07:12,194 [INFO] [INFO] Created Benchmark run directory /Users/user1/quark/benchmark_runs/tsp-2024-10-09-15-07-11 + 2024-10-09 15:07:12,194 [INFO] Codebase is based on revision 1d9d17aad7ddff623ff51f62ca3ec2756621c345 and has no uncommitted changes + 2024-10-09 15:07:12,195 [INFO] Running backlog item 1/1, Iteration 1/1: + 2024-10-09 15:07:12,386 [INFO] Route found: + Node 0 -> + Node 2 -> + Node 1 + 2024-10-09 15:07:12,386 [INFO] All 3 nodes got visited + 2024-10-09 15:07:12,386 [INFO] Total distance (without return): 727223.0 + 2024-10-09 15:07:12,386 [INFO] Total distance (including return): 1436368.0 + 2024-10-09 15:07:12,386 [INFO] + 2024-10-09 15:07:12,386 [INFO] ==== Run backlog item 1/1 with 1 iterations - FINISHED:1 ==== + 2024-10-09 15:07:12,387 [INFO] + 2024-10-09 15:07:12,387 [INFO] =============== Run finished =============== + 2024-10-09 15:07:12,387 [INFO] + 2024-10-09 15:07:12,387 [INFO] ================================================================================ + 2024-10-09 15:07:12,387 [INFO] ====== Run 1 backlog items with 1 iterations - FINISHED:1 + 2024-10-09 15:07:12,387 [INFO] ================================================================================ + 2024-10-09 15:07:12,395 [INFO] + 2024-10-09 15:07:12,400 [INFO] Saving 1 benchmark records to /Users/user1/QUARK/benchmark_runs/tsp-2024-10-09-15-07-11/results.json + 2024-10-09 15:07:12,942 [INFO] Finished creating plots. + 2024-10-09 15:07:12,943 [INFO] ============================================================ + 2024-10-09 15:07:12,944 [INFO] ==================== QUARK finished! ==================== + 2024-10-09 15:07:12,944 [INFO] ============================================================ All used config files, logs and results are stored in a folder in the diff --git a/h origin HEAD b/h origin HEAD deleted file mode 100644 index 028c3ceb..00000000 --- a/h origin HEAD +++ /dev/null @@ -1,31 +0,0 @@ -* (HEAD detached from origin/Christian_ACL) - dev - main - noise_module - noise_module_backup - remotes/origin/Christian_ACL - remotes/origin/HEAD -> origin/main - remotes/origin/KnapsackRebased - remotes/origin/dev - remotes/origin/dev-qml-marwa - remotes/origin/dev_max_noise_implementation - remotes/origin/dev_qml_flo - remotes/origin/dev_qml_flo_qgan - remotes/origin/dev_qml_max - remotes/origin/dev_qml_max_transpile - remotes/origin/hybrid_jobs_aron_qaoa_solving - remotes/origin/jonas-nqar - remotes/origin/main - remotes/origin/new-version-qgan-marwa - remotes/origin/noise_module - remotes/origin/noise_module_no_ibm_runtime - remotes/origin/qgan - remotes/origin/testLint - remotes/origin/unitTests - remotes/origin/warm_starting - remotes/public/advertise_open_call - remotes/public/dev - remotes/public/legacy/1.0 - remotes/public/main - remotes/public/optionalRegenerate - remotes/public/transpile_option diff --git a/src/BenchmarkManager.py b/src/BenchmarkManager.py index cd543705..766f9b5d 100644 --- a/src/BenchmarkManager.py +++ b/src/BenchmarkManager.py @@ -21,7 +21,7 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import List, Dict, Optional +from typing import Optional import numpy as np @@ -30,7 +30,6 @@ from Plotter import Plotter from modules.Core import Core from utils import get_git_revision - from utils_mpi import get_comm comm = get_comm() @@ -48,17 +47,15 @@ class JobStatus(Enum): FAILED = 3 -def _prepend_instruction(result: tuple) -> tuple: +def _prepend_instruction(result: tuple) -> tuple[Instruction, tuple]: """ - If the given list does not contain an instruction as first entry a + If the given list does not contain an Instruction as first entry a PROCEED is inserted at position 0 such that it is guaranteed that - the first entry of the returned list is an INSTRUCTION with PROCEED + the first entry of the returned list is an Instruction with PROCEED as default. - :param result: the list to which the instruction is to be prepended - :type result: tuple - :return: the list with an INSTRUCTION as first entry - :rtype: tuple + :param result: The tuple to which the Instruction is to be prepended + :return: The tuple with an Instruction as first entry """ if isinstance(result[0], Instruction): return result @@ -66,29 +63,25 @@ def _prepend_instruction(result: tuple) -> tuple: return Instruction.PROCEED, *result -def postprocess(module_instance: Core, *args, **kwargs) -> tuple: +def postprocess(module_instance: Core, *args, **kwargs) -> tuple[Instruction, tuple]: """ Wraps module_instance.postprocess such that the first entry of the result list is guaranteed to be an Instruction. See _prepend_instruction. - :param module_instance: the QUARK module on which to call postprocess - :type module_instance: Core - :return: the result list of module_instance.postprocess with an Instruction as first entry. - :rtype: tuple + :param module_instance: The QUARK module on which to call postprocess + :return: The result list of module_instance.postprocess with an Instruction as first entry. """ result = module_instance.postprocess(*args, **kwargs) return _prepend_instruction(result) -def preprocess(module_instance: Core, *args, **kwargs) -> tuple: +def preprocess(module_instance: Core, *args, **kwargs) -> tuple[Instruction, tuple]: """ Wraps module_instance.preprocess such that the first entry of the result list is guaranteed to be an Instruction. See _prepend_instruction. - :param module_instance: the QUARK module on which to call preprocess - :type module_instance: Core - :return: the result list of module_instance.preprocess with an Instruction as first entry. - :rtype: tuple + :param module_instance: The QUARK module on which to call preprocess + :return: The result list of module_instance.preprocess with an Instruction as first entry. """ result = module_instance.preprocess(*args, **kwargs) return _prepend_instruction(result) @@ -104,9 +97,9 @@ class BenchmarkManager: def __init__(self, fail_fast: bool = False): """ - Constructor method + Constructor method. + :param fail_fast: Boolean whether a single failed benchmark run causes QUARK to fail - :type fail_fast: bool """ self.fail_fast = fail_fast self.application = None @@ -118,12 +111,13 @@ def __init__(self, fail_fast: bool = False): def load_interrupted_results(self) -> Optional[list]: """ - :return: the content of the results file from the QUARK run to be resumed or None. - :rtype: Optional[list] + Loads the interrupted results if available. + + :return: The content of the results file from the QUARK run to be resumed or None. """ if self.interrupted_results_path is None or not os.path.exists(self.interrupted_results_path): return None - with open(self.interrupted_results_path, encoding='utf-8') as results_file : + with open(self.interrupted_results_path, encoding='utf-8') as results_file: results = json.load(results_file) return results @@ -132,11 +126,7 @@ def _create_store_dir(self, store_dir: str = None, tag: str = None) -> None: Creates directory for a benchmark run. :param store_dir: Directory where the new directory should be created - :type store_dir: str - :param tag: prefix of the new directory - :type tag: str - :return: - :rtype: None + :param tag: Prefix of the new directory """ if store_dir is None: store_dir = Path.cwd() @@ -145,12 +135,19 @@ def _create_store_dir(self, store_dir: str = None, tag: str = None) -> None: Path(self.store_dir).mkdir(parents=True, exist_ok=True) self._set_logger() - def _resume_store_dir(self, store_dir) -> None: + def _resume_store_dir(self, store_dir: str) -> None: + """ + Resumes the existing store directory. + + :param store-dir: Directory to be resumed + """ self.store_dir = store_dir self._set_logger() def _set_logger(self) -> None: - # Also store the log file to the benchmark dir + """ + Sets up the logger to also write to a file in the store directory. + """ logger = logging.getLogger() formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") filehandler = logging.FileHandler(f"{self.store_dir}/logging.log") @@ -162,23 +159,19 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod """ Executes the benchmarks according to the given settings. - :param benchmark_config_manager: Instance of BenchmarkConfigManager class, where config is already set. - :type benchmark_config_manager: ConfigManager - :param app_modules: the list of application modules as specified in the application modules configuration. - :type app_modules: list of dict - :param store_dir: target directory to store the results of the benchmark (if you decided to store it) - :type store_dir: str - :param interrupted_results_path: result file from which the information for the interrupted jobs will be read. + :param benchmark_config_manager: Instance of BenchmarkConfigManager class, where config is already set + :param app_modules: The list of application modules as specified in the application modules configuration + :param store_dir: Target directory to store the results of the benchmark (if user decided to store it) + :param interrupted_results_path: Result file from which the information for the interrupted jobs will be read. If store_dir is None the parent directory of interrupted_results_path will be used as store_dir. - :type interrupted_results_path: str - :rtype: None """ self.interrupted_results_path = interrupted_results_path if interrupted_results_path and not store_dir: self._resume_store_dir(os.path.dirname(interrupted_results_path)) else: self._create_store_dir(store_dir, tag=benchmark_config_manager.get_config()["application"]["name"].lower()) + benchmark_config_manager.save(self.store_dir) benchmark_config_manager.load_config(app_modules) self.application = benchmark_config_manager.get_app() @@ -187,7 +180,6 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod logging.info(f"Created Benchmark run directory {self.store_dir}") benchmark_backlog = benchmark_config_manager.start_create_benchmark_backlog() - self.run_benchmark(benchmark_backlog, benchmark_config_manager.get_reps()) # Wait until all MPI processes have finished and save results on rank 0 @@ -196,15 +188,12 @@ def orchestrate_benchmark(self, benchmark_config_manager: ConfigManager, app_mod results = self._collect_all_results() self._save_as_json(results) - def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: disable=R0915 + def run_benchmark(self, benchmark_backlog: list, repetitions: int) -> None: # pylint: disable=R0915 """ Goes through the benchmark backlog, which contains all the benchmarks to execute. :param repetitions: Number of repetitions - :type repetitions: int :param benchmark_backlog: List with the benchmark items to run - :type benchmark_backlog: list - :return: """ git_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ) git_revision_number, git_uncommitted_changes = get_git_revision(git_dir) @@ -219,10 +208,12 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di with open(f"{path}/application_config.json", 'w') as filehandler: json.dump(backlog_item["config"], filehandler, indent=2) job_status_count = {} + for i in range(1, repetitions + 1): logging.info(f"Running backlog item {idx_backlog + 1}/{len(benchmark_backlog)}," f" Iteration {i}/{repetitions}:") - # getting information of interrupted jobs + + # Getting information of interrupted jobs job_info_with_meta_data = {} if interrupted_results: for entry in interrupted_results: @@ -231,6 +222,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di break job_info = job_info_with_meta_data['module'] if job_info_with_meta_data else {} quark_job_status_name = job_info.get("quark_job_status") + if quark_job_status_name in (JobStatus.FINISHED.name, JobStatus.FAILED.name): quark_job_status = JobStatus.FINISHED if quark_job_status_name == JobStatus.FINISHED.name \ else JobStatus.FAILED @@ -241,20 +233,21 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di continue try: - - self.benchmark_record_template = BenchmarkRecord(idx_backlog, - datetime.today().strftime('%Y-%m-%d-%H-%M-%S'), - git_revision_number, git_uncommitted_changes, - i, repetitions) + self.benchmark_record_template = BenchmarkRecord( + idx_backlog, + datetime.today().strftime('%Y-%m-%d-%H-%M-%S'), + git_revision_number, git_uncommitted_changes, + i, repetitions + ) self.application.metrics.set_module_config(backlog_item["config"]) - instruction, problem, preprocessing_time = preprocess(self.application, None, - backlog_item["config"], - store_dir=path, rep_count=i, - previous_job_info=job_info) + instruction, problem, preprocessing_time = preprocess( + self.application, None, backlog_item["config"], + store_dir=path, rep_count=i, previous_job_info=job_info + ) self.application.metrics.set_preprocessing_time(preprocessing_time) self.application.save(path, i) - postprocessing_time = 0. + postprocessing_time = 0.0 benchmark_record = self.benchmark_record_template.copy() if instruction == Instruction.PROCEED: instruction, processed_input, benchmark_record = \ @@ -269,10 +262,11 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di quark_job_status = JobStatus.INTERRUPTED else: quark_job_status = JobStatus.FINISHED - self.application.metrics.add_metric("quark_job_status", quark_job_status.name) + self.application.metrics.add_metric("quark_job_status", quark_job_status.name) self.application.metrics.set_postprocessing_time(postprocessing_time) self.application.metrics.validate() + if benchmark_record is not None: benchmark_record.append_module_record_left(deepcopy(self.application.metrics)) benchmark_records.append(benchmark_record) @@ -286,7 +280,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di logging.exception(f"Error during benchmark run: {error}", exc_info=True) quark_job_status = JobStatus.FAILED if job_info: - # restore results/infos from previous run + # Restore results/infos from previous run benchmark_records.append(job_info) if self.fail_fast: raise @@ -319,7 +313,7 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di if break_flag: break - # print overall status information + # Log overall status information status_report = " ".join([f"{status.name}:{count}" for status, count in job_status_count_total.items()]) logging.info(80 * "=") logging.info(f"====== Run {len(benchmark_backlog)} backlog items " @@ -334,30 +328,23 @@ def run_benchmark(self, benchmark_backlog: list, repetitions: int): # pylint: di rel_path = self.store_dir logging.info("====== There are interrupted jobs. You may resume them by running QUARK with") logging.info(f"====== --resume-dir={rel_path}") - logging.info(80*"=") + logging.info(80 * "=") logging.info("") # pylint: disable=R0917 def traverse_config(self, module: dict, input_data: any, path: str, rep_count: int, previous_job_info: - dict = None) -> (any, BenchmarkRecord): + dict = None) -> tuple[Instruction, any, BenchmarkRecord]: """ Executes a benchmark by traversing down the initialized config recursively until it reaches the end. Then traverses up again. Once it reaches the root/application, a benchmark run is finished. :param module: Current module - :type module: dict - :param input_data: The input data needed to execute the current module. - :type input_data: any + :param input_data: The input data needed to execute the current module :param path: Path in case the modules want to store anything - :type path: str :param rep_count: The iteration count - :type rep_count: int - :param previous_job_info: information about previous job - :type previous_job_info: dict - :return: tuple with the output of this step and the according BenchmarkRecord - :rtype: tuple(any, BenchmarkRecord) + :param previous_job_info: Information about previous job + :return: Tuple with the output of this step and the according BenchmarkRecord """ - # Only the value of the dict is needed (dict has only one key) module = module[next(iter(module))] module_instance: Core = module["instance"] @@ -365,45 +352,49 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i submodule_job_info = None if previous_job_info and previous_job_info.get("submodule"): assert module['name'] == previous_job_info["submodule"]["module_name"], \ - f"asyncronous job info given, but no information about module {module['name']} stored in it" #TODO!! + f"asyncronous job info given, but no information about module {module['name']} stored in it" # TODO if 'submodule' in previous_job_info and previous_job_info['submodule']: submodule_job_info = previous_job_info['submodule'] module_instance.metrics.set_module_config(module["config"]) - instruction, module_instance.preprocessed_input, preprocessing_time\ - = preprocess(module_instance, input_data, - module["config"], - store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info) + instruction, module_instance.preprocessed_input, preprocessing_time = preprocess( + module_instance, input_data, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info + ) module_instance.metrics.set_preprocessing_time(preprocessing_time) output = None benchmark_record = self.benchmark_record_template.copy() postprocessing_time = 0.0 + if instruction == Instruction.PROCEED: # Check if end of the chain is reached if not module["submodule"]: # If we reach the end of the chain we create the benchmark record, fill it and then pass it up - instruction, module_instance.postprocessed_input, postprocessing_time = \ - postprocess( module_instance, - module_instance.preprocessed_input, - module["config"], store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info) + instruction, module_instance.postprocessed_input, postprocessing_time = postprocess( + module_instance, + module_instance.preprocessed_input, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info + ) output = module_instance.postprocessed_input else: - instruction, processed_input, benchmark_record = self.traverse_config(module["submodule"], - module_instance.preprocessed_input, path, - rep_count, previous_job_info=submodule_job_info) + instruction, processed_input, benchmark_record = self.traverse_config( + module["submodule"], + module_instance.preprocessed_input, path, + rep_count, previous_job_info=submodule_job_info + ) if instruction == Instruction.PROCEED: - instruction, module_instance.postprocessed_input, postprocessing_time = \ - postprocess(module_instance, processed_input, - module["config"], - store_dir=path, - rep_count=rep_count, - previous_job_info=submodule_job_info) + instruction, module_instance.postprocessed_input, postprocessing_time = postprocess( + module_instance, processed_input, + module["config"], store_dir=path, + rep_count=rep_count, + previous_job_info=submodule_job_info + ) output = module_instance.postprocessed_input else: output = processed_input @@ -414,12 +405,11 @@ def traverse_config(self, module: dict, input_data: any, path: str, rep_count: i return instruction, output, benchmark_record - def _collect_all_results(self) -> List[Dict]: + def _collect_all_results(self) -> list[dict]: """ Collect all results from the multiple results.json. - :return: list of dicts with results - :rtype: List[Dict] + :return: List of dicts with results """ results = [] for filename in glob.glob(f"{self.store_dir}/**/results.json"): @@ -431,6 +421,11 @@ def _collect_all_results(self) -> List[Dict]: return results def _save_as_json(self, results: list) -> None: + """ + Saves benchmark results to a JSON file. + + :param results: Benchmark results to be saved + """ logging.info(f"Saving {len(results)} benchmark records to {self.store_dir}/results.json") with open(f"{self.store_dir}/results.json", 'w') as filehandler: json.dump(results, filehandler, indent=2) @@ -439,9 +434,7 @@ def summarize_results(self, input_dirs: list) -> None: """ Helper function to summarize multiple experiments. - :param input_dirs: list of directories - :type input_dirs: list - :rtype: None + :param input_dirs: List of directories """ self._create_store_dir(tag="summary") logging.info(f"Summarizing {len(input_dirs)} benchmark directories") @@ -454,11 +447,8 @@ def load_results(self, input_dirs: list = None) -> list: Load results from one or more results.json files. :param input_dirs: If you want to load more than 1 results.json (default is just 1, the one from the experiment) - :type input_dirs: list - :return: a list - :rtype: list + :return: A list """ - if input_dirs is None: input_dirs = [self.store_dir] @@ -473,7 +463,7 @@ def load_results(self, input_dirs: list = None) -> list: class NumpyEncoder(json.JSONEncoder): """ - Encoder that is used for json.dump(...) since numpy value items in dictionary might cause problems + Encoder that is used for json.dump(...) since numpy value items in dictionary might cause problems. """ def default(self, o: any): diff --git a/src/BenchmarkRecord.py b/src/BenchmarkRecord.py index 7016977e..d822c806 100644 --- a/src/BenchmarkRecord.py +++ b/src/BenchmarkRecord.py @@ -22,26 +22,22 @@ class BenchmarkRecord: """ - The BenchmarkRecord class contains all the Metric instances and additional general information generated by a single - benchmark run. + The BenchmarkRecord class contains all the Metric instances and additional general information + generated by a single benchmark run. """ # pylint: disable=R0917 def __init__(self, benchmark_backlog_item_number: int, timestamp: str, git_revision_number: str, git_uncommitted_changes: str, repetition: int, total_repetitions: int): """ + Constructor method for BenchmarkRecord. + :param benchmark_backlog_item_number: Number of the item in the benchmark backlog - :type benchmark_backlog_item_number: int :param timestamp: Timestamp of the benchmark run - :type timestamp: str :param git_revision_number: Git revision number during the benchmark run - :type git_revision_number: str :param git_uncommitted_changes: Indication if there were uncommitted changes during the benchmark run - :type git_uncommitted_changes: str :param repetition: Number of current repetitions of the benchmark run - :type repetition: int :param total_repetitions: Number of total repetitions of the benchmark run - :type total_repetitions: int """ self.benchmark_backlog_item_number = benchmark_backlog_item_number self.timestamp = timestamp @@ -54,48 +50,38 @@ def __init__(self, benchmark_backlog_item_number: int, timestamp: str, git_revis self.linked_list_metrics = deque() @final - def append_module_record_right(self, module_record: Metrics): + def append_module_record_right(self, module_record: Metrics) -> None: """ - Adds Metrics instance to the end of the linked list + Adds Metrics instance to the end of the linked list. :param module_record: Metrics instance which should be appended to the end of the linked list - :type: Metrics - :rtype: None """ self.linked_list_metrics.append(module_record) @final def append_module_record_left(self, module_record: Metrics) -> None: """ - Adds Metrics instance to the beginning of the linked list + Adds Metrics instance to the beginning of the linked list. :param module_record: Metrics instance which should be appended to the beginning of the linked list - :type module_record: Metrics - :rtype: None """ self.linked_list_metrics.appendleft(module_record) @final def sum_up_times(self) -> None: """ - Sums up the recording timings - - :rtype: None + Sums up the recording timings. """ - self.total_time = 0.0 - for item in self.linked_list_metrics: - self.total_time += item.total_time + self.total_time = sum(item.total_time for item in self.linked_list_metrics) @final def hash_config(self, llist: deque) -> dict: """ Recursively traverses through linked list and returns a dictionary with the next module's name, config, - and subsequent submodule(s) + and subsequent submodule(s). :param llist: Linked list - :type llist: deque :return: Dictionary with the name and config of the first module in the linked list and subsequent submodule(s) - :rtype: dict """ next_item: Metrics = llist.popleft() return { @@ -111,7 +97,6 @@ def start_hash_config(self) -> int: Then generates hash with it. :return: Hash of the benchmark run config - :rtype: int """ list_copy = deepcopy(self.linked_list_metrics) # Hash assumes that all keys are strings! @@ -120,14 +105,11 @@ def start_hash_config(self) -> int: @final def linked_list_to_dict(self, llist: deque, module_level: int = 0) -> dict: """ - Recursively traverses through linked list and adds the items of the Metrics objects to one single dictionary + Recursively traverses through linked list and adds the items of the Metrics objects to one single dictionary. :param llist: Linked list - :type llist: deque :param module_level: Current level in chain (starts at 0) - :type module_level: int :return: Dictionary with the module, its level, and its submodule(s) - :rtype: dict """ next_item: Metrics = llist.popleft() return { @@ -139,11 +121,10 @@ def linked_list_to_dict(self, llist: deque, module_level: int = 0) -> dict: @final def start_linked_list_to_dict(self) -> dict: """ - Helper function to start linked_list_to_dict function, which merges the various Metrics objects - to one dictionary + Helper function to start linked_list_to_dict function which merges the various Metrics objects + to one dictionary. :return: Resulting dictionary of linked_list_to_dict - :rtype: dict """ list_copy = deepcopy(self.linked_list_metrics) return self.linked_list_to_dict(list_copy) @@ -151,11 +132,10 @@ def start_linked_list_to_dict(self) -> dict: @final def get(self) -> dict: """ - Returns a dictionary containing all benchmark information and a nested dictionary, in which each level - contains the metrics of the respective module + Returns a dictionary containing all benchmark information and a nested dictionary in which each level + contains the metrics of the respective module. :return: Dictionary containing all the records of the benchmark - :rtype: dict """ return { "benchmark_backlog_item_number": self.benchmark_backlog_item_number, @@ -171,11 +151,11 @@ def get(self) -> dict: } @final - def copy(self) -> any: + def copy(self) -> "BenchmarkRecord": """ - Returns a copy of itself + Returns a copy of itself. + :return: Return copy of itself - :rtype: BenchmarkRecord """ return deepcopy(self) @@ -186,10 +166,12 @@ class BenchmarkRecordStored: It is a simple wrapper with the purpose to provide the same interface to the BenchmarkManager as the BenchmarkRecord does. """ + def __init__(self, record: dict): """ + Constructor method for BenchmarkRecordStored. + :param record: the record as dictionary - :type record: dict """ self.record = record @@ -198,14 +180,11 @@ def get(self) -> dict: Simply returns the dictionary as given to the constructor. :return: Dictionary as given to the constructor - :rtype: dict """ return self.record def sum_up_times(self) -> None: """ Dummy implementation which does nothing. - - :rtype: None """ pass diff --git a/src/ConfigManager.py b/src/ConfigManager.py index 4a68f0b7..cec96b93 100644 --- a/src/ConfigManager.py +++ b/src/ConfigManager.py @@ -53,7 +53,6 @@ class ConfigManager: """ A class responsible for generating/loading QUARK benchmark configs. Loading includes instantiation of the various modules specified in the config. - """ def __init__(self): @@ -66,8 +65,6 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: necessary to run the benchmark. :param app_modules: List of application modules as specified in the application modules configuration - :type app_modules: List of dict - :rtype: None """ application_answer = inquirer.prompt([inquirer.List('application', message="What application do you want?", @@ -79,12 +76,10 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: self.application = _get_instance_with_sub_options(app_modules, app_name) application_config = self.application.get_parameter_options() - application_config = ConfigManager._query_for_config( application_config, f"(Option for {application_answer['application']})") submodule_options = self.application.get_available_submodule_options() - submodule_answer = checkbox(key='submodules', message="What submodule do you want?", choices=submodule_options) @@ -102,28 +97,21 @@ def generate_benchmark_configs(self, app_modules: list[dict]) -> None: repetitions_answer = inquirer.prompt( [inquirer.Text('repetitions', message="How many repetitions do you want?", - validate=lambda _, x: re.match("\\d", x), - default=1 - )]) + validate=lambda _, x: re.match("\\d", x), default=1)]) self.config["repetitions"] = int(repetitions_answer["repetitions"]) def query_module(self, module: Core, module_friendly_name: str) -> ConfigModule: """ - Recursive function which queries every module and its submodule until end is reached + Recursive function which queries every module and its submodule until end is reached. :param module: Module instance - :type module: Core :param module_friendly_name: Name of the module - :type module_friendly_name: str :return: Config module with the choices of the user - :rtype: ConfigModule """ - module_config = module.get_parameter_options() module_config = ConfigManager._query_for_config(module_config, f"(Option for {module.__class__.__name__})") available_submodules = module.get_available_submodule_options() - submodule_answer = checkbox(key='submodules', message="What submodule do you want?", choices=available_submodules) @@ -132,7 +120,6 @@ def query_module(self, module: Core, module_friendly_name: str) -> ConfigModule: "config": module_config, "submodules": [self.query_module(module.get_submodule(sm), sm) for sm in submodule_answer["submodules"]] - } def set_config(self, config: BenchmarkConfig) -> None: @@ -140,23 +127,17 @@ def set_config(self, config: BenchmarkConfig) -> None: In case the user provides a config file, this function is used to set the config. :param config: Valid config file - :type config: BenchmarkConfig - :rtype: None """ - if ConfigManager.is_legacy_config(config): config = ConfigManager.translate_legacy_config(config) - self.config = config @staticmethod def is_legacy_config(config: dict) -> bool: """ - Checks if a QUARK 1 config was provided + Checks if a QUARK 1 config was provided. - :param config: Valid config file - :type config: dict - :rtype: bool + :param config: Valid config file :return: True if provided config is QUARK 1 config """ if "mapping" in config.keys(): @@ -168,14 +149,11 @@ def is_legacy_config(config: dict) -> bool: @staticmethod def translate_legacy_config(config: dict) -> BenchmarkConfig: """ - Translates the QUARK 1 config format to QUARK 2 format + Translates the QUARK 1 config format to QUARK 2 format. :param config: QUARK 1 config - :type config: dict - :return: Translated Config - :rtype: BenchmarkConfig + :return: Translated config """ - logging.info("Trying to translate QUARK 1 config to QUARK 2 config format") try: translated_config = {key: config[key] for key in ["application", "repetitions"]} @@ -202,19 +180,13 @@ def translate_legacy_config(config: dict) -> BenchmarkConfig: @staticmethod def translate_legacy_config_helper(config_part: dict, module_key: str) -> list: """ - Helper function for translate_legacy_config, which translates the QUARK 1 config format to QUARK 2 format + Helper function for translate_legacy_config, which translates the QUARK 1 config format to QUARK 2 format. - :param config_part: part of a config - :type config_part: dict + :param config_part: Part of a config :param module_key: Module key: mapping, solver or device - :type module_key: str - :return: translated config_part - :rtype: list + :return: Translated config_part """ - - next_module_key = None - if module_key.lower() == "solver": - next_module_key = "device" + next_module_key = "device" if module_key.lower() == "solver" else None result = [] for item in config_part[module_key]: @@ -229,17 +201,14 @@ def translate_legacy_config_helper(config_part: dict, module_key: str) -> list: return result - def load_config(self, app_modules: list[dict]): + def load_config(self, app_modules: list[dict]) -> None: """ - Uses the config to generate all class instances needed to run the benchmark + Uses the config to generate all class instances needed to run the benchmark. :param app_modules: List of application modules as specified in the application modules configuration - :type app_modules: List of dict - :rtype: None """ self.application = _get_instance_with_sub_options(app_modules, self.config["application"]["name"]) - self.config["application"].update({"instance": self.application, "submodules": [ConfigManager.initialize_module_classes(self.application, c) for c in self.config["application"]["submodules"]]}) @@ -247,70 +216,58 @@ def load_config(self, app_modules: list[dict]): @staticmethod def initialize_module_classes(parent_module: Core, config: ConfigModule) -> ConfigModule: """ - Recursively initializes all instances of the required modules and their submodules for a given config + Recursively initializes all instances of the required modules and their submodules for a given config. :param parent_module: Class of the parent module - :type parent_module: Core :param config: Uninitialized config module - :type config: ConfigModule :return: Config with instances - :rtype: ConfigModule """ - module_instance = parent_module.get_submodule(config["name"]) config.update({"instance": module_instance, - "submodules": [ConfigManager.initialize_module_classes(module_instance, c) for c in - config["submodules"]]} - ) + "submodules": [ConfigManager.initialize_module_classes(module_instance, c) + for c in config["submodules"]]}) return config def get_config(self) -> BenchmarkConfig: """ - Returns the config + Returns the config. - :return: Returns the config - :rtype: BenchmarkConfig + :return: config """ return self.config def get_app(self) -> Application: """ - Returns instance of the application + Returns instance of the application. :return: Instance of the application - :rtype: Application """ return self.config["application"]["instance"] def get_reps(self) -> int: """ - Returns number of repetitions specified in config + Returns number of repetitions specified in config. :return: Number of repetitions - :rtype: int """ return self.config["repetitions"] def start_create_benchmark_backlog(self) -> list: """ - Helper function to kick off the creation of the benchmark backlog + Helper function to kick off the creation of the benchmark backlog. :return: List with all benchmark items - :rtype: list """ return ConfigManager.create_benchmark_backlog(self.config["application"]) @staticmethod def create_benchmark_backlog(module: ConfigModule) -> list: """ - Recursive function which splits up the loaded config into single benchmark runs + Recursive function which splits up the loaded config into single benchmark runs. :param module: ConfigModule - :type module: any :return: List with all benchmark items - :rtype: list """ - items = [] if len(module["config"].items()) > 0: @@ -328,59 +285,48 @@ def create_benchmark_backlog(module: ConfigModule) -> list: "name": module["name"], "instance": module["instance"], "config": single_config, - "submodule": { - submodule["name"]: item - } + "submodule": {submodule["name"]: item} }) else: items.append({ "name": module["name"], "instance": module["instance"], "config": single_config, - "submodule": { - } + "submodule": {} }) return items - def save(self, store_dir: str): + def save(self, store_dir: str) -> None: """ - Saves the config as a YAML file + Saves the config as a YAML file. :param store_dir: Directory in which the file should be stored - :type store_dir: str - :rtype: None """ with open(f"{store_dir}/config.yml", 'w') as filehandler: yaml.dump(self.config, filehandler) def print(self) -> None: """ - Prints the config - :rtype: None + Prints the config. """ print(yaml.dump(self.config)) @staticmethod def _query_for_config(param_opts: dict, prefix: str = "") -> dict: """ - For a given module config, queries users in an interactive mode, which of the options they would like to - include in the final benchmark config + For a given module config, queries users in an interactive mode which of the options they would like to + include in the final benchmark config. :param param_opts: Dictionary containing the options for a parameter including a description - :type param_opts: dict :param prefix: Prefix string, which is attached when interacting with the user - :type prefix: str - :return: Dictionary containing the decisions of the user on what to include in the benchmark. - :rtype: dict + :return: Dictionary containing the decisions of the user on what to include in the benchmark """ config = {} for key, config_answer in param_opts.items(): if config_answer.get("if"): - key_in_cond = config_answer.get("if")["key"] dependency = param_opts.get(key_in_cond) - consistent = False err_msg = None if dependency is None: @@ -400,26 +346,20 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: # When there is only 1 value to choose from skip the user input for now values = config_answer['values'] print(f"{prefix} {config_answer['description']}: {config_answer['values'][0]}") - elif config_answer.get('exclusive', False): answer = inquirer.prompt( - [inquirer.List(key, - message=f"{prefix} {config_answer['description']}", - choices=config_answer['values'] - )]) + [inquirer.List(key, message=f"{prefix} {config_answer['description']}", + choices=config_answer['values'])]) values = (answer[key],) else: - choices = [*config_answer['values'], "Custom Input"] if (config_answer.get("custom_input") and config_answer["custom_input"]) \ else config_answer['values'] - if config_answer.get("allow_ranges") and config_answer["allow_ranges"]: choices.append("Custom Range") - answer = checkbox(key=key, - message=f"{prefix} {config_answer['description']}", - # Add custom_input if it is specified in the parameters - choices=choices) + + # Add custom_input if it is specified in the parameters + answer = checkbox(key=key, message=f"{prefix} {config_answer['description']}", choices=choices) values = answer[key] if "Custom Input" in values: @@ -439,7 +379,6 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: "this input is done!)"), inquirer.Text('step', message=f"What are the steps of your range for {key}? (No validation of " "this input is done!)")]) - values.remove("Custom Range") values.extend(np.arange(float(range_answer["start"]), float(range_answer["stop"]), float(range_answer["step"]))) @@ -451,23 +390,19 @@ def _query_for_config(param_opts: dict, prefix: str = "") -> dict: # with each of the user selected values as argument. # Note that the stored config file will contain the processed values. values = [config_answer["postproc"](v) for v in values] + config[key] = values return config def create_tree_figure(self, store_dir: str) -> None: """ - Visualizes the benchmark as a graph (experimental feature) + Visualizes the benchmark as a graph (experimental feature). :param store_dir: Directory where the file should be stored - :type store_dir: str - :rtype: None """ - graph = nx.DiGraph() - ConfigManager._create_tree_figure_helper(graph, self.config["application"]) - nx.draw(graph, with_labels=True, pos=nx.spectral_layout(graph), node_shape="s") plt.savefig(f"{store_dir}/BenchmarkGraph.png", format="PNG") plt.clf() @@ -475,15 +410,11 @@ def create_tree_figure(self, store_dir: str) -> None: @staticmethod def _create_tree_figure_helper(graph: nx.Graph, config: ConfigModule) -> None: """ - Helper function for create_tree_figure that traverses the config recursively + Helper function for create_tree_figure that traverses the config recursively. - :param graph: networkx Graph - :type graph: networkx.Graph - :param config: benchmark config - :type config: dict - :rtype: None + :param graph: Networkx Graph + :param config: Benchmark config """ - if config: key = config["name"] if "submodules" in config and config["submodules"]: diff --git a/src/Installer.py b/src/Installer.py index f2365d18..95fe0d4f 100644 --- a/src/Installer.py +++ b/src/Installer.py @@ -29,8 +29,8 @@ class Installer: """ - Installer class that can be used by the user to install certain QUARK modules and also return the required python - packages for the demanded modules + Installer class that can be used by the user to install certain QUARK modules and also return the required Python + packages for the demanded modules. """ def __init__(self): @@ -47,7 +47,7 @@ def __init__(self): {"name": "MIS", "class": "MIS", "module": "modules.applications.optimization.MIS.MIS"}, {"name": "SCP", "class": "SCP", "module": "modules.applications.optimization.SCP.SCP"}, {"name": "GenerativeModeling", "class": "GenerativeModeling", - "module": "modules.applications.QML.generative_modeling.GenerativeModeling"} + "module": "modules.applications.qml.generative_modeling.GenerativeModeling"} ] self.core_requirements = [ @@ -64,14 +64,10 @@ def __init__(self): def configure(self, env_name="default") -> None: """ - Configures a new QUARK environment or overwrites an existing one + Configures a new QUARK environment or overwrites an existing one. :param env_name: Name of the env to configure - :type env_name: str - :return: - :rtype: None """ - configured_envs = self.check_for_configs() if env_name in configured_envs: @@ -88,8 +84,7 @@ def configure(self, env_name="default") -> None: chosen_config_type = inquirer.prompt([ inquirer.List("config", message="Do you want to use the default configuration or a custom environment?", - choices=["Default", "Custom"], - )])["config"] + choices=["Default", "Custom"])])["config"] logging.info(f"You chose {chosen_config_type}") module_db = self.get_module_db() @@ -117,52 +112,44 @@ def configure(self, env_name="default") -> None: activate_answer = inquirer.prompt([ inquirer.List("activate", message="Do you want to activate the QUARK module environment?", - choices=["Yes", "No"], - )])["activate"] + choices=["Yes", "No"])])["activate"] if activate_answer == "Yes": self.set_active_env(env_name) def check_for_configs(self) -> list: """ - Checks if QUARK is already configured and if yes, which environments + Checks if QUARK is already configured and if yes, which environments. :return: Returns the configured QUARK envs in a list - :rtype: list """ return list(p.stem for p in Path(self.envs_dir).glob("*.json")) def set_active_env(self, name: str) -> None: """ - Sets active env to active_env.json + Sets the active env to active_env.json. :param name: Name of the env - :type name: str - :return: - :rtype: None """ self._check_if_env_exists(name) with open(f"{self.settings_dir}/active_env.json", "w") as jsonFile: - data = {"name": name} - json.dump(data, jsonFile, indent=2) + json.dump({"name": name}, jsonFile, indent=2) logging.info(f"Set active QUARK module environment to {name}") def check_active_env(self) -> bool: """ - Checks if .settings/active_env.json exists + Checks if .settings/active_env.json exists. :return: True if active_env.json exists - :rtype: bool """ return Path(f"{self.settings_dir}/active_env.json").is_file() def get_active_env(self) -> str: """ - Returns the current active environment + Returns the current active environment. :return: Returns the name of the active env - :rtype: str """ if not self.check_active_env(): logging.warning("No active QUARK module environment found, using default") @@ -176,12 +163,10 @@ def get_active_env(self) -> str: def get_env(self, name: str) -> list[dict]: """ - Loads the env from file and returns it + Loads the env from file and returns it. :param name: Name of the env - :type name: dict :return: Returns the modules of the env - :rtype: list[dict] """ file = f"{self.envs_dir}/{name}.json" self._check_if_env_exists(name) @@ -200,12 +185,10 @@ def get_env(self, name: str) -> list[dict]: def _check_if_env_exists(self, name: str) -> str: """ - Checks if a given env exists, returns the location of the associated JSON file and raises an error otherwise + Checks if a given env exists, returns the location of the associated JSON file and raises an error otherwise. :param name: Name of the env - :type name: str :return: Returns location of the JSON file associated with the env if it exists - :rtype: str """ file = f"{self.envs_dir}/{name}.json" if not Path(file).is_file(): @@ -214,16 +197,11 @@ def _check_if_env_exists(self, name: str) -> str: def save_env(self, env: dict, name: str) -> None: """ - Saves a created env to a file with the name of choice + Saves a created env to a file with the name of choice. :param env: Env which should be saved - :type env: dict :param name: Name of the env - :type name: str - :return: - :rtype: None """ - with open(f"{self.envs_dir}/{name}.json", "w") as jsonFile: json.dump(env, jsonFile, indent=2) @@ -231,14 +209,11 @@ def save_env(self, env: dict, name: str) -> None: def start_query_user(self, module_db: dict) -> dict: """ - Queries the user which applications and submodules to include + Queries the user which applications and submodules to include. :param module_db: module_db file - :type module_db: dict :return: Returns the module_db with selected (sub)modules - :rtype: dict """ - answer_apps = checkbox("apps", "Which application would you like to include?", [m["name"] for m in module_db["modules"]])["apps"] @@ -254,13 +229,8 @@ def query_user(self, submodules: dict, name: str) -> None: Queries the user which submodules to include :param submodules: Submodules for the module - :type submodules: dict :param name: Name of the module - :type name: str - :return: - :rtype: None """ - if submodules["submodules"]: answer_submodules = \ checkbox("submodules", f"Which submodule would you like to include for {name}?", @@ -272,20 +242,16 @@ def query_user(self, submodules: dict, name: str) -> None: def get_module_db(self) -> dict: """ - Returns the module database that contains all module possibilities + Returns the module database that contains all module possibilities. :return: Module Database - :rtype: dict """ with open(f"{self.settings_dir}/module_db.json", "r") as filehandler: return json.load(filehandler) def create_module_db(self) -> None: """ - Creates module database by automatically going through the available submodules for each module - - :return: - :rtype: None + Creates module database by automatically going through the available submodules for each module. """ logging.info("Creating Module Database") @@ -323,23 +289,15 @@ def create_module_db(self) -> None: @staticmethod def _create_module_db_helper(module: Core, name: str) -> dict: """ - Recursive helper function for create_module_db + Recursive helper function for create_module_db. - :param module: module - :type module: Core + :param module: Module instance :param name: Name of the module - :type name: str - :return: module dict - :rtype: dict + :return: Module dict """ - return { "name": name, "class": module.__class__.__name__, - # TODO Verify the following really works as intended - # Since some modules are initialized with parameters in their constructor, we need to check what these - # parameters were. Hence we check whether any parameters in the class instance match the one from - # the constructor. "args": {k: v for k, v in module.__dict__.items() if k in inspect.signature(module.__init__).parameters.keys()}, "module": module.__module__, @@ -350,12 +308,10 @@ def _create_module_db_helper(module: Core, name: str) -> dict: def get_module_db_build_number(self) -> int: """ - Returns the build number of the module_db + Returns the build number of the module_db. :return: Returns the build number of the module_db if it exists, otherwise 0 - :rtype: int """ - if Path(f"{self.settings_dir}/module_db.json").is_file(): module_db = self.get_module_db() return module_db["build_number"] @@ -364,14 +320,11 @@ def get_module_db_build_number(self) -> int: def collect_requirements(self, env: list[dict]) -> dict: """ - Collects requirements of the different modules in the given env file + Collects requirements of the different modules in the given env file. - :param env: env file - :type env: list[dict] + :param env: Environment configuration :return: Collected requirements - :rtype: dict """ - requirements: list[dict] = self.core_requirements for app in env: requirements.extend(Installer._collect_requirements_helper(app)) @@ -402,14 +355,11 @@ def collect_requirements(self, env: list[dict]) -> dict: @staticmethod def _collect_requirements_helper(module: dict) -> list[dict]: """ - Helper function for collect_requirements_helper that recursively checks modules for requirements + Helper function for collect_requirements_helper that recursively checks modules for requirements. - :param module: module dict - :type module: dict + :param module: Module dict :return: List of dicts with the requirements - :rtype: list[dict] """ - requirements = module["requirements"] for submodule in module["submodules"]: requirements.extend(Installer._collect_requirements_helper(submodule)) @@ -418,16 +368,11 @@ def _collect_requirements_helper(module: dict) -> list[dict]: def create_conda_file(self, requirements: dict, name: str, directory: str = None) -> None: """ - Creates conda yaml file based on the requirements + Creates conda yaml file based on the requirements. :param requirements: Collected requirements - :type requirements: dict :param name: Name of the conda env - :type name: str - :param directory: Directory where the file should be saved. If None self.envs_dir will be taken - :type directory: str - :return: - :rtype: None + :param directory: Directory where the file should be saved. If None self.envs_dir will be taken. """ if directory is None: directory = self.envs_dir @@ -449,16 +394,11 @@ def create_conda_file(self, requirements: dict, name: str, directory: str = None def create_req_file(self, requirements: dict, name: str, directory: str = None) -> None: """ - Creates pip txt file based on the requirements + Creates pip txt file based on the requirements. :param requirements: Collected requirements - :type requirements: dict :param name: Name of the env - :type name: str - :param directory: Directory where the file should be saved. If None self.envs_dir will be taken - :type directory: str - :return: - :rtype: None + :param directory: Directory where the file should be saved. If None self.envs_dir will be taken. """ if directory is None: directory = self.envs_dir @@ -472,12 +412,8 @@ def create_req_file(self, requirements: dict, name: str, directory: str = None) def list_envs(self) -> None: """ - List all existing envs - - :return: - :rtype: None + List all existing environments. """ - logging.info("Existing environments:") for env in self.check_for_configs(): logging.info(f" - {env}") @@ -485,12 +421,9 @@ def list_envs(self) -> None: @staticmethod def show(env: list[dict]) -> None: """ - Visualize the env + Visualize the env. - :param env: env - :type env: list[dict] - :return: - :rtype: None + :param env: Environment configuration """ space = " " branch = "| " @@ -500,16 +433,12 @@ def show(env: list[dict]) -> None: def tree(modules: list[dict], prefix: str = ""): """ A recursive function that generates a tree from the modules. - This function is based on https://stackoverflow.com/a/59109706, but modified to the needs here + This function is based on https://stackoverflow.com/a/59109706, but modified to the needs here. - :param modules: Modules - :type modules: list[dict] + :param modules: Modules list :param prefix: Prefix for the indentation - :type prefix: str - :return: - :rtype: + :return: Generator yielding formatted lines of the environment tree """ - # Modules in the middle/beginning get a |--, the final leaf >-- pointers = [connector] * (len(modules) - 1) + [leaf] for pointer, module in zip(pointers, modules): diff --git a/src/Metrics.py b/src/Metrics.py index 1d2e3b19..3f93c21e 100644 --- a/src/Metrics.py +++ b/src/Metrics.py @@ -17,17 +17,15 @@ class Metrics: """ - Metrics Module, used by every QUARK module + Metrics Module, used by every QUARK module. """ def __init__(self, module_name: str, module_src: str): """ - Constructor for Metrics class + Constructor for Metrics class. :param module_name: Name of the module this metrics object belongs to - :type module_name: str :param module_src: Source file of the module this metrics object belongs to - :type module_src: str """ self.module_name = module_name self.module_src = module_src @@ -44,83 +42,65 @@ def __init__(self, module_name: str, module_src: str): def validate(self) -> None: """ Validates whether the mandatory metrics got recorded, then sets total time. - - :return: - :rtype: None """ - assert self.preprocessing_time is not None, "preprocessing time must not be None!" - assert self.postprocessing_time is not None, "postprocessing time must not be None!" + assert self.preprocessing_time is not None, ( + "preprocessing time must not be None!" + ) + assert self.postprocessing_time is not None, ( + "postprocessing time must not be None!" + ) self.total_time = self.preprocessing_time + self.postprocessing_time @final def set_preprocessing_time(self, value: float) -> None: """ - Sets the preprocessing time + Sets the preprocessing time. :param value: Time - :type value: float - :return: - :rtype: None """ self.preprocessing_time = value @final def set_module_config(self, config: dict) -> None: """ - Sets the config of the module this metrics object belongs to + Sets the config of the module this metrics object belongs to. :param config: Config of the QUARK module - :type config: dict - :return: - :rtype: None """ self.module_config = config @final def set_postprocessing_time(self, value: float) -> None: """ - Sets the postprocessing time + Sets the postprocessing time. :param value: Time - :type value: float - :return: - :rtype: None """ self.postprocessing_time = value @final def add_metric(self, name: str, value: any) -> None: """ - Adds a single metric + Adds a single metric. :param name: Name of the metric - :type name: str :param value: Value of the metric - :type value: any - :return: - :rtype: None """ self.additional_metrics.update({name: value}) @final def add_metric_batch(self, key_values: dict) -> None: """ - Adds a dictionary containing metrics to the existing metrics + Adds a dictionary containing metrics to the existing metrics. - :param key_values: dict containing metrics - :type key_values: dict - :return: - :rtype: None + :param key_values: Dict containing metrics """ self.additional_metrics.update(key_values) @final def reset(self) -> None: """ - Resets all recorded metrics - - :return: - :rtype: None + Resets all recorded metrics. """ self.preprocessing_time = None self.postprocessing_time = None @@ -129,10 +109,9 @@ def reset(self) -> None: @final def get(self) -> dict: """ - Returns all recorded metrics + Returns all recorded metrics. :return: Metrics as a dict - :rtype: dict """ return { "module_name": self.module_name, diff --git a/src/Plotter.py b/src/Plotter.py index 61ad58c6..8ca02ab0 100644 --- a/src/Plotter.py +++ b/src/Plotter.py @@ -13,7 +13,6 @@ # limitations under the License. from collections import defaultdict -from typing import List, Dict import logging import matplotlib.pyplot as plt @@ -27,30 +26,26 @@ class Plotter: """ - Plotter class which generates some general plots + Plotter class which generates some general plots. """ @staticmethod - def visualize_results(results: List[Dict], store_dir: str) -> None: + def visualize_results(results: list[dict], store_dir: str) -> None: """ Function to plot the execution times of the benchmark. - :param results: dict containing the results - :type results: list[dict] - :param store_dir: directory where the plots are stored - :type store_dir: str - :return: - :rtype: None + :param results: Dict containing the results + :param store_dir: Directory where the plots are stored """ - if results is None or len(results) == 0: logging.info("Nothing to plot since results are empty.") return processed_results_with_application_score = [] processed_results_rest = [] - required_application_score_keys = ["application_score_value", "application_score_unit", - "application_score_type"] + required_application_score_keys = [ + "application_score_value", "application_score_unit", "application_score_type" + ] application_name = None application_axis = None static_keys, changing_keys = Plotter._get_config_keys(results) @@ -66,17 +61,19 @@ def visualize_results(results: List[Dict], store_dir: str) -> None: application_config = ', '.join( [f"{key}: {value}" for (key, value) in sorted(result["module"]["module_config"].items(), key=lambda key_value_pair: - key_value_pair[0]) if key not in static_keys]) + key_value_pair[0]) if key not in static_keys] + ) if len(static_keys) > 0: # Include the static items in the axis name application_axis += "(" + ', '.join( - [f"{key}: {result['module']['module_config'][key]}" for key in static_keys]) + ")" + [f"{key}: {result['module']['module_config'][key]}" for key in static_keys] + ) + ")" - processed_item = Plotter._extract_columns({"benchmark_backlog_item_number": - result["benchmark_backlog_item_number"], - "total_time": result["total_time"], - "application_config": application_config}, - result["module"]) + processed_item = Plotter._extract_columns({ + "benchmark_backlog_item_number": result["benchmark_backlog_item_number"], + "total_time": result["total_time"], + "application_config": application_config + }, result["module"]) if all(k in result["module"] for k in required_application_score_keys): # Check if all required keys are present to create application score plots @@ -88,12 +85,15 @@ def visualize_results(results: List[Dict], store_dir: str) -> None: if len(processed_results_with_application_score) > 0: logging.info("Found results with an application score, generating according plots.") - Plotter.plot_application_score(application_name, application_axis, - processed_results_with_application_score, store_dir) + Plotter.plot_application_score( + application_name, application_axis, processed_results_with_application_score, store_dir + ) - Plotter.plot_times(application_name, application_axis, [*processed_results_with_application_score, - *processed_results_rest], store_dir, - required_application_score_keys) + Plotter.plot_times( + application_name, application_axis, + [*processed_results_with_application_score, *processed_results_rest], + store_dir, required_application_score_keys + ) logging.info("Finished creating plots.") @@ -103,18 +103,11 @@ def plot_times(application_name: str, application_axis: str, results: list[dict] """ Function to plot execution times of the different modules in a benchmark. - :param application_name: name of the application - :type application_name: str - :param application_axis: name of the application axis - :type application_axis: str - :param results: dict containing the results - :type results: list[dict] - :param store_dir: directory where the plots are stored - :type store_dir: str - :param required_application_score_keys: list of keys which have to be present to calculate an application score - :type required_application_score_keys: list - :return: - :rtype: None + :param application_name: Name of the application + :param application_axis: Name of the application axis + :param results: Dict containing the results + :param store_dir: Directory where the plots are stored + :param required_application_score_keys: List of keys which have to be present to calculate an application score """ df = pd.DataFrame.from_dict(results) @@ -140,7 +133,7 @@ def plot_times(application_name: str, application_axis: str, results: list[dict] # Put the legend out of the figure plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0., title="Modules used") plt.title(application_name) - matplotlib.pyplot.sca(ax) + plt.sca(ax) # If column values are very long and of type string rotate the ticks if (pd.api.types.is_string_dtype(df.application_config.dtype) or pd.api.types.is_object_dtype( df.application_config.dtype)) and df.application_config.str.len().max() > 10: @@ -153,20 +146,13 @@ def plot_times(application_name: str, application_axis: str, results: list[dict] def plot_application_score(application_name: str, application_axis: str, results: list[dict], store_dir: str) -> None: """ - Funtion to create plots showing the application score. - - :param application_name: name of the application - :type application_name: str - :param application_axis: name of the application axis - :type application_axis: str - :param results: dict containing the results - :type results: list[dict] - :param store_dir: directory where the plots are stored - :type store_dir: str - :return: - :rtype: None - """ + Function to create plots showing the application score. + :param application_name: Name of the application + :param application_axis: Name of the application axis + :param results: Dict containing the results + :param store_dir: Directory where the plots are stored + """ df = pd.DataFrame.from_dict(results) application_score_units = df["application_score_unit"].unique() count_invalid_rows = pd.isna(df['application_score_value']).sum() @@ -178,18 +164,24 @@ def plot_application_score(application_name: str, application_axis: str, results logging.info(f"{count_invalid_rows} out of {len(df)} benchmark runs have an invalid application score.") if len(application_score_units) != 1: - logging.warning(f"Found more or less than exactly 1 application_score_unit in {application_score_units}." - f" This might lead to incorrect plots!") + logging.warning( + f"Found more or less than exactly 1 application_score_unit in {application_score_units}." + f" This might lead to incorrect plots!" + ) ax = sns.barplot(x="application_config", y="application_score_value", data=df, hue="config_combo") ax.set(xlabel=application_axis, ylabel=application_score_units[0]) # Put the legend out of the figure plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0., title="Modules used") - ax.text(1.03, 0.5, f"{len(df) - count_invalid_rows}/{len(df)} runs have a valid \napplication score", - transform=ax.transAxes, fontsize=12, verticalalignment='top', bbox={"boxstyle": "round", "alpha": 0.15}) + ax.text( + 1.03, 0.5, + f"{len(df) - count_invalid_rows}/{len(df)} runs have a valid \napplication score", + transform=ax.transAxes, fontsize=12, verticalalignment='top', + bbox={"boxstyle": "round", "alpha": 0.15} + ) plt.title(application_name) - matplotlib.pyplot.sca(ax) + plt.sca(ax) # If column values are very long and of type string, rotate the ticks if (pd.api.types.is_string_dtype(df.application_config.dtype) or pd.api.types.is_object_dtype( df.application_config.dtype)) and df.application_config.str.len().max() > 10: @@ -197,18 +189,15 @@ def plot_application_score(application_name: str, application_axis: str, results plt.savefig(f"{store_dir}/application_score.pdf", dpi=300, bbox_inches='tight') logging.info(f"Saved {f'{store_dir}/application_score.pdf'}.") - plt.clf() @staticmethod - def _get_config_keys(results: list[dict]) -> (list, list): + def _get_config_keys(results: list[dict]) -> tuple[list, list]: """ Function that extracts config keys. - :param results: results of a benchmark run - :type results: list[dict] - :return: tuple with list of static keys and list of changing keys - :rtype: (list, list) + :param results: Results of a benchmark run + :return: Tuple with list of static keys and list of changing keys """ static_keys = [] changing_keys = [] @@ -231,17 +220,13 @@ def _get_config_keys(results: list[dict]) -> (list, list): @staticmethod def _extract_columns(config: dict, rest_result: dict) -> dict: """ - Funtion to extract and summarize certain data fields like the time spent in every module + Function to extract and summarize certain data fields like the time spent in every module from the nested module chain. - - :param config: dictionary containing multiple data fields like the config a module - :type config: dict - :param rest_result: rest of the module chain - :type rest_result: dict - :return: extracted data - :rtype: dict - """ + :param config: Dictionary containing multiple data fields like the config of a module + :param rest_result: Rest of the module chain + :return: Extracted data + """ if rest_result: module_name = rest_result["module_name"] for key, value in sorted(rest_result["module_config"].items(), @@ -253,8 +238,8 @@ def _extract_columns(config: dict, rest_result: dict) -> dict: { **config, "config_combo": config_combo, - module_name: rest_result["total_time"] if module_name not in config else config[module_name] + - rest_result["total_time"] + module_name: rest_result["total_time"] + if module_name not in config else config[module_name] + rest_result["total_time"] }, rest_result["submodule"] ) diff --git a/src/demo/instruction_demo.py b/src/demo/instruction_demo.py index 612c0bf9..24e102aa 100644 --- a/src/demo/instruction_demo.py +++ b/src/demo/instruction_demo.py @@ -9,43 +9,76 @@ class InstructionDemo(Application): """ A simple QUARK Application implementation showing the usage of instructions. """ + def __init__(self, application_name: str = None): super().__init__(application_name) self.submodule_options = ["Dummy"] - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple: + """ + Preprocess input data with given configuration and instructions. + + :param input_data: Data to be processed. + :param config: Configuration for processing the data + :param kwargs: Additional keyword arguments + :return: Instruction, processed data, and processing time. + """ logging.info("%s", kwargs.keys()) logging.info("previous_job_info: %s", kwargs.get("previous_job_info")) + rep_count = kwargs["rep_count"] instruction_name = config.get("instruction", Instruction.PROCEED.name) instruction = Instruction.PROCEED + if instruction_name == Instruction.PROCEED.name: instruction = Instruction.PROCEED elif instruction_name == Instruction.INTERRUPT.name: instruction = Instruction.INTERRUPT if instruction_name == "mixed": instruction = Instruction.PROCEED - if rep_count%2 == 1: + if rep_count % 2 == 1: instruction = Instruction.INTERRUPT elif instruction_name == "exception": raise Exception("demo exception") - logging.info("InstructionDemo iteration %s returns instruction %s", rep_count, instruction.name) + logging.info( + "InstructionDemo iteration %s returns instruction %s", + rep_count, instruction.name + ) return instruction, "", 0. def get_parameter_options(self) -> dict: + """ + Returns parameter options for the preprocess method. + """ return { - "instruction": {"values": [Instruction.PROCEED.name, - Instruction.INTERRUPT.name, - "exception", - "mixed"], - "description": "How should preprocess behave?"} + "instruction": { + "values": [ + Instruction.PROCEED.name, + Instruction.INTERRUPT.name, + "exception", + "mixed" + ], + "description": "How should preprocess behave?" + } } def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + + :param option: The submodule option + :return: Default submodule + """ return Dummy() def save(self, path: str, iter_count: int) -> None: + """ + Saves the current state to the specified path. + + :param path: Path where the state should be saved + :param iter_count: Iteration count. + """ pass @@ -55,7 +88,17 @@ class Dummy(Core): """ def get_parameter_options(self) -> dict: + """ + Returns parameter options for the Dummy module. + + :return: Dictionary containing parameter options + """ return {} def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule for the given option. + + :param option: The submodule option + """ pass diff --git a/src/main.py b/src/main.py index 74098203..d08b23cd 100644 --- a/src/main.py +++ b/src/main.py @@ -26,7 +26,7 @@ comm = get_comm() -# add the paths +# Add the paths install_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(install_dir) @@ -39,9 +39,7 @@ def _filter_comments(file: Iterable) -> str: Returns the content of the filehandle, ignoring all lines starting with '#'. :param file: file to be read - :type file: Iterable :return: file content without comment lines - :rtype: str """ lines = [] for line in file: @@ -53,10 +51,7 @@ def _filter_comments(file: Iterable) -> str: def setup_logging() -> None: """ - Sets up the logging - - :return: - :rtype: None + Sets up the logging. """ logging.root.handlers = [] logging.basicConfig( @@ -82,17 +77,14 @@ def setup_logging() -> None: logging.info(" ============================================================ ") -def start_benchmark_run(config_file: str = None, store_dir: str = None, fail_fast: bool = False) -> None: +def start_benchmark_run(config_file: str = None, store_dir: str = None, + fail_fast: bool = False) -> None: """ - Starts a benchmark run from the code - - :return: - :rtype: None + Starts a benchmark run from the code. """ - setup_logging() - # Helper for Hybrid Jobs + # Helper for hybrid jobs if not config_file: config_file = os.environ["AMZN_BRAKET_HP_FILE"] if not store_dir: @@ -115,7 +107,9 @@ def start_benchmark_run(config_file: str = None, store_dir: str = None, fail_fas # Can be overridden by using the -m|--modules option installer = Installer() app_modules = installer.get_env(installer.get_active_env()) - benchmark_manager.orchestrate_benchmark(config_manager, store_dir=store_dir, app_modules=app_modules) + benchmark_manager.orchestrate_benchmark( + config_manager, store_dir=store_dir, app_modules=app_modules + ) def create_benchmark_parser(parser: argparse.ArgumentParser): @@ -148,12 +142,9 @@ def create_env_parser(parser: argparse.ArgumentParser): def handle_benchmark_run(args: argparse.Namespace) -> None: """ - Handles the different options of a benchmark run + Handles the different options of a benchmark run. :param args: Namespace with the arguments given by the user - :type args: argparse.Namespace - :return: - :rtype: None """ from BenchmarkManager import BenchmarkManager # pylint: disable=C0415 from Plotter import Plotter # pylint: disable=C0415 @@ -172,7 +163,9 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: # + Replaces relative paths by taking them relative to the location of the modules configuration file base_dir = os.path.dirname(args.modules) with open(args.modules) as filehandler: - app_modules = _expand_paths(json.loads(_filter_comments(filehandler)), base_dir) + app_modules = _expand_paths(json.loads( + _filter_comments(filehandler)), base_dir + ) else: # Gets current env here installer = Installer() @@ -199,10 +192,13 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: logging.info("Selected config is:") config_manager.print() else: - interrupted_results_path = None if args.resume_dir is None else os.path.join(args.resume_dir, - "results.json") - benchmark_manager.orchestrate_benchmark(config_manager, app_modules, - interrupted_results_path=interrupted_results_path) + interrupted_results_path = None if args.resume_dir is None else os.path.join( + args.resume_dir, "results.json" + ) + benchmark_manager.orchestrate_benchmark( + config_manager, app_modules, + interrupted_results_path=interrupted_results_path + ) comm.Barrier() if comm.Get_rank() == 0: results = benchmark_manager.load_results() @@ -211,12 +207,9 @@ def handle_benchmark_run(args: argparse.Namespace) -> None: def handler_env_run(args: argparse.Namespace) -> None: """ - Orchestrates the requests to the QUARK module environment + Orchestrates the requests to the QUARK module environment. - :param args: Namespace with the arguments by the user - :type args: argparse.Namespace - :return: - :rtype: None + :param args: Namespace with the arguments given by the user """ installer = Installer() if args.createmoduledb: diff --git a/src/modules/Core.py b/src/modules/Core.py index f09b54a1..cf6663ba 100644 --- a/src/modules/Core.py +++ b/src/modules/Core.py @@ -15,25 +15,24 @@ from __future__ import annotations # Needed if you want to type hint a method with the type of the enclosing class import os -from abc import ABC, abstractmethod +import sys import logging +from abc import ABC, abstractmethod from typing import final -import sys from utils import _get_instance_with_sub_options - from Metrics import Metrics class Core(ABC): """ - Core Module for QUARK used by all other Modules that are part of a benchmark process + Core Module for QUARK, used by all other Modules that are part of a benchmark process. """ def __init__(self, name: str = None): """ - Constructor method + Constructor method. + :param name: name used to identify this QUARK module. If not specified class name will be used as default. - :type name: str """ self.submodule_options = [] self.sub_options = [] @@ -47,7 +46,7 @@ def __init__(self, name: str = None): @abstractmethod def get_parameter_options(self) -> dict: """ - Returns the parameters for a given module + Returns the parameters for a given module. Should always be in this format: @@ -65,19 +64,18 @@ def get_parameter_options(self) -> dict: } :return: Available settings for this application - :rtype: dict """ + raise NotImplementedError("Please don't use the base version of get_parameter_options. " + "Implement your own override instead.") @final - def get_submodule(self, option: str) -> any: + def get_submodule(self, option: str) -> Core: """ Submodule is instantiated according to the information given in self.sub_options. If self.sub_options is None, get_default_submodule is called as a fallback. :param option: String with the options - :type option: str :return: Instance of a module - :rtype: any """ if self.sub_options is None or not self.sub_options: return self.get_default_submodule(option) @@ -87,60 +85,49 @@ def get_submodule(self, option: str) -> any: @abstractmethod def get_default_submodule(self, option: str) -> Core: """ - Given an option string by the user, this returns a submodule + Given an option string by the user, this returns a submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ - raise NotImplementedError("Please don't use the base version of this method. " + raise NotImplementedError("Please don't use the base version of get_default_submodule. " "Implement your own override instead.") - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - Essential method for the benchmarking process. Is always executed before traversing down to the next module, - passing the data returned by this function. + Essential method for the benchmarking process. This is always executed before traversing down + to the next module, passing the data returned by this function. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ return input_data, 0.0 - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Essential Method for the benchmarking process. Is always executed after the submodule is finished. The data by this method is passed up to the parent module. :param input_data: Input data comes from the submodule if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the postprocessing and the time it took to postprocess - :rtype: (any, float) """ return input_data, 0.0 @final def get_available_submodule_options(self) -> list: """ - Gets list of available options + Gets the list of available options. :return: List of module options - :rtype: list """ if self.sub_options is None or not self.sub_options: return self.submodule_options else: - return [o["name"] for o in self.sub_options] + return [option["name"] for option in self.sub_options] @staticmethod def get_requirements() -> list: @@ -148,6 +135,5 @@ def get_requirements() -> list: Returns the required pip packages for this module. Optionally, version requirements can be added. :return: List of dictionaries - :rtype: list """ return [] diff --git a/src/modules/applications/Application.py b/src/modules/applications/Application.py index a23b34ff..c1a93b77 100644 --- a/src/modules/applications/Application.py +++ b/src/modules/applications/Application.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.Core import * +from abc import ABC, abstractmethod +from modules.Core import Core class Application(Core, ABC): @@ -23,7 +24,7 @@ class Application(Core, ABC): def __init__(self, application_name: str): """ - Constructor method + Constructor method. """ super().__init__(application_name) self.application_name = self.name @@ -31,22 +32,18 @@ def __init__(self, application_name: str): def get_application(self) -> any: """ - Gets the application + Gets the application. :return: self.application - :rtype: any """ return self.application @abstractmethod def save(self, path: str, iter_count: int) -> None: """ - Saves the concrete problem - :param path: path of the experiment directory for this run - :type path: str - :param iter_count: the iteration count - :type iter_count: int - :return: - :rtype: None + Saves the concrete problem. + + :param path: Path of the experiment directory for this run + :param iter_count: The iteration count """ pass diff --git a/src/modules/applications/Mapping.py b/src/modules/applications/Mapping.py index 9fa95b10..25d18d94 100644 --- a/src/modules/applications/Mapping.py +++ b/src/modules/applications/Mapping.py @@ -12,72 +12,59 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.Core import * +from abc import ABC, abstractmethod +from modules.Core import Core class Mapping(Core, ABC): """ - This module translates the input data and problem specification from the parent module, e.g., - the application into a mathematical formulation suitable the submodule, e.g., a solver. + This module translates the input data and problem specification from the parent module, + e.g., the application into a mathematical formulation suitable the submodule, e.g., a solver. """ - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - Maps the data to the correct target format + Maps the data to the correct target format. :param input_data: Data which should be mapped - :type input_data: any :param config: Config of the mapping - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: Tuple with mapped problem and the time it took to map it - :rtype: (any, float) """ output, preprocessing_time = self.map(input_data, config) return output, preprocessing_time - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Reverse transformation/mapping from the submodule's format to the mathematical formulation - suitable for the parent module + suitable for the parent module. :param input_data: Data which should be reverse-mapped - :type input_data: any :param config: Config of the reverse mapping - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param kwargs: Optional keyword arguments :return: Tuple with reverse-mapped problem and the time it took to map it - :rtype: (any,float) """ output, postprocessing_time = self.reverse_map(input_data) return output, postprocessing_time @abstractmethod - def map(self, problem: any, config: dict) -> (any, float): + def map(self, problem: any, config: dict) -> tuple[any, float]: """ - Maps the given problem into a specific format suitable for the submodule, e.g., a solver + Maps the given problem into a specific format suitable for the submodule, e.g., a solver. :param config: Instance of class Config specifying the mapping settings - :type config: dict :param problem: Problem instance which should be mapped to the target representation - :type problem: any :return: Mapped problem and the time it took to map it - :rtype: tuple(any, float) """ pass - def reverse_map(self, solution) -> (any, float): + def reverse_map(self, solution: any) -> tuple[any, float]: """ Maps the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. :param solution: Solution provided by submodule, e.g., the Solver class - :type solution: any :return: Reverse-mapped solution and the time it took to create it - :rtype: tuple(any, float) - """ return solution, 0 diff --git a/src/modules/applications/optimization/ACL/ACL.py b/src/modules/applications/optimization/ACL/ACL.py index d20971c8..fabea710 100644 --- a/src/modules/applications/optimization/ACL/ACL.py +++ b/src/modules/applications/optimization/ACL/ACL.py @@ -17,23 +17,24 @@ # Modifications Copyright (c) 2007- Stuart Anthony Mitchell # # Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the Software +# is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. +import os +import logging from typing import TypedDict import pandas as pd import numpy as np import pulp -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement @@ -41,64 +42,65 @@ class ACL(Optimization): """ The distribution of passenger vehicles is a complex task and a high cost factor for automotive original -equipment manufacturers (OEMs). On the way from the production plant to the customer, vehicles travel -long distances on different carriers such as ships, trains, and trucks. To save costs, OEMs and logistics service -providers aim to maximize their loading capacities. Modern auto carriers are extremely flexible. Individual -platforms can be rotated, extended, or combined to accommodate vehicles of different shapes and weights -and to nest them in a way that makes the best use of the available space. In practice, finding feasible -combinations is done with the help of simple heuristics or based on personal experience. In research, most -papers that deal with auto carrier loading focus on route or cost optimization. Only a rough approximation -of the loading sub-problem is considered. We formulate the problem as a mixed integer quadratically constrained -assignment problem. + equipment manufacturers (OEMs). Vehicles travel long distance on different carriers, such as ships, + trains, and trucks, from the production plant to the customer. + + To save costs, OEMs and logistics service providers aim to maximize their loading capacities. + Modern auto carriers are flexible, allowing individual platforms to be rotated, extended, or combined + to accommodate vehicles of different shapes and weights in a space-efficient manner. + + In practice, finding feasible combinations is often based on heuristics or personal experience. + We formulate the problem as a mixed integer quadratically constrained assignment problem. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("ACL") self.submodule_options = ["MIPsolverACL", "QUBO"] self.application = None @staticmethod - def get_requirements() -> list: - return [ - { - "name": "pulp", - "version": "2.9.0" - }, - { - "name": "pandas", - "version": "2.2.2" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "openpyxl", - "version": "3.1.5" - } + def get_requirements() -> list[dict]: + """ + Returns the list of module requirements. + :return: List of dictionaries containing module requirements + """ + return [ + {"name": "pulp", "version": "2.9.0"}, + {"name": "pandas", "version": "2.2.2"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "openpyxl", "version": "3.1.5"}, ] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "MIPsolverACL": from modules.solvers.MIPsolverACL import MIPaclp # pylint: disable=C0415 return MIPaclp() - # elif option == "Ising": - # from modules.applications.optimization.ACL.mappings.ISING import Ising # pylint: disable=C0415 - # return Ising() elif option == "QUBO": from modules.applications.optimization.ACL.mappings.QUBO import Qubo # pylint: disable=C0415 return Qubo() else: raise NotImplementedError(f"Submodule Option {option} not implemented") - def get_parameter_options(self): + def get_parameter_options(self) -> dict: + """ + Returns parameter options for selecting different models. + + :return: Dictionary containing model selection options + """ return { "model_select": { - "values": list(["Full", "Small", "Tiny"]), + "values": ["Full", "Small", "Tiny"], "description": "Do you want the full model or a simplified (QUBO friendly) one?" } } @@ -107,25 +109,37 @@ class Config(TypedDict): model_select: str @staticmethod - def intersectset(p1, p2): + def intersectset(p1: list, p2: list) -> list: + """ + Computes the intersection of two lists. + + :param p1: First list + :param p2: Second list + :return: List containing elements common to both p1 and p2 + """ return np.intersect1d(p1, p2).tolist() @staticmethod - def diffset(p1, p2): + def diffset(p1: list, p2: list) -> list: + """ + Computes the difference between two lists. + + :param p1: First list + :param p2: Second list + :return: List containing elements in p1 that are not in p2 + """ return np.setdiff1d(p1, p2).tolist() def generate_problem(self, config: Config) -> dict: # pylint: disable=R0915 """ This function includes three models: Full, small and tiny. Full refers to the original model with all of its - constraints. Small refers to the simplified model which is more suitable for solving it with QC methods. + constraints. Small refers to the simplified model suitable for solving it with QC methods. The simplified model does not consider the possibility that vehicles can be angled or that they can be oriented forwards or backwards in relation to the auto carrier. For the tiny version we do not consider split platforms and consider only weight constraints. :param config: Config containing the selected scenario - :type config: Config :return: Dictionary with scenario-dependent model formulated as linear problem - :rtype: dict """ # Enter vehicles to load (BMW model codes) vehicles = ["G20", "G20", "G20", "G20", "G07", "G20"] @@ -133,545 +147,548 @@ def generate_problem(self, config: Config) -> dict: # pylint: disable=R0915 df = pd.read_excel(os.path.join(os.path.dirname(__file__), "Vehicle_data_QUARK.xlsx")) model_select = config['model_select'] - # All the parameters are given in decimeters -> 4m == 400 cm == 40 dm or decitons -> 2 tons -> 20 dt + # All the parameters are in decimeters and decitons (4m == 400 cm == 40 dm , 2 tons -> 20 dt # Below are the model specific parameters, constraints and objectives for the tiny, small and the full model if model_select == "Tiny": - # Weight parameters - # max. total weight on truck / trailer - wt = [100] - # wt = [10] - # max. weight on the four levels - wl = [50, 60] - # wl = [5, 6] - # max. weights on platforms p, if not angled - wp = [23, 23, 23, 26, 17] - # wp = [2, 2, 2, 2, 1] - - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model - weight_list = [0] * (len(vehicles)) - - for i in set(range(len(vehicles))): - df_new = df.loc[df['Type'] == vehicles[i]] - # df_new = (df.loc[df['Type'] == vehicles[i]]) - weight_list[i] = int(df_new["Weight"].iloc[0]) - # weight_list[i] = int(int(df_new["Weight"].iloc[0])/10) - - # Construct sets - # Set of available cars - vecs = set(range(len(vehicles))) - # Set of available platforms - platforms_array = np.array([0, 1, 2, 3, 4]) - plats = set(range(len(platforms_array))) - - # Set of platforms that have a limitation on allowed weight - platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) - plats_l = set(range(len(platforms_level_array))) - - # Set of platforms that form trailer and truck - platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) - plats_t = set(range(len(platforms_truck_trailer_array))) - - # Create decision variables - # Vehicle v assigned to p - x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') - - # Create the 'prob' variable to contain the problem data - prob = pulp.LpProblem("ACL", pulp.LpMaximize) - - # Objective function - # Maximize number of vehicles on the truck - prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) - - # Constraints - # Assignment constraints - # (1) Every vehicle can only be assigned to a single platform - for p in plats: - prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 - - # (2) Every platform can only hold a single vehicle + self._generate_tiny_model(df, vehicles) + elif model_select == "Small": + self._generate_small_model(df, vehicles) + else: + self._generate_full_model(df, vehicles) + + problem_instance = self.application.to_dict() + self.application = problem_instance + return self.application + + def _generate_tiny_model(self, df: any, vehicles: list) -> None: + """ + Generate the problem model for the Tiny configurations. + + :param df: Datafile + :param vehicles: List of vehicle types + """ + # Weight parameters + # Max. total weight on truck / trailer + wt = [100] + # Max. weight on the four levels + wl = [50, 60] + # Max. weights on platforms p, if not angled + wp = [23, 23, 23, 26, 17] + + # Create empty lists for different vehicle parameters. This is required for proper indexing in the model. + weight_list = [0] * (len(vehicles)) + + for i, vehicle in enumerate(vehicles): + df_new = df.loc[df['Type'] == vehicle] + weight_list[i] = int(df_new["Weight"].iloc[0]) + + # Set of available cars + vecs = set(range(len(vehicles))) + # Set of available platforms + platforms_array = np.array([0, 1, 2, 3, 4]) + plats = set(range(len(platforms_array))) + + # Set of platforms that have a limitation on allowed weight + platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) + plats_l = set(range(len(platforms_level_array))) + + # Set of platforms that form trailer and truck + platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) + plats_t = set(range(len(platforms_truck_trailer_array))) + + # Create decision variables + x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') + + # Create the problem instance + prob = pulp.LpProblem("ACL", pulp.LpMaximize) + + # Objective function + # Maximize number of vehicles on the truck + prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) + + # Constraints + # (1) Every vehicle can only be assigned to a single platform + for p in plats: + prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 + + # (2) Every platform can only hold a single vehicle + for v in vecs: + prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + + # (3) Weight limit for every platform + for p in plats: for v in vecs: - prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + prob += weight_list[v] * x[p, v] <= wp[p] - # (3) Weight limit for every platform - for p in plats: - for v in vecs: - prob += weight_list[v] * x[p, v] <= wp[p] + # (4) Weight constraint for every level + for p_l in plats_l: + prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ + wl[p_l] - # (4) Weight constraint for every level - for p_l in plats_l: - prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ - wl[p_l] + # (5) Weight constraint for truck and trailer + for t in plats_t: + prob += pulp.lpSum( + weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[t] for v in vecs) <= wt[t] - # (5) Weight constraint for truck and trailer - for t in plats_t: - prob += pulp.lpSum( - weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[t] for v in vecs) <= wt[t] + self.application = prob - elif model_select == "Small": + def _generate_small_model(self, df: any, vehicles: list) -> None: + """ + Generate the problem model for the Small configuration - # For the small model, we only consider two levels with 3 and 2 platforms each - - # Length parameters - # Level 1 (Truck up), 2 (Truck down) - lmax_l = [97, 79] - - # Height parameters - # Considers base truck height and height distance between vehicles (~10cm) - hmax_truck = [34, 34, 33, 36, 32, 36] - # [0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4] - - # Weight parameters - # max. total weight on truck / trailer - wt = [100] - # max. weight on the two levels - wl = [65, 50] - # max. weights on platforms p, if not angled - wp = [23, 23, 23, 26, 17] - # max. weight on p, if sp is used - wsp = [28, 28, 28] - - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model - class_list = [0] * (len(vehicles)) - length_list = [0] * (len(vehicles)) - height_list = [0] * (len(vehicles)) - weight_list = [0] * (len(vehicles)) - - for i in set(range(len(vehicles))): - df_new = df.loc[df['Type'] == vehicles[i]] - class_list[i] = int(df_new["Class"].iloc[0]) - length_list[i] = int(df_new["Length"].iloc[0]) - height_list[i] = int(df_new["Height"].iloc[0]) - weight_list[i] = int(df_new["Weight"].iloc[0]) - - # Construct sets - # Set of available cars - vecs = set(range(len(vehicles))) - # Set of available platforms - platforms_array = np.array([0, 1, 2, 3, 4]) - plats = set(range(len(platforms_array))) - - # Set of possible split platforms - split_platforms_array = np.array([[0, 1], [1, 2], [3, 4]], dtype=object) - plats_sp = set(range(len(split_platforms_array))) - - # Set of platforms that have a limitation on allowed length and weight because they are on the same level - platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) - plats_l = set(range(len(platforms_level_array))) - - # Set of platforms that form trailer and truck - platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) - plats_t = set(range(len(platforms_truck_trailer_array))) - - # Set of platforms that have a limitation on allowed height - platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], - dtype=object) - plats_h1 = set(range(len(platforms_height_array_truck))) - - # Create decision variables - # Vehicle v assigned to p - x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') - # Usage of split platform - sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') - # Auxiliary variable for linearization of quadratic constraints - gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') - - # Create the 'prob' variable to contain the problem data - prob = pulp.LpProblem("ACL", pulp.LpMaximize) - - # Objective function - # Maximize number of vehicles on the truck - prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) - - # Constraints - # Assignment constraints - # (1) Every vehicle can only be assigned to a single platform - for p in plats: - prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 - - # (2) Every platform can only hold a single vehicle + :param df: Datafile + :param vehicles: List of vehicle types + """ + # Parameters for the small model (2 levels with 3 and 2 platforms each) + + # Length parameters + # Level 1 (Truck up), 2 (Truck down) + lmax_l = [97, 79] + # Height parameters + # Considers base truck height and height distance between vehicles (~10cm) + hmax_truck = [34, 34, 33, 36, 32, 36] + # Weight parameters + # Max. total weight on truck / trailer + wt = [100] + # Max. weight on the two levels + wl = [65, 50] + # Max. weights on platforms p, if not angled + wp = [23, 23, 23, 26, 17] + # Max. weight on p, if sp is used + wsp = [28, 28, 28] + + _, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) + + # Set of available cars + vecs = set(range(len(vehicles))) + # Set of available platforms + platforms_array = np.array([0, 1, 2, 3, 4]) + plats = set(range(len(platforms_array))) + + # Set of possible split platforms + split_platforms_array = np.array([[0, 1], [1, 2], [3, 4]], dtype=object) + plats_sp = set(range(len(split_platforms_array))) + + # Set of platforms that have a limitation on allowed length and weight because they are on the same level + platforms_level_array = np.array([[0, 1, 2], [3, 4]], dtype=object) + plats_l = set(range(len(platforms_level_array))) + + # Set of platforms that form trailer and truck + platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4]], dtype=object) + plats_t = set(range(len(platforms_truck_trailer_array))) + + # Set of platforms that have a limitation on allowed height + platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], dtype=object) + plats_h1 = set(range(len(platforms_height_array_truck))) + + # Create decision variables + x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') + # Usage of split platform + sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') + # Auxiliary variable for linearization of quadratic constraints + gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') + + # Create the 'prob' variable to contain the problem data + prob = pulp.LpProblem("ACL", pulp.LpMaximize) + # Maximize number of vehicles on the truck + prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) + + # Constraints + # (1) Every vehicle can only be assigned to a single platform + for p in plats: + prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 + + # (2) Every platform can only hold a single vehicle + for v in vecs: + prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + + # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used + for q in plats_sp: + prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ + <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] + + # (4) It is always only possible to use a single split-platform for any given p + for q in plats_sp: + for p in plats_sp: + if p != q: + z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) + if z is True: + prob += sp[q] + sp[p] <= 1 + + # (5) Length constraint + # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length + for L in plats_l: + prob += (pulp.lpSum(x[p, v] * length_list[v] for p in platforms_level_array[L] for v in vecs) + <= lmax_l[L]) + + # (6) Height constraints for truck and trailer, analogue to length constraints + # Truck + for h in plats_h1: + prob += pulp.lpSum(x[p, v] * height_list[v] for p in platforms_height_array_truck[h] for v in vecs) \ + <= hmax_truck[h] + + # (7) Linearization constraint -> gamma == 1, if split platform is used + for q in plats_sp: + prob += pulp.lpSum( + sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) + for v in vecs) >= 2 * gamma[q] + + # (8) Weight limit for every platform + for p in plats: for v in vecs: - prob += pulp.lpSum(x[p, v] for p in plats) <= 1 - - # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used - for q in plats_sp: - prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ - <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] - - # (4) It is always only possible to use a single split-platform for any given p - for q in plats_sp: - for p in plats_sp: - if p != q: - z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) - if z is True: - prob += sp[q] + sp[p] <= 1 - - # (5) Length constraint - # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length - for L in plats_l: - prob += (pulp.lpSum(x[p, v] * length_list[v] for p in platforms_level_array[L] for v in vecs) - <= lmax_l[L]) - - # (6) Height constraints for truck and trailer, analogue to length constraints - # Truck - for h in plats_h1: - prob += pulp.lpSum(x[p, v] * height_list[v] for p in platforms_height_array_truck[h] for v in vecs) \ - <= hmax_truck[h] - - # (7) Linearization constraint -> gamma == 1, if split platform is used - for q in plats_sp: - prob += pulp.lpSum( - sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) - for v in vecs) >= 2 * gamma[q] - - # (8) Weight limit for every platform - for p in plats: - for v in vecs: - prob += weight_list[v] * x[p, v] <= wp[p] - - # (9) If a split platform is used, weight limit == wsp, if not, then weight limit == wp - for q in plats_sp: - for p in split_platforms_array[q]: - prob += pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] \ - + (1 - gamma[q]) * wp[p] - - # (10) Weight constraint for every level - for p_l in plats_l: - prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ - wl[p_l] - - # (11) Weight constraint for truck and trailer - for p_t in plats_t: - prob += pulp.lpSum( - weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) <= wt[p_t] - else: - # Horizontal Coefficients: Length reduction - # 1 = forward, 0 = backward - # [0:1, 1:1, 0:0, 1:0] - v_coef = np.array([[0.20, 0.15, 0.14, 0.19], - [0.22, 0.22, 0.22, 0.22], - [0.22, 0.13, 0.12, 0.17]]) - - # Vertical Coefficients: Height increase - # [0:1, 1:1, 0:0, 1:0] - h_coef = np.array([[0.40, 1, 1, 1], - [0.17, 0.22, 0.21, 0.22], - [0.17, 0.38, 0.32, 0.32]]) - - # Length parameters - # Level 1 (Truck up), 2 (Truck down), 3 (Trailer up), 4 (Trailer down) - lmax_l = [97, 79, 97, 97] - - # Height parameters - # Considers base truck height and height distance between vehicles (~10cm) - hmax_truck = [34, 34, 33, 36, 32, 36] - # [0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4] - hmax_trailer = [36, 32, 32, 34] - # [5, 7], [5, 8], [6, 8], [6, 9] - - # Weight parameters - # max. total weight - wmax = 180 - # max. total weight on truck / trailer - wt = [100, 100] - # max. weight on the four levels - wl = [50, 60, 50, 90] - # max. weights on platforms p, if not angled - wp = [23, 23, 23, 26, 17, 26, 26, 26, 23, 26] - # max. weights on p, angled (if possible: 1, 2, 4, 7, 8, 9): - wpa = [20, 22, 17, 18, 19, 22] - # max. weight on p, if sp is used - wsp = [28, 28, 28, 28, 28, 28] - - # Create empty lists for different vehicle parameters. This is required for proper indexing in the model - class_list = [0] * (len(vehicles)) - length_list = [0] * (len(vehicles)) - height_list = [0] * (len(vehicles)) - weight_list = [0] * (len(vehicles)) - - for i in set(range(len(vehicles))): - df_new = df.loc[df['Type'] == vehicles[i]] - class_list[i] = int(df_new["Class"].iloc[0]) - length_list[i] = int(df_new["Length"].iloc[0]) - height_list[i] = int(df_new["Height"].iloc[0]) - weight_list[i] = int(df_new["Weight"].iloc[0]) - - # Construct sets - # Set of available cars - vecs = set(range(len(vehicles))) - # Set of available platforms - platforms_array = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - plats = set(range(len(platforms_array))) - - # Set of platforms that can be angled - platforms_angled_array = [1, 2, 4, 7, 8] - vp = [0, 1, 3, 8, 9] # Platforms "under" a_p - plats_a = set(range(len(platforms_angled_array))) - - # Set of possible split platforms - split_platforms_array = np.array([[0, 1], [1, 2], [3, 4], [5, 6], [7, 8], [8, 9]], dtype=object) - plats_sp = set(range(len(split_platforms_array))) - - # Set of platforms that have a limitation on allowed length and weight because they are on the same level - platforms_level_array = np.array([[0, 1, 2], [3, 4], [5, 6], [7, 8, 9]], dtype=object) - plats_l = set(range(len(platforms_level_array))) - - # Set of platforms that form trailer and truck - platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], dtype=object) - plats_t = set(range(len(platforms_truck_trailer_array))) - - # Set of platforms that have a limitation on allowed height - platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], - dtype=object) - platforms_height_array_trailer = np.array([[5, 7], [5, 8], [6, 8], [6, 9]], dtype=object) - - plats_h1 = set(range(len(platforms_height_array_truck))) - plats_h2 = set(range(len(platforms_height_array_trailer))) - - # Create the 'prob' variable to contain the problem data - prob = pulp.LpProblem("ACL", pulp.LpMaximize) - - # Create decision variables - # Vehicle v assigned to p - x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') - # Usage of split platform - sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') - # Direction of vehicle on p - d = pulp.LpVariable.dicts('d', (p for p in plats), cat='Binary') - # State of platform p in PA - angled == 1, not angled == 0 - a_p = pulp.LpVariable.dicts('a_p', (p for p in plats_a), cat='Binary') - - # Create auxiliary variables for linearization of quadratic constraints - y1 = pulp.LpVariable.dicts('y1', (p for p in plats_a), cat='Binary') - y2 = pulp.LpVariable.dicts('y2', (p for p in plats_a), cat='Binary') - y3 = pulp.LpVariable.dicts('y3', (p for p in plats_a), cat='Binary') - y4 = pulp.LpVariable.dicts('y4', (p for p in plats_a), cat='Binary') - ay1 = pulp.LpVariable.dicts('ay1', (p for p in plats_a), cat='Binary') - ay2 = pulp.LpVariable.dicts('ay2', (p for p in plats_a), cat='Binary') - ay3 = pulp.LpVariable.dicts('ay3', (p for p in plats_a), cat='Binary') - ay4 = pulp.LpVariable.dicts('ay4', (p for p in plats_a), cat='Binary') - # Weight for split-platforms - gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') - - # Here the model starts, including objective and constraints - - # Objective function - # Maximize number of vehicles on the truck - prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) - - # Constraints - # Assignment constraints - # (1) Every vehicle can only be assigned to a single platform - for p in plats: - prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 - - # (2) Every platform can only hold a single vehicle + prob += weight_list[v] * x[p, v] <= wp[p] + + # (9) If a split platform is used, weight limit == wsp, if not, then weight limit == wp + for q in plats_sp: + for p in split_platforms_array[q]: + prob += pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] \ + + (1 - gamma[q]) * wp[p] + + # (10) Weight constraint for every level + for p_l in plats_l: + prob += pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= \ + wl[p_l] + + # (11) Weight constraint for truck and trailer + for p_t in plats_t: + prob += pulp.lpSum( + weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) <= wt[p_t] + + self.application = prob + + def _generate_full_model(self, df: any, vehicles: list) -> None: # pylint: disable=R0915 + """ + Generate the problem model for the Full configuration. + + :param df: Datafile + :param vehicles: List of vehicle types + """ + # Horizontal Coefficients: Length reduction + # 1 = forward, 0 = backward + v_coef = np.array([[0.20, 0.15, 0.14, 0.19], + [0.22, 0.22, 0.22, 0.22], + [0.22, 0.13, 0.12, 0.17]]) + + # Vertical Coefficients: Height increase + h_coef = np.array([[0.40, 1, 1, 1], + [0.17, 0.22, 0.21, 0.22], + [0.17, 0.38, 0.32, 0.32]]) + + # Length parameters + # Level 1 (Truck up), 2 (Truck down), 3 (Trailer up), 4 (Trailer down) + lmax_l = [97, 79, 97, 97] + + # Height parameters + # Considers base truck height and height distance between vehicles (~10cm) + hmax_truck = [34, 34, 33, 36, 32, 36] + hmax_trailer = [36, 32, 32, 34] + + # Weight parameters + # Max. total weight + wmax = 180 + # Max. total weight on truck / trailer + wt = [100, 100] + # Max. weight on the four levels + wl = [50, 60, 50, 90] + # Max. weights on platforms p, if not angled + wp = [23, 23, 23, 26, 17, 26, 26, 26, 23, 26] + # Max. weights on p, angled (if possible: 1, 2, 4, 7, 8, 9): + wpa = [20, 22, 17, 18, 19, 22] + # Max. weight on p, if sp is used + wsp = [28, 28, 28, 28, 28, 28] + + class_list, length_list, height_list, weight_list = self._get_vehicle_params(df, vehicles) + + # Set of available cars + vecs = set(range(len(vehicles))) + # Set of available platforms + platforms_array = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + plats = set(range(len(platforms_array))) + + # Set of platforms that can be angled + platforms_angled_array = [1, 2, 4, 7, 8] + vp = [0, 1, 3, 8, 9] + plats_a = set(range(len(platforms_angled_array))) + + # Set of possible split platforms + split_platforms_array = np.array([[0, 1], [1, 2], [3, 4], [5, 6], [7, 8], [8, 9]], dtype=object) + plats_sp = set(range(len(split_platforms_array))) + + # Set of platforms that have a limitation on allowed length and weight because they are on the same level + platforms_level_array = np.array([[0, 1, 2], [3, 4], [5, 6], [7, 8, 9]], dtype=object) + plats_l = set(range(len(platforms_level_array))) + + # Set of platforms that form trailer and truck + platforms_truck_trailer_array = np.array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], dtype=object) + plats_t = set(range(len(platforms_truck_trailer_array))) + + # Set of platforms that have a limitation on allowed height + platforms_height_array_truck = np.array([[0, 3], [1, 3], [2, 3], [0, 4], [1, 4], [2, 4]], dtype=object) + platforms_height_array_trailer = np.array([[5, 7], [5, 8], [6, 8], [6, 9]], dtype=object) + + plats_h1 = set(range(len(platforms_height_array_truck))) + plats_h2 = set(range(len(platforms_height_array_trailer))) + + # Create decision variables + # Vehicle v assigned to p + x = pulp.LpVariable.dicts('x', ((p, v) for p in plats for v in vecs), cat='Binary') + # Usage of split platform + sp = pulp.LpVariable.dicts('sp', (q for q in plats_sp), cat='Binary') + # Direction of vehicle on p + d = pulp.LpVariable.dicts('d', (p for p in plats), cat='Binary') + # State of platform p in PA - angled == 1, not angled == 0 + a_p = pulp.LpVariable.dicts('a_p', (p for p in plats_a), cat='Binary') + + # Auxiliary variables for linearization of quadratic constraints + y1 = pulp.LpVariable.dicts('y1', (p for p in plats_a), cat='Binary') + y2 = pulp.LpVariable.dicts('y2', (p for p in plats_a), cat='Binary') + y3 = pulp.LpVariable.dicts('y3', (p for p in plats_a), cat='Binary') + y4 = pulp.LpVariable.dicts('y4', (p for p in plats_a), cat='Binary') + ay1 = pulp.LpVariable.dicts('ay1', (p for p in plats_a), cat='Binary') + ay2 = pulp.LpVariable.dicts('ay2', (p for p in plats_a), cat='Binary') + ay3 = pulp.LpVariable.dicts('ay3', (p for p in plats_a), cat='Binary') + ay4 = pulp.LpVariable.dicts('ay4', (p for p in plats_a), cat='Binary') + # Weight for split-platforms + gamma = pulp.LpVariable.dicts('gamma', (p for p in plats_sp), cat='Binary') + + # Create the 'prob' variable to contain the problem data + prob = pulp.LpProblem("ACL", pulp.LpMaximize) + # Maximize number of vehicles on the truck + prob += pulp.lpSum(x[p, v] for p in plats for v in vecs) + + # Assignment constraints + # (1) Every vehicle can only be assigned to a single platform + for p in plats: + prob += pulp.lpSum(x[p, v] for v in vecs) <= 1 + # (2) Every platform can only hold a single vehicle + for v in vecs: + prob += pulp.lpSum(x[p, v] for p in plats) <= 1 + + # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used + for q in plats_sp: + prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs) \ + <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] + + # (3.1) It is always only possible to use a single split-platform for any given p + for q in plats_sp: + for p in plats_sp: + if p != q: + z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) + if z is True: + prob += sp[q] + sp[p] <= 1 + + # (3.2) It is not allowed to angle platforms next to empty platforms + for i, p in enumerate(platforms_angled_array): + prob += pulp.lpSum(x[p, v] + x[vp[i], v] for v in vecs) >= 2 * a_p[i] + + # Linearization constraints + # Linearization of d_p and d_v(p) -> orientations of two neighboring cars + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + v_p = vp[z] + prob += (1 - d[p]) + d[v_p] >= 2 * y1[z] + prob += d[p] + d[v_p] >= 2 * y2[z] + prob += (1 - d[p]) + (1 - d[v_p]) >= 2 * y3[z] + prob += d[p] + (1 - d[v_p]) >= 2 * y4[z] + + # Linearization of a_p with y1 - y4 -> linear combination of angle and orientations + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + prob += a_p[z] + y1[z] >= 2 * ay1[z] + prob += a_p[z] + y2[z] >= 2 * ay2[z] + prob += a_p[z] + y3[z] >= 2 * ay3[z] + prob += a_p[z] + y4[z] >= 2 * ay4[z] + + # Linearization of x * ay -> linear combination of assignment and orientation/angle + xay1 = pulp.LpVariable.dicts('xay1', ((p, v) for p in plats_a for v in vecs), cat='Binary') + xay2 = pulp.LpVariable.dicts('xay2', ((p, v) for p in plats_a for v in vecs), cat='Binary') + xay3 = pulp.LpVariable.dicts('xay3', ((p, v) for p in plats_a for v in vecs), cat='Binary') + xay4 = pulp.LpVariable.dicts('xay4', ((p, v) for p in plats_a for v in vecs), cat='Binary') + for p in platforms_angled_array: + z = platforms_angled_array.index(p) for v in vecs: - prob += pulp.lpSum(x[p, v] for p in plats) <= 1 - - # (3) If a split platform q in plats_sp is used, only one of its "sub platforms" can be used - for q in plats_sp: - prob += pulp.lpSum(x[p, v] for p in split_platforms_array[q] for v in vecs)\ - <= len(split_platforms_array[q]) * (1 - sp[q]) + sp[q] - - # (3.1) It is always only possible to use a single split-platform for any given p - for q in plats_sp: - for p in plats_sp: - if p != q: - z = bool(set(split_platforms_array[q]) & set(split_platforms_array[p])) - if z is True: - prob += sp[q] + sp[p] <= 1 - - # (3.2) It is not allowed to angle platforms next to empty platforms - for i, p in enumerate(platforms_angled_array): - prob += pulp.lpSum(x[p, v] + x[vp[i], v] for v in vecs) >= 2 * a_p[i] - - # Linearization constraints - # Linearization of d_p and d_v(p) -> orientations of two neighboring cars - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - v_p = vp[z] - prob += (1 - d[p]) + d[v_p] >= 2 * y1[z] - prob += d[p] + d[v_p] >= 2 * y2[z] - prob += (1 - d[p]) + (1 - d[v_p]) >= 2 * y3[z] - prob += d[p] + (1 - d[v_p]) >= 2 * y4[z] - - # Linearization of a_p with y1 - y4 -> linear combination of angle and orientations - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - prob += a_p[z] + y1[z] >= 2 * ay1[z] - prob += a_p[z] + y2[z] >= 2 * ay2[z] - prob += a_p[z] + y3[z] >= 2 * ay3[z] - prob += a_p[z] + y4[z] >= 2 * ay4[z] - - # Linearization of x * ay -> linear combination of assignment and orientation/angle - xay1 = pulp.LpVariable.dicts('xay1', ((p, v) for p in plats_a for v in vecs), cat='Binary') - xay2 = pulp.LpVariable.dicts('xay2', ((p, v) for p in plats_a for v in vecs), cat='Binary') - xay3 = pulp.LpVariable.dicts('xay3', ((p, v) for p in plats_a for v in vecs), cat='Binary') - xay4 = pulp.LpVariable.dicts('xay4', ((p, v) for p in plats_a for v in vecs), cat='Binary') - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - for v in vecs: - prob += ay1[z] + x[z, v] >= 2 * xay1[z, v] - prob += ay2[z] + x[z, v] >= 2 * xay2[z, v] - prob += ay3[z] + x[z, v] >= 2 * xay3[z, v] - prob += ay4[z] + x[z, v] >= 2 * xay4[z, v] - - # Making sure always only 1 case applies - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - prob += ay1[z] + ay2[z] + ay3[z] + ay4[z] <= 1 - prob += y1[z] + y2[z] + y3[z] + y4[z] <= 1 - - # (4) Length constraint - # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length - # The length of the vehicles depends on whether they are angled or not and which vehicle is standing on - # platform v(p) - for L in plats_l: - prob += pulp.lpSum(x[p, v] * length_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][0]*length_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][1]*length_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(v_coef[class_list[v]][2]*length_list[v]) - - xay4[platforms_angled_array.index(p), v] - * int(v_coef[class_list[v]][3]*length_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) - for v in vecs)\ - + pulp.lpSum(x[p, v] * length_list[v] - for p in self.diffset(platforms_level_array[L], platforms_angled_array) - for v in vecs) \ - <= lmax_l[L] - - # (5) Platforms can not be angled, if they are part of a split platform - for q in plats_sp: - prob += pulp.lpSum(a_p[platforms_angled_array.index(p)] - for p in self.intersectset(platforms_angled_array, split_platforms_array[q]))\ - <= len(split_platforms_array[q]) * (1 - sp[q]) - - # (6) Weight constraint if split platform is used, gamma == 1 - for q in plats_sp: - prob += pulp.lpSum(sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) - for v in vecs) >= 2 * gamma[q] - - # If split platform is used, weight limit == wsp, if not, then weight limit == wp - for q in plats_sp: - for p in split_platforms_array[q]: - prob += (pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] + (1 - gamma[q]) * - wp[p]) - - # (7) If a platform that can be angled is angled, weight limit == wpa - # Need another linearization for that: - apx = pulp.LpVariable.dicts('apx', ((p, v) for p in plats_a for v in vecs), cat='Binary') - for p in platforms_angled_array: - z = platforms_angled_array.index(p) - for v in vecs: - prob += a_p[z] + x[z, v] >= 2 * apx[z, v] - - for p in platforms_angled_array: - prob += pulp.lpSum(weight_list[v] * apx[platforms_angled_array.index(p), v] for v in vecs) \ - <= wpa[platforms_angled_array.index(p)] - - # (8) Weight constraint for every level - for p_l in plats_l: - prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= - wl[p_l]) - - # (9) Weight constraint for truck and trailer - for p_t in plats_t: - prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) - <= wt[p_t]) - - # (10) Weight constraint for entire auto carrier - prob += pulp.lpSum(weight_list[v] * x[p, v] for p in plats for v in vecs) <= wmax - - # (11) Height constraints for truck and trailer, analogue to length constraints - # Truck - for h in plats_h1: - prob += pulp.lpSum(x[p, v] * height_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][0]*height_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][1]*height_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][2]*height_list[v]) - - xay4[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][3]*height_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) - for v in vecs)\ - + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) - for v in vecs) \ - <= hmax_truck[h] - # Trailer - for h in plats_h2: - prob += pulp.lpSum(x[p, v] * height_list[v] - - xay1[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][0]*height_list[v]) - - xay2[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][1]*height_list[v]) - - xay3[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][2]*height_list[v]) - - xay4[platforms_angled_array.index(p), v] * - int(h_coef[class_list[v]][3]*height_list[v]) - for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) - for v in vecs)\ - + pulp.lpSum(x[p, v] * height_list[v] - for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) - for v in vecs) \ - <= hmax_trailer[h] - - # Set the problem sense and name - problem_instance = prob.to_dict() - self.application = problem_instance - return self.application + prob += ay1[z] + x[z, v] >= 2 * xay1[z, v] + prob += ay2[z] + x[z, v] >= 2 * xay2[z, v] + prob += ay3[z] + x[z, v] >= 2 * xay3[z, v] + prob += ay4[z] + x[z, v] >= 2 * xay4[z, v] + + # Making sure always only 1 case applies + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + prob += ay1[z] + ay2[z] + ay3[z] + ay4[z] <= 1 + prob += y1[z] + y2[z] + y3[z] + y4[z] <= 1 + + # (4) Length constraint + # Checks that vehicles v on platforms p that belong to level L are shorter than the maximum available length + # The length of the vehicles depends on whether they are angled or not and which vehicle is standing on + # platform v(p) + for L in plats_l: + prob += pulp.lpSum(x[p, v] * length_list[v] + - xay1[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][0] * length_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][1] * length_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(v_coef[class_list[v]][2] * length_list[v]) + - xay4[platforms_angled_array.index(p), v] + * int(v_coef[class_list[v]][3] * length_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_level_array[L]) + for v in vecs) \ + + pulp.lpSum(x[p, v] * length_list[v] + for p in self.diffset(platforms_level_array[L], platforms_angled_array) + for v in vecs) \ + <= lmax_l[L] + + # (5) Platforms can not be angled, if they are part of a split platform + for q in plats_sp: + prob += pulp.lpSum(a_p[platforms_angled_array.index(p)] + for p in self.intersectset(platforms_angled_array, split_platforms_array[q])) \ + <= len(split_platforms_array[q]) * (1 - sp[q]) + + # (6) Weight constraint if split platform is used, gamma == 1 + for q in plats_sp: + prob += pulp.lpSum(sp[q] + x[p, v] for p in self.intersectset(split_platforms_array[q], platforms_array) + for v in vecs) >= 2 * gamma[q] + + # If split platform is used, weight limit == wsp, if not, then weight limit == wp + for q in plats_sp: + for p in split_platforms_array[q]: + prob += (pulp.lpSum(weight_list[v] * x[p, v] for v in vecs) <= gamma[q] * wsp[q] + (1 - gamma[q]) * + wp[p]) + + # (7) If a platform that can be angled is angled, weight limit == wpa + # Need another linearization for that: + apx = pulp.LpVariable.dicts('apx', ((p, v) for p in plats_a for v in vecs), cat='Binary') + for p in platforms_angled_array: + z = platforms_angled_array.index(p) + for v in vecs: + prob += a_p[z] + x[z, v] >= 2 * apx[z, v] + + for p in platforms_angled_array: + prob += pulp.lpSum(weight_list[v] * apx[platforms_angled_array.index(p), v] for v in vecs) \ + <= wpa[platforms_angled_array.index(p)] + + # (8) Weight constraint for every level + for p_l in plats_l: + prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_level_array[p_l] for v in vecs) <= + wl[p_l]) + + # (9) Weight constraint for truck and trailer + for p_t in plats_t: + prob += (pulp.lpSum(weight_list[v] * x[p, v] for p in platforms_truck_trailer_array[p_t] for v in vecs) + <= wt[p_t]) + + # (10) Weight constraint for entire auto carrier + prob += pulp.lpSum(weight_list[v] * x[p, v] for p in plats for v in vecs) <= wmax + + # (11) Height constraints for truck and trailer, analogue to length constraints + # Truck + for h in plats_h1: + prob += pulp.lpSum(x[p, v] * height_list[v] + - xay1[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][0] * height_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][1] * height_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][2] * height_list[v]) + - xay4[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][3] * height_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_height_array_truck[h]) + for v in vecs) \ + + pulp.lpSum(x[p, v] * height_list[v] + for p in self.diffset(platforms_height_array_truck[h], platforms_angled_array) + for v in vecs) \ + <= hmax_truck[h] + # Trailer + for h in plats_h2: + prob += pulp.lpSum(x[p, v] * height_list[v] + - xay1[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][0] * height_list[v]) + - xay2[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][1] * height_list[v]) + - xay3[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][2] * height_list[v]) + - xay4[platforms_angled_array.index(p), v] * + int(h_coef[class_list[v]][3] * height_list[v]) + for p in self.intersectset(platforms_angled_array, platforms_height_array_trailer[h]) + for v in vecs) \ + + pulp.lpSum(x[p, v] * height_list[v] + for p in self.diffset(platforms_height_array_trailer[h], platforms_angled_array) + for v in vecs) \ + <= hmax_trailer[h] + + self.application = prob + + def _get_vehicle_params(self, df: any, vehicles: list) -> tuple[list, list, list, list]: + """ + Extract vehicle parameters for the problem formulation - def validate(self, solution:any) -> (bool, float): + :param df: Dataframe containing vehicle data + :param vehicles: List of vehicle types to consider + :return: Lists containing class, length, height, and weight of vehicles """ - Checks if the solution is a valid solution + class_list = [0] * (len(vehicles)) + length_list = [0] * (len(vehicles)) + height_list = [0] * (len(vehicles)) + weight_list = [0] * (len(vehicles)) + + for i in set(range(len(vehicles))): + df_new = df.loc[df['Type'] == vehicles[i]] + class_list[i] = int(df_new["Class"].iloc[0]) + length_list[i] = int(df_new["Length"].iloc[0]) + height_list[i] = int(df_new["Height"].iloc[0]) + weight_list[i] = int(df_new["Weight"].iloc[0]) + + return class_list, length_list, height_list, weight_list + + def validate(self, solution: any) -> tuple[bool, float]: + """ + Checks if the solution is a valid solution. : :param solution: Proposed solution - :type solution: any - :return: bool value if solution is valid and the time it took to validate the solution - :rtype: tuple(bool, float) + :return: Tuple containing a boolean indicating if the solution is valid + and the time it took to validate the solution """ - start = start_time_measurement() - status = solution["status"] - if status == 'Optimal': - return True, end_time_measurement(start) - else: - return False, end_time_measurement(start) + status = solution.get("status") + is_valid = status == "Optimal" + return is_valid, end_time_measurement(start) def get_solution_quality_unit(self) -> str: + """ + Provides the unit of measure for solution quality. + + :return: The unit of measure fro solution quality + """ return "Number of loaded vehicles" - def evaluate(self, solution: any) -> (float, float): + def evaluate(self, solution: any) -> tuple[float, float]: """ - Checks how good the solution is + Checks how good the solution is. :param solution: Provided solution - :type solution: any - :return: Evaluation and the time it took to create it - :rtype: tuple(any, float) - + :return: Tuple containing the objective value and the time it took to evaluate the solution """ start = start_time_measurement() - objective_value = solution["obj_value"] + objective_value = solution.get("obj_value", 0) logging.info("Loading successful!") logging.info(f"{objective_value} cars will fit on the auto carrier.") - variables = solution["variables"] - assignments = [] - # Check which decision variables are equal to 1 - for key in variables: - if variables[key] > 0: - assignments.append(key) + + variables = solution.get("variables", {}) + assignments = [key for key in variables if variables[key] > 0] + logging.info(f"vehicle-to-platform assignments (platform, vehicle): {assignments}") return objective_value, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the problem instance to a JSON file. + + :param path: Directory path where the instance should be saved + :param iter_count: Iteration count (unused) + """ # Convert our problem instance from Dict to an LP problem and then to json _, problem_instance = pulp.LpProblem.from_dict(self.application) - # Save problem instance to json + # Save problem instance to JSON problem_instance.to_json(f"{path}/ACL_instance.json") diff --git a/src/modules/applications/optimization/ACL/__init__.py b/src/modules/applications/optimization/ACL/__init__.py index b9f77189..b22c875d 100644 --- a/src/modules/applications/optimization/ACL/__init__.py +++ b/src/modules/applications/optimization/ACL/__init__.py @@ -12,4 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing the ACL""" +""" +Module containing the ACL + +This module initializes the ACL application, which is responsible for formulating +and solving the ACL problem using various mappings and solvers. +""" diff --git a/src/modules/applications/optimization/ACL/mappings/ISING.py b/src/modules/applications/optimization/ACL/mappings/ISING.py index d8f5bbee..ee4722a7 100644 --- a/src/modules/applications/optimization/ACL/mappings/ISING.py +++ b/src/modules/applications/optimization/ACL/mappings/ISING.py @@ -11,66 +11,55 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import logging from typing import TypedDict -from more_itertools import locate import numpy as np +from more_itertools import locate from qiskit_optimization import QuadraticProgram from qiskit_optimization.converters import QuadraticProgramToQubo -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class Ising(Mapping): """ Ising formulation of the auto-carrier loading (ACL) problem. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "QiskitQAOA"] - self.global_variables = 0 + self.global_variables = [] logging.warning("Currently, all scenarios are too large to be solved with an Ising model.") logging.warning("Consider using another mapping until the modelling is refined.") @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "more-itertools", - "version": "10.5.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, + {"name": "numpy", "version": "1.26.4"}, + {"name": "more-itertools", "version": "10.5.0"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, ] - def get_parameter_options(self): + def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - } + return {} class Config(TypedDict): """ @@ -78,18 +67,17 @@ class Config(TypedDict): """ pass - def map_pulp_to_qiskit(self, problem: any): + def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: """ Maps the problem dict to a quadratic program. :param problem: Problem formulation in dict form - :type problem: dict - :return: quadratic program in qiskit-optimization format - :rtype: QuadraticProgram + :return: Quadratic program in qiskit-optimization format """ # Details at: # https://coin-or.github.io/pulp/guides/how_to_export_models.html # https://qiskit.org/documentation/stable/0.26/tutorials/optimization/2_converters_for_quadratic_programs.html + qp = QuadraticProgram() # Variables @@ -106,11 +94,7 @@ def map_pulp_to_qiskit(self, problem: any): qp.integer_var(lowerbound=lb, upperbound=ub, name=name) # Objective function - # Arguments: - obj_arguments = {} - for arg in problem["objective"]["coefficients"]: - obj_arguments[arg["name"]] = arg["value"] - + obj_arguments = {arg["name"]: arg["value"] for arg in problem["objective"]["coefficients"]} # Maximize if problem["parameters"]["sense"] == -1: qp.maximize(linear=obj_arguments) @@ -120,88 +104,82 @@ def map_pulp_to_qiskit(self, problem: any): # Constraints for constraint in problem["constraints"]: - const_arguments = {} - for arg in constraint["coefficients"]: - const_arguments[arg["name"]] = arg["value"] + const_arguments = {arg["name"]: arg["value"] for arg in constraint["coefficients"]} sense = constraint["sense"] - if sense == -1: - const_sense = "LE" - elif sense == 1: - const_sense = "GE" - else: - const_sense = "E" - qp.linear_constraint(linear=const_arguments, sense=const_sense, rhs=-1 * constraint["constant"], - name=constraint["name"]) + const_sense = "LE" if sense == -1 else "GE" if sense == 1 else "E" + qp.linear_constraint( + linear=const_arguments, + sense=const_sense, + rhs=-1 * constraint["constant"], + name=constraint["name"] + ) + return qp - def map(self, problem: any, config: Config) -> (dict, float): + def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ - Use Ising mapping of qiskit-optimize - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) + Use Ising mapping of qiskit-optimize. + + :param problem: Dict containing the problem parameters + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ start = start_time_measurement() # Map Linear problem from dictionary (generated by pulp) to quadratic program qp = self.map_pulp_to_qiskit(problem) - # print(qp.prettyprint()) logging.info(qp.export_as_lp_string()) # convert quadratic problem to qubo to ising conv = QuadraticProgramToQubo() qubo = conv.convert(qp) - # get variables - variables = [] - for variable in qubo.variables: - variables.append(variable.name) - qubitOp, _ = qubo.to_ising() + variables = [variable.name for variable in qubo.variables] + qubit_op, _ = qubo.to_ising() self.global_variables = variables # reverse generate J and t out of qubit PauliSumOperator from qiskit - t_matrix = np.zeros(qubitOp.num_qubits, dtype=complex) - j_matrix = np.zeros((qubitOp.num_qubits, qubitOp.num_qubits), dtype=complex) - - for i in qubitOp: - pauli_str, coeff = i.primitive.to_list()[0] - logging.info((pauli_str, coeff)) - pauli_str_list = list(pauli_str) - index_pos_list = list(locate(pauli_str_list, lambda a: a == 'Z')) + t_matrix = np.zeros(qubit_op.num_qubits, dtype=complex) + j_matrix = np.zeros((qubit_op.num_qubits, qubit_op.num_qubits), dtype=complex) + + for pauli_op in qubit_op: + pauli_str, coeff = pauli_op.primitive.to_list()[0] + index_pos_list = list(locate(pauli_str, lambda a: a == 'Z')) + if len(index_pos_list) == 1: - # update t t_matrix[index_pos_list[0]] = coeff elif len(index_pos_list) == 2: j_matrix[index_pos_list[0]][index_pos_list[1]] = coeff return {"J": j_matrix, "t": t_matrix}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. - :param solution: bit_string containing the solution - :type solution: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: Dict with a bit_string containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() - if np.any(solution == "-1"): # ising model output from Braket QAOA + + if np.any(solution == "-1"): solution = self._convert_ising_to_qubo(solution) + result = {"status": [0]} variables = {} objective_value = 0 + for bit in solution: if solution[bit] > 0: - # We only care about assignments: if "x" in self.global_variables[bit]: variables[self.global_variables[bit]] = solution[bit] result["status"] = 'Optimal' objective_value += solution[bit] + result["variables"] = variables result["obj_value"] = objective_value + return result, end_time_measurement(start) @staticmethod @@ -214,6 +192,13 @@ def _convert_ising_to_qubo(solution: any) -> any: return solution def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/ACL/mappings/QUBO.py b/src/modules/applications/optimization/ACL/mappings/QUBO.py index 6546ae37..27adacf1 100644 --- a/src/modules/applications/optimization/ACL/mappings/QUBO.py +++ b/src/modules/applications/optimization/ACL/mappings/QUBO.py @@ -14,58 +14,53 @@ from typing import TypedDict import re +import logging import numpy as np from qiskit_optimization import QuadraticProgram -from qiskit_optimization.converters import (QuadraticProgramToQubo, InequalityToEquality, IntegerToBinary, - LinearEqualityToPenalty) +from qiskit_optimization.converters import ( + QuadraticProgramToQubo, InequalityToEquality, IntegerToBinary, + LinearEqualityToPenalty +) -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement +# TODO Large chunks of this code is duplicated in ACL.mappings.ISING -> unify + class Qubo(Mapping): """ QUBO formulation for the ACL. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] - self.global_variables = 0 + self.global_variables = [] @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, + {"name": "numpy", "version": "1.26.4"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, ] - def get_parameter_options(self): + def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. - :return: empty dictionary - :rtype: dict + :return: Empty dictionary """ - return { - } + return {} class Config(TypedDict): """ @@ -73,18 +68,17 @@ class Config(TypedDict): """ pass - def map_pulp_to_qiskit(self, problem: any): + def map_pulp_to_qiskit(self, problem: dict) -> QuadraticProgram: """ Maps the problem dict to a quadratic program. :param problem: Problem formulation in dict form - :type problem: dict - :return: quadratic program in qiskit-optimization format - :rtype: QuadraticProgram + :return: Quadratic program in qiskit-optimization format """ # Details at: # https://coin-or.github.io/pulp/guides/how_to_export_models.html # https://qiskit.org/documentation/stable/0.26/tutorials/optimization/2_converters_for_quadratic_programs.html + qp = QuadraticProgram() # Variables @@ -101,10 +95,7 @@ def map_pulp_to_qiskit(self, problem: any): qp.integer_var(lowerbound=lb, upperbound=ub, name=name) # Objective function - # Arguments: - obj_arguments = {} - for arg in problem["objective"]["coefficients"]: - obj_arguments[arg["name"]] = arg["value"] + obj_arguments = {arg["name"]: arg["value"] for arg in problem["objective"]["coefficients"]} # Maximize if problem["parameters"]["sense"] == -1: @@ -115,37 +106,35 @@ def map_pulp_to_qiskit(self, problem: any): # Constraints for constraint in problem["constraints"]: - const_arguments = {} - for arg in constraint["coefficients"]: - const_arguments[arg["name"]] = arg["value"] + const_arguments = {arg["name"]: arg["value"] for arg in constraint["coefficients"]} sense = constraint["sense"] - if sense == -1: - const_sense = "LE" - elif sense == 1: - const_sense = "GE" - else: - const_sense = "E" - qp.linear_constraint(linear=const_arguments, sense=const_sense, rhs=-1 * constraint["constant"], - name=constraint["name"]) + const_sense = "LE" if sense == -1 else "GE" if sense == 1 else "E" + + qp.linear_constraint( + linear=const_arguments, + sense=const_sense, + rhs=-1 * constraint["constant"], + name=constraint["name"] + ) + return qp - def convert_string_to_arguments(self, input_string: str): + def convert_string_to_arguments(self, input_string: str) -> list[any]: """ - Converts QUBO in string format to a list of separated arguments, used to construct the QUBO matrix. + Converts QUBO in string format to a list of separated arguments, + used to construct the QUBO matrix. :param input_string: QUBO in raw string format - :type input_string: str - :return: list of arguments - :rtype: list + :return: List of arguments """ terms = re.findall(r'[+\-]?[^+\-]+', input_string) # Convert the penalty string to a list of lists of the individual arguments in the penalty term result = [term.strip() for term in terms] separated_arguments = [] first_item = True - # Loop over all arguments in the penalty + for argument in result: - if first_item is True: + if first_item: # Remove "maximize" or minimize string from the first argument argument = argument[8:] first_item = False @@ -155,25 +144,22 @@ def convert_string_to_arguments(self, input_string: str): # Convert string of numbers to floats new_argument = elements[0].strip() # Remove empty strings - new_argument = [int(new_argument.replace(" ", "")) if new_argument.replace(" ", "").isdigit() else - float(new_argument.replace(" ", ""))] - for el in elements[1:]: - new_argument += [el.strip()] + new_argument = [int(new_argument.replace(" ", "")) if new_argument.replace(" ", "").isdigit() + else float(new_argument.replace(" ", ""))] + new_argument += [el.strip() for el in elements[1:]] separated_arguments.append(new_argument) else: separated_arguments.append(argument) + return separated_arguments - def construct_qubo(self, penalty: list[list], variables: list): + def construct_qubo(self, penalty: list[list], variables: list[str]) -> np.ndarray: """ - Creates QUBO matrix Q to solve linear problem of the form x^T * Q + x + Creates QUBO matrix Q to solve linear problem of the form x^T * Q + x. - :param penalty: list of lists containing all non-zero elements of the QUBO matrix as strings - :type penalty: list - :param variables: listing of all variables used in the problem - :type variables: list + :param penalty: List of lists containing all non-zero elements of the QUBO matrix as strings + :param variables: Listing of all variables used in the problem :return: QUBO in numpy array format - :rtype: array """ # Create empty qubo matrix count_variables = len(variables) @@ -185,91 +171,103 @@ def construct_qubo(self, penalty: list[list], variables: list): # Save the parameters (values in the qubo) parameter = 0 for argument in penalty: - if type(argument) is list: + if isinstance(argument, list): # squared variables in diagonals (x^2 == x) - if len(argument) == 2: - if any(isinstance(elem, str) and variable in elem for elem in argument) and col == row: - parameter += argument[0] + if ( + len(argument) == 2 + and any(isinstance(elem, str) and variable in elem for elem in argument) + and col == row + ): + parameter += argument[0] # Multiplication of different variables not on diagonal - if len(argument) == 3: - if variable in argument and variable2 in argument and variable > variable2: - parameter += argument[0] - # this value is already taking into account the factor 2 from quadratic term - # For the variables on the diagonal, if the parameter is zero, we still have to check the sign in - # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. - elif type(argument) is str: - if variable in argument and variable2 in argument and variable == variable2: - if "-" in argument: - parameter += -1 + if ( + len(argument) == 3 + and variable in argument and variable2 in argument and variable > variable2 + ): + parameter += argument[0] + # This value is already taking into account the factor 2 from quadratic term + # For the variables on the diagonal, if the parameter is zero + # We still have to check the sign in + # front of the decision variable. If it is "-", we have to put "-1" on the diagonal. + elif (isinstance(argument, str) and variable in argument + and variable2 in argument and variable == variable2): + if "-" in argument: + parameter += -1 + qubo[col, row] = parameter + # Minimization problem qubo = -qubo.astype(int) return qubo - def map(self, problem: any, config: Config) -> (dict, float): + def map(self, problem: dict, config: Config) -> tuple[dict, float]: """ - Use Ising mapping of qiskit-optimize - Converts linear program created with pulp to quadratic program to Ising with qiskit to QUBO matrix + Converts linear program created with pulp to quadratic program to Ising with qiskit to QUBO matrix. - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) + :param problem: Dict containing the problem parameters + :param config: Config with the parameters specified in Config class + :return: Dict with the QUBO, time it took to map it """ start = start_time_measurement() # Map Linear problem from dictionary (generated by pulp) to quadratic program to QUBO qp = self.map_pulp_to_qiskit(problem) - # print(qp.prettyprint()) logging.info(qp.export_as_lp_string()) + ineq2eq = InequalityToEquality() qp_eq = ineq2eq.convert(qp) + int2bin = IntegerToBinary() qp_eq_bin = int2bin.convert(qp_eq) + lineq2penalty = LinearEqualityToPenalty(100) qubo = lineq2penalty.convert(qp_eq_bin) - # get variables - variables = [] - for variable in qubo.variables: - variables.append(variable.name) + variables = [variable.name for variable in qubo.variables] # convert penalty term to string to QUBO qubo_string = str(qubo.objective) arguments = self.convert_string_to_arguments(qubo_string) - qubo = self.construct_qubo(arguments, variables) + qubo_matrix = self.construct_qubo(arguments, variables) self.global_variables = variables - return {"Q": qubo}, end_time_measurement(start) + return {"Q": qubo_matrix}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the ACL class for validation/evaluation. :param solution: bit_string containing the solution - :type solution: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() + result = {"status": [0]} objective_value = 0 variables = {} for bit in solution: - if solution[bit] > 0: + if solution[bit] > 0 and "x" in self.global_variables[bit]: # We only care about assignments of vehicles to platforms: # We map the solution to the original variables - if "x" in self.global_variables[bit]: - variables[self.global_variables[bit]] = solution[bit] - result["status"] = 'Optimal' # TODO: I do not think every solution with at least one car is optimal - objective_value += solution[bit] + variables[self.global_variables[bit]] = solution[bit] + result["status"] = 'Optimal' # TODO: I do not think every solution with at least one car is optimal + objective_value += solution[bit] + result["variables"] = variables result["obj_value"] = objective_value + return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/ACL/mappings/__init__.py b/src/modules/applications/optimization/ACL/mappings/__init__.py index b38557a3..d7364160 100644 --- a/src/modules/applications/optimization/ACL/mappings/__init__.py +++ b/src/modules/applications/optimization/ACL/mappings/__init__.py @@ -12,4 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for ACL mappings""" +""" +Module for ACL mappings. + +This module provides initializations for ACL related +mappings that are used in the QUARK framework. +""" diff --git a/src/modules/applications/optimization/MIS/MIS.py b/src/modules/applications/optimization/MIS/MIS.py index dbdf90e8..7636cef9 100644 --- a/src/modules/applications/optimization/MIS/MIS.py +++ b/src/modules/applications/optimization/MIS/MIS.py @@ -12,15 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict import pickle +import logging +from typing import TypedDict -import networkx +import networkx as nx -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization -from modules.applications.optimization.MIS.data.graph_layouts import \ - generate_hexagonal_graph +from modules.applications.optimization.MIS.data.graph_layouts import generate_hexagonal_graph from utils import start_time_measurement, end_time_measurement # define R_rydberg @@ -29,15 +29,24 @@ 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. + subset of vertices in a graph such that no two vertices are adjacent. MIS has numerous application in computer + science, network design, resource allocation, and even in physics, where finding optimal configurations can + solve fundamental problems related to stability and energy minimization. + + In a graph, the maximum independent set represents a set of nodes such that no two nodes share an edge. This + property makes it a key element in various optimization scenarios. Due to the problem's combinatorial nature, + it becomes computationally challenging, especially for large graphs, often requiring heuristic or approximate + solutions. + + In the context of QUARK, we employ quantum-inspired approaches and state-of-the-art classical algorithms to + tackle the problem. The graph is generated based on user-defined parameters such as size, spacing, and + filling fraction, which affect the complexity and properties of the generated instance. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("MIS") self.submodule_options = ["NeutralAtom"] @@ -45,18 +54,28 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - ] + return [] def get_solution_quality_unit(self) -> str: + """ + Returns the unit of measurement for solution quality. + + :return: The unit of measure for solution quality + """ return "Set size" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "NeutralAtom": from modules.applications.optimization.MIS.mappings.NeutralAtom import NeutralAtom # pylint: disable=C0415 return NeutralAtom() @@ -65,26 +84,26 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. - :return: - .. code-block:: python + :return: Configuration dictionary for this application + .. code-block:: python - return { - "size": { - "values": list(range(1, 18)), - "description": "How large should your graph be?" - }, - "spacing": { - "values": [x/10 for x in range(1, 11)], - "description": "How much space do you want between your nodes," - " relative to Rydberg distance?" - }, - "filling_fraction": { - "values": [x/10 for x in range(1, 11)], - "description": "What should the filling fraction be?" - }, - } + return { + "size": { + "values": list(range(1, 18)), + "description": "How large should your graph be?" + }, + "spacing": { + "values": [x/10 for x in range(1, 11)], + "description": "How much space do you want between your nodes," + " relative to Rydberg distance?" + }, + "filling_fraction": { + "values": [x/10 for x in range(1, 11)], + "description": "What should the filling fraction be?" + }, + } """ return { @@ -96,15 +115,14 @@ def get_parameter_options(self) -> dict: "description": "How large should your graph be?" }, "spacing": { - "values": [x/10 for x in range(3, 11, 2)], + "values": [x / 10 for x in range(3, 11, 2)], "custom_input": True, "allow_ranges": True, "postproc": float, - "description": "How much space do you want between your nodes," - " relative to Rydberg distance?" + "description": "How much space do you want between your nodes,relative to Rydberg distance?" }, "filling_fraction": { - "values": [x/10 for x in range(2, 11, 2)], + "values": [x / 10 for x in range(2, 11, 2)], "custom_input": True, "allow_ranges": True, "postproc": float, @@ -114,39 +132,29 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - - size: int - spacing: float - filling_fraction: float + Configuration attributes for generating an MIS problem. + Attributes: + size (int): The number of nodes in the graph. + spacing (float): The spacing between nodes in the graph. + filling_fraction (float): The fraction of available places in the lattice filled with nodes """ size: int spacing: float filling_fraction: float - def generate_problem(self, config: Config) -> networkx.Graph: + def generate_problem(self, config: Config) -> nx.Graph: """ - Generates a graph to solve the MIS for. + Generates a graph to solve the MIS problem for. :param config: Config specifying the size and connectivity for the problem - :type config: Config - :return: networkx graph representing the problem - :rtype: networkx.Graph + :return: Networkx graph representing the problem """ - 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'] - ) + config = {"size": 3, "spacing": 1, "filling_fraction": 0.5} + + # Ensure config has the necessary information + assert all(key in config for key in ['size', 'spacing', 'filling_fraction']) size = config.get('size') spacing = config.get('spacing') * R_rydberg @@ -158,8 +166,7 @@ def generate_problem(self, config: Config) -> networkx.Graph: filling_fraction=filling_fraction, ) - logging.info("Created MIS problem with the generate hexagonal " - "graph method, with the following attributes:") + logging.info("Created MIS problem with the generate hexagonal graph method, with the following attributes:") logging.info(f" - Graph size: {size}") logging.info(f" - Spacing: {spacing}") logging.info(f" - Filling fraction: {filling_fraction}") @@ -167,27 +174,22 @@ def generate_problem(self, config: Config) -> networkx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: list) -> (list, float): + def process_solution(self, solution: list) -> tuple[list, float]: """ - Returns list of visited nodes and the time it took to process the solution + Returns list of visited nodes and the time it took to process the solution. :param solution: Unprocessed solution - :type solution: list :return: Processed solution and the time it took to process it - :rtype: tuple(list, float) """ start_time = start_time_measurement() - return solution, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: list) -> tuple[bool, float]: """ - Checks if the solution is an independent set + Checks if the solution is an independent set. :param solution: List containing the nodes of the solution - :type solution: list :return: Boolean whether the solution is valid and time it took to validate - :rtype: tuple(bool, float) """ start = start_time_measurement() is_valid = True @@ -215,8 +217,8 @@ def validate(self, solution: list) -> (bool, float): is_valid = False # Check if the solution is a subset of the original nodes - is_set = all(node in nodes for node in solution) - if is_set: + is_subset = all(node in nodes for node in solution) + if is_subset: logging.info("The solution is a subset of the problem") else: logging.warning("The solution is not a subset of the problem") @@ -224,14 +226,12 @@ def validate(self, solution: list) -> (bool, float): return is_valid, end_time_measurement(start) - def evaluate(self, solution: list) -> (int, float): + def evaluate(self, solution: list) -> tuple[int, float]: """ - Calculates the size of the solution + Calculates the size of the solution. :param solution: List containing the nodes of the solution - :type solution: list :return: Set size, time it took to calculate the set size - :rtype: tuple(int, float) """ start = start_time_measurement() set_size = len(solution) @@ -241,5 +241,11 @@ def evaluate(self, solution: list) -> (int, float): return set_size, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the generated problem graph to a file. + + :param path: Path to save the problem graph + :param iter_count: Iteration count for file versioning + """ with open(f"{path}/graph_iter_{iter_count}.gpickle", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/MIS/__init__.py b/src/modules/applications/optimization/MIS/__init__.py index e808d010..b7ca66cc 100644 --- a/src/modules/applications/optimization/MIS/__init__.py +++ b/src/modules/applications/optimization/MIS/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for MIS""" +""" +Module for MIS mappings + +This module initialize the MIS package +""" diff --git a/src/modules/applications/optimization/MIS/data/__init__.py b/src/modules/applications/optimization/MIS/data/__init__.py index 310100cb..1afbe94e 100644 --- a/src/modules/applications/optimization/MIS/data/__init__.py +++ b/src/modules/applications/optimization/MIS/data/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for MIS data""" +""" +Module for MIS data + +This module initialize the MIS package +""" diff --git a/src/modules/applications/optimization/MIS/data/graph_layouts.py b/src/modules/applications/optimization/MIS/data/graph_layouts.py index 44695408..c66abded 100644 --- a/src/modules/applications/optimization/MIS/data/graph_layouts.py +++ b/src/modules/applications/optimization/MIS/data/graph_layouts.py @@ -15,63 +15,51 @@ import math import random -import networkx +import networkx as nx import pulser # define R_rydberg R_rydberg = 9.75 -def generate_hexagonal_graph(n_nodes:int, spacing:float, - filling_fraction:float=1.0) -> networkx.Graph: - """ - 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) +def generate_hexagonal_graph(n_nodes: int, spacing: float, filling_fraction: float = 1.0) -> nx.Graph: + """ + Generate a hexagonal graph layout based on the number of nodes and spacing. - Returns: - Graph: networkx Graph representing the hexagonal graph layout. + :param n_nodes: The number of nodes in the graph + :param spacing: The spacing between nodes (atoms) + :param filling_fraction: The fraction of available places in the lattice to be filled with nodes. (default: 1.0) + :return: Networkx Graph representing the hexagonal graph layout """ - if 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) + if not 0.0 < filling_fraction <= 1.0: + raise ValueError("The filling fraction must be in the domain of (0.0, 1.0].") + + # Create a layout large enough to contain the desired number of atoms at the filling fraction + n_traps = int(n_nodes / filling_fraction) hexagonal_layout = pulser.register.special_layouts.TriangularLatticeLayout( - n_traps=n_traps, spacing=spacing) + n_traps=n_traps, spacing=spacing + ) # Fill the layout with traps reg = hexagonal_layout.hexagonal_register(n_traps) ids = reg._ids # pylint: disable=W0212 - coords = reg._coords # pylint: disable=W0212 - coords = [l.tolist() for l in coords] + coords = [coord.tolist() for coord in reg._coords] # pylint: disable=W0212 traps = dict(zip(ids, coords)) # Remove random atoms to get the desired number of atoms - # This is needed if the filling fraction is below 1.0 while len(traps) > n_nodes: atom_to_remove = random.choice(list(traps)) traps.pop(atom_to_remove) # Rename the atoms - i = 0 - node_positions = {} - for trap in traps.keys(): # pylint: disable=C0206 - node_positions[i] = traps[trap] - i += 1 + node_positions = {i: traps[trap] for i, trap in enumerate(traps.keys())} # pylint: disable=C0206 # Create the graph - hexagonal_graph = networkx.Graph() + hexagonal_graph = nx.Graph() - # Add the nodes - for ID, coord in node_positions.items(): - hexagonal_graph.add_node(ID, pos=coord) + # Add nodes to the graph + for node_id, coord in node_positions.items(): + hexagonal_graph.add_node(node_id, pos=coord) # Generate the edges and add them to the graph edges = _generate_edges(node_positions=node_positions) @@ -79,44 +67,34 @@ def generate_hexagonal_graph(n_nodes:int, spacing:float, 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. + +def _generate_edges(node_positions: dict[list[int, list[float]]], radius: float = R_rydberg) -> list[tuple]: + """ + Generate edges between vertices within a given distance 'radius', which defaults to R_rydberg. + + :param node_positions: A dictionary with the node ids as keys, and the node coordinates as values + :param radius: When the distance between two nodes is smaller than this radius, an edge is generated between them + :return: A list of 2-tuples. Each 2-tuple contains two different node ids and represents an edge between those nodes """ edges = [] vertex_keys = list(node_positions.keys()) for i, vertex_key in enumerate(vertex_keys): - for neighbor_key in vertex_keys[i+1:]: - distance = _vertex_distance(node_positions[vertex_key], - node_positions[neighbor_key]) + for neighbor_key in vertex_keys[i + 1:]: + distance = _vertex_distance(node_positions[vertex_key], node_positions[neighbor_key]) if distance <= radius: edges.append((vertex_key, neighbor_key)) return edges -def _vertex_distance(v0: tuple, v1: tuple) -> float: + +def _vertex_distance(v0: tuple[float, ...], v1: tuple[float, ...]) -> float: """ Calculates distance between two n-dimensional vertices. - For 2 dimensions: distance = sqrt((x0-x1)**2 + (y0-y1)**2) + For 2 dimensions: distance = sqrt((x0 - x1)**2 + (y0 - y1)**2) + + :param v0: Coordinates of the first vertex + :param v1: Coordinates of the second vertex + return: Distance between the vertices """ - squared_difference = 0 - for coordinate0, coordinate1 in zip(v0, v1): - squared_difference += (coordinate0 -coordinate1)**2 + squared_difference = sum((coordinate0 - coordinate1) ** 2 for coordinate0, coordinate1 in zip(v0, v1)) + return math.sqrt(squared_difference) diff --git a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py index 6dedc239..04c1bd9b 100644 --- a/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py +++ b/src/modules/applications/optimization/MIS/mappings/NeutralAtom.py @@ -14,11 +14,10 @@ from typing import TypedDict -import networkx -import numpy as np +import networkx as nx import pulser -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -29,7 +28,7 @@ class NeutralAtom(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["NeutralAtomMIS"] @@ -37,63 +36,54 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of requirements of this module """ - return [ - { - "name": "pulser", - "version": "0.19.0" - } - ] + return [{"name": "pulser", "version": "0.19.0"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return {} + Returns the configurable settings for this mapping. + :return: Empty dictionary, as this mapping has no configurable settings """ return {} class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - pass + Configuration options for Neutral Atom MIS mapping. """ pass - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to a neutral atom MIS problem. - :param problem: networkx graph - :type problem: networkx.Graph - :param 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) + :param problem: Networkx graph representing the MIS problem + :param config: Config with the parameters specified in Config class + :return: Tuple containing a dictionary with the neutral MIS and time it took to map it """ start = start_time_measurement() - pos = networkx.get_node_attributes(problem, 'pos') + pos = nx.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: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "NeutralAtomMIS": from modules.solvers.NeutralAtomMIS import NeutralAtomMIS # pylint: disable=C0415 return NeutralAtomMIS() diff --git a/src/modules/applications/optimization/MIS/mappings/__init__.py b/src/modules/applications/optimization/MIS/mappings/__init__.py index a701f7ff..b7ca66cc 100644 --- a/src/modules/applications/optimization/MIS/mappings/__init__.py +++ b/src/modules/applications/optimization/MIS/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for MIS mappings""" +""" +Module for MIS mappings + +This module initialize the MIS package +""" diff --git a/src/modules/applications/optimization/Optimization.py b/src/modules/applications/optimization/Optimization.py index ec45bef0..aa09c6e0 100644 --- a/src/modules/applications/optimization/Optimization.py +++ b/src/modules/applications/optimization/Optimization.py @@ -11,129 +11,115 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from abc import ABC, abstractmethod +import logging -from modules.applications.Application import * +from modules.applications.Application import Application from utils import start_time_measurement, end_time_measurement class Optimization(Application, ABC): """ - Optimization Module for QUARK, is used by all Optimization applications + Optimization Module for QUARK, is used by all Optimization applications. """ @abstractmethod - def validate(self, solution) -> (bool, float): + def validate(self, solution: any) -> tuple[bool, float]: """ - Checks if the solution is a valid solution + Checks if the solution is a valid solution. :param solution: Proposed solution - :type solution: any - :return: bool value if solution is valid and the time it took to validate the solution - :rtype: tuple(bool, float) - + :return: Bool value if solution is valid and the time it took to validate the solution """ pass @abstractmethod def get_solution_quality_unit(self) -> str: """ - Returns the unit of the evaluation + Returns the unit of the evaluation. :return: String with the unit - :rtype: str """ pass @abstractmethod - def evaluate(self, solution: any) -> (float, float): + def evaluate(self, solution: any) -> tuple[float, float]: """ - Checks how good the solution is + Checks how good the solution is. :param solution: Provided solution - :type solution: any - :return: Evaluation and the time it took to create it - :rtype: tuple(any, float) - + :return: Tuple with the evaluation and the time it took to create it """ pass @abstractmethod - def generate_problem(self, config) -> any: + def generate_problem(self, config: dict) -> any: """ - Creates a concrete problem and returns it + Creates a concrete problem and returns it. - :param config: - :type config: dict - :return: - :rtype: any + :param config: Configuration for problem creation + :return: Generated problem """ pass - def process_solution(self, solution) -> (any, float): + def process_solution(self, solution: any) -> tuple[any, float]: """ Most of the time the solution has to be processed before it can be validated and evaluated. This might not be necessary in all cases, so the default is to return the original solution. :param solution: Proposed solution - :type solution: any - :return: Processed solution and the execution time to process it - :rtype: tuple(any, float) - + :return: Tuple with processed solution and the execution time to process it """ return solution, 0.0 - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ For optimization problems, we generate the actual problem instance in the preprocess function. :param input_data: Input data (usually not used in this method) - :type input_data: any :param config: Config for the problem creation - :type config: dict :param kwargs: Optional additional arguments - :type kwargs: dict :return: Tuple with output and the preprocessing time - :rtype: (any, float) """ start = start_time_measurement() output = self.generate_problem(config) return output, end_time_measurement(start) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ For optimization problems, we process the solution here, then validate and evaluate it. :param input_data: Data which should be evaluated for this optimization problem - :type input_data: any - :param config: Config - :type config: dict + :param config: Config for the problem creation :param kwargs: Optional additional arguments - :type kwargs: dict :return: Tuple with results and the postprocessing time - :rtype: (any, float) """ processed_solution = None try: - processed_solution, time_to_process_solution = self.process_solution( - input_data) - solution_validity, time_to_validation = self.validate( - processed_solution) + processed_solution, time_to_process_solution = self.process_solution(input_data) + solution_validity, time_to_validation = self.validate(processed_solution) except Exception as e: logging.exception(f"Exception on processing the solution: {e}") solution_validity = False time_to_process_solution = None time_to_validation = None + if solution_validity and (processed_solution is not None): solution_quality, time_to_evaluation = self.evaluate(processed_solution) else: solution_quality = None time_to_evaluation = None - self.metrics.add_metric_batch({"application_score_value": solution_quality, - "application_score_unit": self.get_solution_quality_unit(), - "application_score_type": str(float), - "processed_solution": processed_solution, - "time_to_process_solution": time_to_process_solution, - "time_to_validation": time_to_validation, - "time_to_evaluation": time_to_evaluation}) - return solution_validity, sum(filter(None, [time_to_process_solution, time_to_validation, time_to_evaluation])) + self.metrics.add_metric_batch({ + "application_score_value": solution_quality, + "application_score_unit": self.get_solution_quality_unit(), + "application_score_type": str(float), + "processed_solution": processed_solution, + "time_to_process_solution": time_to_process_solution, + "time_to_validation": time_to_validation, + "time_to_evaluation": time_to_evaluation + }) + + return solution_validity, sum(filter(None, [ + time_to_process_solution, time_to_validation, time_to_evaluation + ])) diff --git a/src/modules/applications/optimization/PVC/PVC.py b/src/modules/applications/optimization/PVC/PVC.py index abe3c7b8..fb04f1a1 100644 --- a/src/modules/applications/optimization/PVC/PVC.py +++ b/src/modules/applications/optimization/PVC/PVC.py @@ -15,61 +15,71 @@ import itertools from typing import TypedDict import pickle +import logging +import os import networkx as nx import numpy as np -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement class PVC(Optimization): """ - In modern vehicle manufacturing, robots - take on a significant workload, including performing welding - jobs, sealing welding joints, or applying paint to the car body. - While the robot’s tasks vary widely, the objective remains - the same: Perform a job with the highest possible quality in the - shortest amount of time. For instance, to protect a car’s underbody - from corrosion, exposed welding seams are sealed by applying - a polyvinyl chloride layer (PVC). The welding seams need to be - traversed by a robot to apply the material. - It is related to TSP, but different and even more complex in some - aspects. + In modern vehicle manufacturing, robots take on a significant workload, including performing welding + jobs, sealing welding joints, or applying paint to the car body. While the robot’s tasks vary widely, + the objective remains the same: Perform a job with the highest possible quality in the shortest amount + of time, optimizing efficiency and productivity on the manufacturing line. + + For instance, to protect a car’s underbody from corrosion, exposed welding seams are sealed + by applying a polyvinyl chloride layer (PVC). The welding seams need to be traversed by a robot to + apply the material. It is related to TSP, but different and even more complex in some aspects. + + The problem of determining the optimal route for robots to traverse all seams shares similarities + with Traveling Salesman Problem (TSP), as it involves finding the shortest possible route to + visit multiple locations. However, it introduces additional complexities, such as different tool + and configuration requirements for each seam, making it an even more challenging problem to solve. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("PVC") - self.submodule_options = ["Ising", "QUBO", "GreedyClassicalPVC", "ReverseGreedyClassicalPVC", "RandomPVC"] + self.submodule_options = [ + "Ising", "QUBO", "GreedyClassicalPVC", "ReverseGreedyClassicalPVC", "RandomPVC" + ] @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_solution_quality_unit(self) -> str: + """ + Returns the unit of measure for solution quality. + + :return: Unit of measure for solution quality + """ return "Tour cost" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Ising": from modules.applications.optimization.PVC.mappings.ISING import Ising # pylint: disable=C0415 return Ising() @@ -90,49 +100,42 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. - :return: - .. code-block:: python - - return { - "seams": { - "values": list(range(1, 18)), - # Currently the graph can only be as large as the reference input graph - "description": "How many seams does your graph need?" - } - } + :return: Dictionary containing parameter options + .. code-block:: python + return { + "seams": { + "values": list(range(1, 18)), + "description": "How many seams should be sealed?" + } + } """ return { "seams": { "values": list(range(1, 18)), # In the current implementation the graph can only be as large as the reference input graph - "description": "How many seams does you graph need?" + "description": "How many seams should be sealed?" } } class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - - seams: int + Configuration attributes for PVC problem generation. + Attributes: + seams (int): Number of seams for the graph """ seams: int def generate_problem(self, config: Config) -> nx.Graph: """ - Uses the reference graph to generate a problem for a given config + Uses the reference graph to generate a problem for a given config. :param config: Config specifying the number of seams for the problem - :type config: Config - :return: networkx graph representing the problem - :rtype: networkx.Graph + :return: Networkx graph representing the problem """ - if config is None: config = {"seams": 3} seams = config['seams'] @@ -141,19 +144,17 @@ def generate_problem(self, config: Config) -> nx.Graph: with open(os.path.join(os.path.dirname(__file__), "data", "reference_graph.gpickle"), "rb") as file: graph = pickle.load(file) - # Remove seams until the target number of seams is reached # Get number of seam in graph seams_in_graph = list({x[0] for x in graph.nodes}) seams_in_graph.sort() - # Remove 0 as we always need the base node 0 (which is not a seam anyway) - seams_in_graph.remove(0) + seams_in_graph.remove(0) # Always need the base node 0 (which is not a seam) if len(seams_in_graph) < seams: - raise ValueError("Too many seams! The original graph has less seams than that!") + logging.info("Too many seams! The original graph has less seams than that!") unwanted_seams = seams_in_graph[-len(seams_in_graph) + seams:] unwanted_nodes = [x for x in graph.nodes if x[0] in unwanted_seams] - # Remove one node after another + for node in unwanted_nodes: graph.remove_node(node) @@ -161,30 +162,34 @@ def generate_problem(self, config: Config) -> nx.Graph: logging.error("Graph is not connected!") raise ValueError("Graph is not connected!") + # Gather unique configurations and tools config = [x[2]['c_start'] for x in graph.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in graph.edges(data=True)])) - tool = [x[2]['t_start'] for x in graph.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in graph.edges(data=True)])) - # Now lets fill the rest of the missing edges with high values - # get current edges - current_edges = [(edge[0], edge[1], edge[2]['t_start'], edge[2]['t_end'], edge[2]['c_start'], edge[2]['c_end']) - for edge in graph.edges(data=True)] - - # get all possible edges + # Fill the rest of the missing edges with high values + current_edges = [ + (edge[0], edge[1], edge[2]['t_start'], edge[2]['t_end'], edge[2]['c_start'], edge[2]['c_end']) + for edge in graph.edges(data=True) + ] all_possible_edges = list(itertools.product(list(graph.nodes), repeat=2)) - all_possible_edges = [(edges[0], edges[1], t_start, t_end, c_start, c_end) for edges in all_possible_edges for - c_end in config for c_start in config for t_end in tool - for t_start in tool if edges[0] != edges[1]] - # calculate missing edges + all_possible_edges = [ + (edges[0], edges[1], t_start, t_end, c_start, c_end) + for edges in all_possible_edges + for c_end in config + for c_start in config + for t_end in tool + for t_start in tool if edges[0] != edges[1] + ] + missing_edges = [item for item in all_possible_edges if item not in current_edges] - # add these edges with very high values + + # Add these edges with very high values for edge in missing_edges: - weight = 100000 # TODO Check if this value is fine - # c_start, t_start, c_end, t_end - graph.add_edge(edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], - weight=weight) + graph.add_edge( + edge[0], edge[1], c_start=edge[4], t_start=edge[2], c_end=edge[5], t_end=edge[3], weight=100000 + ) logging.info("Created PVC problem with the following attributes:") logging.info(f" - Number of seams: {seams}") @@ -194,25 +199,23 @@ def generate_problem(self, config: Config) -> nx.Graph: self.application = graph return graph.copy() - def process_solution(self, solution: dict) -> (list, bool): + def process_solution(self, solution: dict) -> tuple[list, float]: """ - Converts dict to list of visited seams + Converts solution dictionary to list of visited seams. :param solution: Unprocessed solution - :type solution: dict :return: Processed solution and the time it took to process it - :rtype: tuple(list, bool) """ start_time = start_time_measurement() nodes = list(self.application.nodes()) start = ((0, 0), 1, 1) - # fill route with None values route: list = [None] * int((len(self.application) - 1) / 2 + 1) visited_seams = [] - # get nodes from sample + if sum(value == 1 for value in solution.values()) > len(route): logging.warning("Result is longer than route! This might be problematic!") - # NOTE: Prevent duplicate node entries by enforcing only one occurrence per node along route + + # Prevent duplicate node entries by enforcing only one occurrence per node along route for (node, config, tool, timestep), val in solution.items(): if val and (node[0] not in visited_seams): if route[timestep] is not None: @@ -220,9 +223,8 @@ def process_solution(self, solution: dict) -> (list, bool): route[timestep] = (node, config, tool) visited_seams.append(node[0]) - # run heuristic replacing None values + # Fill missing values in the route if None in route: - # get not assigned nodes logging.info(f"Route until now is: {route}") nodes_unassigned = [(node, 1, 1) for node in nodes if node[0] not in visited_seams] nodes_unassigned = list(np.random.permutation(nodes_unassigned, dtype=object)) @@ -231,76 +233,85 @@ def process_solution(self, solution: dict) -> (list, bool): logging.info(nodes) for idx, node in enumerate(route): if node is None: - route[idx] = nodes_unassigned[0] - nodes_unassigned.remove(route[idx]) + route[idx] = nodes_unassigned.pop(0) - # cycle solution to start at provided start location + # Cycle solution to start at provided start location if start is not None and route[0] != start: - # rotate to put the start in front idx = route.index(start) route = route[idx:] + route[:idx] - # print route parsed_route = ' ->\n'.join( - [f' Node {visit[0][1]} of Seam {visit[0][0]} using config {visit[1]} & tool {visit[2]}' for visit in route]) + [ + f' Node {visit[0][1]} of Seam {visit[0][0]} using config ' + f' {visit[1]} & tool {visit[2]}' + for visit in route + ] + ) logging.info(f"Route found:\n{parsed_route}") + return route, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: list) -> tuple[bool, float]: """ - Checks if all seams and the home position are visited for a given solution + Checks if all seams and the home position are visited for a given solution. :param solution: List containing the nodes of the solution - :type solution: list :return: Boolean whether the solution is valid and time it took to validate - :rtype: tuple(bool, float) """ # Check if all seams are visited in route start = start_time_measurement() visited_seams = {seam[0][0] for seam in solution if seam is not None} if len(visited_seams) == len(solution): - logging.info( - f"All {len(solution) - 1} seams and the base node got visited (We only need to visit 1 node per seam)") + logging.info(f"All {len(solution) - 1} seams and " + "the base node got visited (We only need to visit 1 node per seam)") return True, end_time_measurement(start) else: logging.error(f"Only {len(visited_seams) - 1} got visited") return False, end_time_measurement(start) - def evaluate(self, solution: list) -> (float, float): + def evaluate(self, solution: list) -> tuple[float, float]: """ - Calculates the tour length for a given valid tour + Calculates the tour length for a given valid tour. :param solution: List containing the nodes of the solution - :type solution: list :return: Tour length, time it took to calculate the tour length - :rtype: tuple(float, float) """ start = start_time_measurement() - # get the total distance + + # Get the total distance total_dist = 0 for idx, _ in enumerate(solution[:-1]): - edge = next(item for item in list(self.application[solution[idx][0]][solution[idx + 1][0]].values()) if - item["c_start"] == solution[idx][1] and item["t_start"] == solution[idx][2] and item[ - "c_end"] == solution[idx + 1][1] and item["t_end"] == solution[idx + 1][2]) + edge = next( + item for item in list(self.application[solution[idx][0]][solution[idx + 1][0]].values()) + if item["c_start"] == solution[idx][1] and item["t_start"] == solution[idx][2] and + item["c_end"] == solution[idx + 1][1] and item["t_end"] == solution[idx + 1][2] + ) dist = edge['weight'] total_dist += dist - logging.info(f"Total distance (without return): {total_dist}") - # add distance between start and end point to complete cycle - return_edge = next(item for item in list(self.application[solution[0][0]][solution[-1][0]].values()) if - item["c_start"] == solution[0][1] and item["t_start"] == solution[0][2] and item[ - "c_end"] == solution[-1][1] and item["t_end"] == solution[-1][2]) + # Add distance between start and end point to complete cycle + return_edge = next( + item for item in list(self.application[solution[0][0]][solution[-1][0]].values()) + if item["c_start"] == solution[0][1] and item["t_start"] == solution[0][2] and + item["c_end"] == solution[-1][1] and item["t_end"] == solution[-1][2] + ) return_distance = return_edge['weight'] logging.info(f"Distance between start and end: {return_distance}") - # get distance for full cycle + # Get distance for full cycle distance = total_dist + return_distance logging.info(f"Total distance (including return): {distance}") return distance, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the generated problem graph to a file. + + :param path: Path to save the problem graph + :param iter_count: Iteration count for file versioning + """ with open(f"{path}/graph_iter_{iter_count}.gpickle", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/PVC/__init__.py b/src/modules/applications/optimization/PVC/__init__.py index e8853ea4..e9c96938 100644 --- a/src/modules/applications/optimization/PVC/__init__.py +++ b/src/modules/applications/optimization/PVC/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for PVC""" +""" +Module for PVC mappings. + +This module initializes the PVC mapping packages +""" diff --git a/src/modules/applications/optimization/PVC/data/createReferenceGraph.py b/src/modules/applications/optimization/PVC/data/createReferenceGraph.py index 097ae1a4..148fe444 100644 --- a/src/modules/applications/optimization/PVC/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/PVC/data/createReferenceGraph.py @@ -15,31 +15,19 @@ import networkx as nx import pickle -# Read in the original graph +# Create the original graph as a MultiDiGraph graph = nx.MultiDiGraph() with open("reference_data.txt") as infile: for line in infile: - line_elements = line.split(" ") - - print(line_elements) - - r_start = int(line_elements[1]) - s_start = int(line_elements[2]) - n_start = int(line_elements[3]) - c_start = int(line_elements[4]) - t_start = int(line_elements[5]) - l_start = int(line_elements[6]) - - r_end = int(line_elements[8]) - s_end = int(line_elements[9]) - n_end = int(line_elements[10]) - c_end = int(line_elements[11]) - t_end = int(line_elements[12]) - l_end = int(line_elements[13]) + line_elements = line.split() + # Extract start and end attributes from line elements + r_start, s_start, n_start, c_start, t_start, l_start = map(int, line_elements[1:7]) + r_end, s_end, n_end, c_end, t_end, l_end = map(int, line_elements[8:14]) duration = float(line_elements[15]) + # Handle missing or invalid data with default values if s_start == -1: s_start = 0 t_start = 1 # TODO except of picking a hardcoded value here we should select 1 from the dataset itself @@ -52,12 +40,15 @@ n_start = 0 if n_end == -1: n_end = 0 - # c_start, t_start, c_end, t_end + # Reduce the number of tools and configurations for simplicity if c_end < 3 and c_start < 3 and t_start < 2 and t_end < 2: - # Let's reduce the number of tools and configs for now - graph.add_edge((s_start, n_start), (s_end, n_end), c_start=c_start, t_start=t_start, c_end=c_end, - t_end=t_end, weight=duration) + graph.add_edge( + (s_start, n_start), (s_end, n_end), + c_start=c_start, t_start=t_start, + c_end=c_end, t_end=t_end, weight=duration + ) +# Save the graph to a file in gpickle format with open("reference_graph.gpickle", "wb") as file: pickle.dump(graph, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/PVC/mappings/ISING.py b/src/modules/applications/optimization/PVC/mappings/ISING.py index d98dca74..6c5ded38 100644 --- a/src/modules/applications/optimization/PVC/mappings/ISING.py +++ b/src/modules/applications/optimization/PVC/mappings/ISING.py @@ -13,25 +13,26 @@ # limitations under the License. from typing import TypedDict +import logging -import networkx +import networkx as nx import numpy as np from dimod import qubo_to_ising -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.PVC.mappings.QUBO import QUBO from utils import start_time_measurement, end_time_measurement class Ising(Mapping): """ - Ising formulation for the PVC + Ising formulation for the PVC. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA"] @@ -40,91 +41,79 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: List of dict with requirements of this module - :rtype: list[dict] + :return: List of dictionaries with requirements of this module """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, *QUBO.get_requirements() ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. - :return: - .. code-block:: python - - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" - } - } + :return: Dictionary containing parameter options. + .. code-block:: python + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your Lagrange?" + } + } """ return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" + "description": "By which factor would you like to multiply your Lagrange?" } } class Config(TypedDict): """ - Attributes of a valid config - - .. code-block:: python - - lagrange_factor: float + Configuration attributes for Ising mapping. + Attributes: + lagrange_factor (float): Factor to multiply the Langrange. """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ - Uses the PVC QUBO formulation and converts it to an Ising - - :param problem: networkx graph - :type problem: networkx.Graph - :param config: Dict with the mapping config - :type config: Config - :return: Dict with the ising and time it took to map it - :rtype: tuple(dict, float) + Uses the PVC QUBO formulation and converts it to an Ising representation. + + :param problem: Networkx graph representing the PVC problem + :param config: Config dictionary with the mapping configuration + :return: Tuple containing a dictionary with the ising problem and time it took to map it """ start = start_time_measurement() + + # Convert the PVC problem to QUBO qubo_mapping = QUBO() q, _ = qubo_mapping.map(problem, config) + + # Convert QUBO to ising using dimod t, j, _ = qubo_to_ising(q["Q"]) + # Extract unique configuration and tool attributes from the graph config = [x[2]['c_start'] for x in problem.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in problem.edges(data=True)])) tool = [x[2]['t_start'] for x in problem.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in problem.edges(data=True)])) - # Convert Ising dict to matrix - timesteps = int((problem.number_of_nodes() - 1) / 2 + 1) # G.number_of_nodes() - + # Initialize J matrix and mapping + timesteps = int((problem.number_of_nodes() - 1) / 2 + 1) matrix_size = problem.number_of_nodes() * len(config) * len(tool) * timesteps j_matrix = np.zeros((matrix_size, matrix_size), dtype=float) - self.key_mapping = {} - index_counter = 0 + # Map J values to a matrix representation + index_counter = 0 for key, value in j.items(): if key[0] not in self.key_mapping: self.key_mapping[key[0]] = index_counter @@ -138,25 +127,28 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): return {"J": j_matrix, "t": np.array(list(t.values()))}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the PVC class for validation/evaluation. :param solution: Dictionary containing the solution - :type solution: dict - :return: Solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :return: Tuple with the remapped solution and time it took to reverse map """ start = start_time_measurement() logging.info(f"Key Mapping: {self.key_mapping}") - result = {} - for key, value in self.key_mapping.items(): - result[key] = 1 if solution[value] == 1 else 0 + + result = {key: 1 if solution[self.key_mapping[key]] == 1 else 0 for key in self.key_mapping} return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/PVC/mappings/QUBO.py b/src/modules/applications/optimization/PVC/mappings/QUBO.py index 05c062a2..3d3c8b80 100644 --- a/src/modules/applications/optimization/PVC/mappings/QUBO.py +++ b/src/modules/applications/optimization/PVC/mappings/QUBO.py @@ -15,22 +15,22 @@ import itertools from collections import defaultdict from typing import TypedDict +import logging -import networkx +import networkx as nx -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement class QUBO(Mapping): """ - QUBO formulation for the PVC - + QUBO formulation for the PVC. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -38,98 +38,77 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dictionaries with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. - :return: - .. code-block:: python + :return: Dictionary containing parameter options + .. code-block:: python - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" - } - } + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your Lagrange?" + } + } """ return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" + "description": "By which factor would you like to multiply your Lagrange?" } } class Config(TypedDict): """ - Attributes of a valid config + Configuration attributes of QUBO mapping. - .. code-block:: python - - lagrange_factor: float + Attributes: + lagrange_factor (float): Factor to multiply the Langrange. """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to a QUBO formulation. - :param problem: a networkx graph - :type problem: networkx.Graph - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) + :param problem: Networkx graph representing the PVC problem + :param config: Config dictionary with the mapping configuration + :return: Tuple containing the QUBO dictionary and the time it took to map it """ + # Inspired by https://dnx.readthedocs.io/en/latest/_modules/dwave_networkx/algorithms/tsp.html start = start_time_measurement() - lagrange = None lagrange_factor = config['lagrange_factor'] - weight = 'weight' - # Inspired by https://dnx.readthedocs.io/en/latest/_modules/dwave_networkx/algorithms/tsp.html + # Estimate lagrange if not provided n = problem.number_of_nodes() - # we only need this number of timesteps since we only need to visit 1 node per seam - # (plus we start and end at the base node) timesteps = int((n - 1) / 2 + 1) - # Let`s get the number of different configs and tools + + # Get the number of different configs and tools config = [x[2]['c_start'] for x in problem.edges(data=True)] config = list(set(config + [x[2]['c_end'] for x in problem.edges(data=True)])) tool = [x[2]['t_start'] for x in problem.edges(data=True)] tool = list(set(tool + [x[2]['t_end'] for x in problem.edges(data=True)])) - if lagrange is None: - # If no lagrange parameter provided, set to 'average' tour length. - # Usually a good estimate for a lagrange parameter is between 75-150% - # of the objective function value, so we come up with an estimate for - # tour length and use that. - if problem.number_of_edges() > 0: - weights = [x[2]['weight'] for x in problem.edges(data=True)] - # At the moment we need to filter out the very high artificial values we added during generate_problem - # as this would mess up the lagrange - weights = list(filter(lambda a: a != max(weights), weights)) - lagrange = sum(weights) / len(weights) * timesteps - else: - lagrange = 2 - - lagrange = lagrange * lagrange_factor + if problem.number_of_edges() > 0: + weights = [x[2]['weight'] for x in problem.edges(data=True)] + weights = list(filter(lambda a: a != max(weights), weights)) + lagrange = sum(weights) / len(weights) * timesteps + else: + lagrange = 2 + lagrange *= lagrange_factor logging.info(f"Selected lagrange is: {lagrange}") - # some input checking if n in (1, 2) or len(problem.edges) < n * (n - 1) // 2: msg = "graph must be a complete graph with at least 3 nodes or empty" raise ValueError(msg) @@ -147,52 +126,37 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): for pos_1 in range(timesteps): # for number of timesteps for t_start in tool: for c_start in config: - q[((node, c_start, t_start, pos_1), - (node, c_start, t_start, - pos_1))] -= lagrange # lagrange # nodes to itself on the same timestep + q[((node, c_start, t_start, pos_1), (node, c_start, t_start, pos_1))] -= lagrange for t_end in tool: - # for all configs and tools + # For all configs and tools for c_end in config: if c_start != c_end or t_start != t_end: - q[((node, c_start, t_start, pos_1), - (node, c_end, t_end, pos_1))] += 1.0 * lagrange - for pos_2 in range(pos_1 + 1, - timesteps): # For each following timestep set value for u -> u - # penalize visiting same node again in another timestep - q[((node, c_start, t_start, pos_1), - (node, c_end, t_end, - pos_2))] += 2.0 * lagrange - - # penalize visiting other node of same seam + q[((node, c_start, t_start, pos_1), (node, c_end, t_end, pos_1))] += 1.0 * lagrange + for pos_2 in range(pos_1 + 1, timesteps): + # Penalize visiting same node again in another timestep + q[((node, c_start, t_start, pos_1), (node, c_end, t_end, pos_2))] += 2.0 * lagrange + # Penalize visiting other node of same seam if node != (0, 0): # (0,0) is the base node, it is not a seam - # get the other nodes of the same seam - other_seam_nodes = [x for x in problem.nodes if x[0] == node[0] - and x[1] != node] + # Get the other nodes of the same seam + other_seam_nodes = [ + x for x in problem.nodes if x[0] == node[0] and x[1] != node + ] for other_seam_node in other_seam_nodes: - # penalize visiting other node of same seam + # Penalize visiting other node of same seam q[((node, c_start, t_start, pos_1), - (other_seam_node, c_end, t_end, - pos_2))] += 2.0 * lagrange + (other_seam_node, c_end, t_end, pos_2))] += 2.0 * lagrange # Constraint to only visit a single node in a single timestep - for pos in range(timesteps): # for all timesteps - for node_1 in problem: # for all nodes + for pos in range(timesteps): + for node_1 in problem: for t_start in tool: for c_start in config: - q[((node_1, c_start, t_start, pos), - (node_1, c_start, t_start, pos))] -= lagrange + q[((node_1, c_start, t_start, pos), (node_1, c_start, t_start, pos))] -= lagrange for t_end in tool: for c_end in config: - # if c_start != c_end or t_start != t_end: - # Q[((node_1, c_start, t_start, pos), - # (node_1, c_end, t_end, pos))] += lagrange for node_2 in set(problem) - {node_1}: # for all nodes except node1 -> node1 - # QUBO coefficient is 2*lagrange, but we are placing this value - # above *and* below the diagonal, so we put half in each position. - # penalize from node1 -> node2 in the same timestep - q[((node_1, c_start, t_start, pos), (node_2, c_end, t_end, - pos))] += lagrange + q[((node_1, c_start, t_start, pos), (node_2, c_end, t_end, pos))] += lagrange # Objective that minimizes distance for u, v in itertools.combinations(problem.nodes, 2): @@ -202,25 +166,34 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): for c_start in config: for c_end in config: nextpos = (pos + 1) % timesteps - edge_u_v = next(item for item in list(problem[u][v].values()) if - item["c_start"] == c_start and item["t_start"] == t_start and item[ - "c_end"] == c_end and item["t_end"] == t_end) - # since it is the other direction we switch start and end of tool and config - edge_v_u = next(item for item in list(problem[v][u].values()) if - item["c_start"] == c_end and item["t_start"] == t_end and item[ - "c_end"] == c_start and item["t_end"] == t_start) - # going from u -> v - q[((u, c_start, t_start, pos), (v, c_end, t_end, nextpos))] += edge_u_v[weight] - - # going from v -> u - q[((v, c_end, t_end, pos), (u, c_start, t_start, nextpos))] += edge_v_u[weight] + edge_u_v = next( + item for item in list(problem[u][v].values()) + if item["c_start"] == c_start and item["t_start"] == t_start and + item["c_end"] == c_end and item["t_end"] == t_end + ) + # Since it is the other direction we switch start and end of tool and config + edge_v_u = next( + item for item in list(problem[v][u].values()) + if item["c_start"] == c_end and item["t_start"] == t_end and + item["c_end"] == c_start and item["t_end"] == t_start + ) + # Going from u -> v + q[((u, c_start, t_start, pos), (v, c_end, t_end, nextpos))] += edge_u_v['weight'] + # Going from v -> u + q[((v, c_end, t_end, pos), (u, c_start, t_start, nextpos))] += edge_v_u['weight'] logging.info("Created Qubo") return {"Q": q}, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/PVC/mappings/__init__.py b/src/modules/applications/optimization/PVC/mappings/__init__.py index f7edfc4a..e9c96938 100644 --- a/src/modules/applications/optimization/PVC/mappings/__init__.py +++ b/src/modules/applications/optimization/PVC/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for PVC mappings""" +""" +Module for PVC mappings. + +This module initializes the PVC mapping packages +""" diff --git a/src/modules/applications/optimization/SAT/SAT.py b/src/modules/applications/optimization/SAT/SAT.py index 42db0cb2..abb62310 100644 --- a/src/modules/applications/optimization/SAT/SAT.py +++ b/src/modules/applications/optimization/SAT/SAT.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TypedDict import nnf @@ -19,26 +20,45 @@ from nnf import Var, And, Or from nnf.dimacs import dump -from modules.applications.Application import * +from modules.Core import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement class SAT(Optimization): """ - Before a new vehicle model can be deployed for production, several tests have to be carried out on pre-series - vehicles to ensure the feasibility and gauge the functionality of specific configurations of components. - Naturally, the manufacturer wants to save resources and produce as few pre-series vehicles as possible while - still performing all desired tests. Further, not all feature configurations can realistically be implemented in - all vehicles, leading to constraints that the produced vehicles must satisfy. This can be modeled as a SAT problem. + The SAT (Satisfiability) problem plays a crucial role in the field of computational optimization. In the context + of vehicle manufacturing, it is essential to test various pre-series vehicle configurations to ensure they meet + specific requirements before production begins. This testing involves making sure that each vehicle configuration + complies with several hard constraints related to safety, performance, and buildability while also fulfilling + soft constraints such as feature combinations or specific requirements for testing. The SAT problem models these + constraints in a way that enables a systematic approach to determine feasible vehicle configurations and minimize + the need for excessive physical prototypes. + + This problem is modeled as a Max-SAT problem, where the aim is to find a configuration that satisfies as many + constraints as possible while balancing between the number of satisfied hard and soft constraints. The formulation + uses a conjunctive normal form (CNF) representation of logical expressions to model the dependencies and + incompatibilities between various features and components in vehicle assembly. By leveraging optimization + algorithms, the SAT module aims to produce a minimal but sufficient set of configurations, ensuring that all + necessary tests are performed while minimizing resource usage. This approach helps in creating a robust testing + framework and reducing the overall cost of vehicle development. + + To solve the SAT problem, various approaches are employed, including translating the CNF representation into + different quantum and classical optimization mappings such as QUBO (Quadratic Unconstrained Binary Optimization) + or Ising formulations. These mappings make the SAT problem suitable for solving on quantum computers and + classical annealers. The SAT problem in this module is implemented with a flexible interface, allowing integration + with a range of solvers that can exploit different computational paradigms, making it adaptable for a variety of + hardware and optimization backends. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("SAT") - self.submodule_options = ["QubovertQUBO", "Direct", "ChoiQUBO", "DinneenQUBO", "ChoiIsing", "DinneenIsing"] + self.submodule_options = [ + "QubovertQUBO", "Direct", "ChoiQUBO", "DinneenQUBO", "ChoiIsing", "DinneenIsing" + ] self.literals = None self.num_tests = None self.num_constraints = None @@ -47,27 +67,26 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "nnf", "version": "0.4.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_solution_quality_unit(self) -> str: return "Evaluation" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "QubovertQUBO": from modules.applications.optimization.SAT.mappings.QubovertQUBO import \ QubovertQUBO # pylint: disable=C0415 @@ -93,43 +112,42 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application - - :return: - .. code-block:: python - - return { - "variables": { - "values": list(range(10, 151, 10)), - "custom_input": True, - "allow_ranges": True, - "postproc": int, - "description": "How many variables do you need?" - }, - "clvar_ratio_cons": { - "values": [2, 3, 4, 4.2, 5], - "custom_input": True, - "allow_ranges": True, - "postproc": int, - "description": "What clause:variable ratio do you want for the (hard) constraints?" - }, - "clvar_ratio_test": { - "values": [2, 3, 4, 4.2, 5], - "custom_input": True, - "allow_ranges": True, - "postproc": int, - "description": "What clause:variable ratio do you want for the tests (soft con.)?" - }, - "problem_set": { - "values": list(range(10)), - "description": "Which problem set do you want to use?" - }, - "max_tries": { - "values": [100], - "description": "Maximum number of tries to create problem" - } - } + Returns the configurable settings for this application. + :return: Dictionary with configurable settings + .. code-block:: python + + return { + "variables": { + "values": list(range(10, 151, 10)), + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "How many variables do you need?" + }, + "clvar_ratio_cons": { + "values": [2, 3, 4, 4.2, 5], + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "What clause-to-variable ratio do you want for the (hard) constraints?" + }, + "clvar_ratio_test": { + "values": [2, 3, 4, 4.2, 5], + "custom_input": True, + "allow_ranges": True, + "postproc": int, + "description": "What clause-to-variable ratio do you want for the tests (soft con.)?" + }, + "problem_set": { + "values": list(range(10)), + "description": "Which problem set do you want to use?" + }, + "max_tries": { + "values": [100], + "description": "Maximum number of tries to create problem?" + } + } """ return { "variables": { @@ -144,14 +162,14 @@ def get_parameter_options(self) -> dict: "custom_input": True, "allow_ranges": True, "postproc": int, - "description": "What clause:variable ratio do you want for the (hard) constraints?" + "description": "What clause-to-variable ratio do you want for the (hard) constraints?" }, "clvar_ratio_test": { "values": [2, 3, 4, 4.2, 5], "custom_input": True, "allow_ranges": True, "postproc": int, - "description": "What clause:variable ratio do you want for the tests (soft constraints)?" + "description": "What clause-to-variable ratio do you want for the tests (soft constraints)?" }, "problem_set": { "values": list(range(10)), @@ -159,13 +177,13 @@ def get_parameter_options(self) -> dict: }, "max_tries": { "values": [100], - "description": "Maximum number of tries to create problem" + "description": "Maximum number of tries to create problem?" } } class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -182,31 +200,28 @@ class Config(TypedDict): problem_set: int max_tries: int - def generate_problem(self, config: Config) -> (nnf.And, list): + def generate_problem(self, config: Config) -> tuple[nnf.And, list]: """ - Generates a vehicle configuration problem out of a given config. Returns buildability constraints (hard - constraints) and tests (soft constraints), the successful evaluation of which we try to maximize. Both - are given in nnf form, which we then convert accordingly. - - :param config: config with the parameters specified in Config class - :type config: Config - :return: - :rtype: tuple(nnf.And, list) + Generates a vehicle configuration problem out of a given config. + Returns buildability constraints (hard constraints) and tests (soft + constraints), the successful evaluation of which we try to maximize. + Both are given in nnf form, which we then convert accordingly. + + :param config: Configuration parameters for problem generation + :return: A tuple containing the problem, number of variables, and other details """ self.num_variables = config["variables"] num_constraints = round(config["clvar_ratio_cons"] * self.num_variables) num_tests = round(config["clvar_ratio_test"] * self.num_variables) - max_tries = config["max_tries"] self.literals = [Var(f"L{i}") for i in range(self.num_variables)] - self.application = {} def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): - # iterate over the desired number of attempts: we break if we find a solvable instance. + # Iterate over the desired number of attempts: break if we find a solvable instance. for attempt in range(nr_tries): - # initialize random number generator -- we multiply the attempt to traverse distinct random seeds + # Initialize random number generator -- multiply the attempt to traverse distinct random seeds # for the hard and soft constraints, respectively (since rseed of the hard and soft constraints differs # by 1). rng = np.random.default_rng(rseed + attempt * 2) @@ -214,45 +229,44 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): # generate literal list to sample from lit_vars = [Var(f"L{i}") for i in range(nr_vars)] for _ in range(nr_clauses): - # we select three (non-repeated) literals and negate them randomly -- together constituting a clause + # Select three (non-repeated) literals and negate them randomly -- together constituting a clause chosen_literals = rng.choice(lit_vars, 3, replace=False) negate_literals = rng.choice([True, False], 3, replace=True) - clause = [] - # we perform the random negations and append to clause: - for lit, neg in zip(chosen_literals, negate_literals): - if neg: - clause.append(lit.negate()) - else: - clause.append(lit) - # append the generated clause to the total container + # Perform the random negations and append to clause: + clause = [ + lit.negate() if neg else lit + for lit, neg in zip(chosen_literals, negate_literals) + ] + # Append the generated clause to the total container clause_list.append(Or(clause)) - # we generate the conjunction of the problem, such that we can use the nnf native function and test its - # satisfiability. prob = And(clause_list) - if not satisfiable or prob.satisfiable(): return clause_list - # loop ran out of tries + # Loop ran out of tries logging.error("Unable to generate valid solutions. Consider increasing max_tries or decreasing " "the clause:variable ratio.") raise ValueError("Unable to generate valid solution.") - # we choose a random seed -- since we try at most max_tries times to generate a solvable instance, - # we space the initial random seeds by 2 * max_tries (because we need both hard and soft constraints). + # Choose a random seed -- since we try at most max_tries times to generate a solvable instance, + # Space the initial random seeds by 2 * max_tries (because we need both hard and soft constraints). random_seed = 2 * config["problem_set"] * max_tries - # generate hard & soft constraints. We make both satisfiable, but this can in principle be tuned. - hard = And(_generate_3sat_clauses(num_constraints, self.num_variables, - satisfiable=True, rseed=random_seed, nr_tries=max_tries)) - # the random_seed + 1 ensures that a different set of seeds is sampled compared to the hard constraints. - soft = _generate_3sat_clauses(num_tests, self.num_variables, satisfiable=True, rseed=random_seed + 1, - nr_tries=config["max_tries"]) + # Generate hard & soft constraints. Make both satisfiable, but this can in principle be tuned. + hard = And(_generate_3sat_clauses( + num_constraints, self.num_variables, satisfiable=True, + rseed=random_seed, nr_tries=max_tries + )) + # The random_seed + 1 ensures that a different set of seeds is sampled compared to the hard constraints. + soft = _generate_3sat_clauses( + num_tests, self.num_variables, satisfiable=True, + rseed=random_seed + 1, nr_tries=config["max_tries"] + ) if (hard is None) or (soft is None): raise ValueError("Unable to generate satisfiable") - # saving constraints and tests + # Saving constraints and tests self.application["constraints"] = hard self.application["tests"] = soft - # and their cardinalities: + # And their cardinalities: self.num_constraints = len(hard) self.num_tests = len(soft) @@ -261,43 +275,36 @@ def _generate_3sat_clauses(nr_clauses, nr_vars, satisfiable, rseed, nr_tries): f" and {self.num_tests} tests") return hard, soft - def validate(self, solution: dict) -> (bool, float): + def validate(self, solution: dict) -> tuple[bool, float]: """ - Checks given solution. + Validate a given solution against the constraints. - :param solution: - :type solution: dict - :return: Boolean whether the solution is valid, time it took to validate - :rtype: tuple(bool, float) + :param solution: The solution to validate + :return: True if the solution is valid, False otherwise, and time it took to complete """ start = start_time_measurement() logging.info("Checking validity of solution:") - # logging.info(solution) nr_satisfied_hardcons = len(*np.where( [c.satisfied_by(solution) for c in self.application["constraints"].children] )) ratio = nr_satisfied_hardcons / self.num_constraints is_valid = ratio == 1.0 - # prints the ratio of satisfied constraints and prints if all constraints are satisfied logging.info(f"Ratio of satisfied constraints: {ratio}\nSuccess:{['no', 'yes'][int(is_valid)]}") + return is_valid, end_time_measurement(start) - def evaluate(self, solution: dict) -> (float, float): + def evaluate(self, solution: dict) -> tuple[float, float]: """ Calculates the quality of the solution. - :param solution: - :type solution: dict + :param solution: Dictionary containing the solution :return: Tour length, time it took to calculate the tour length - :rtype: tuple(float, float) """ start = start_time_measurement() - logging.info("Checking the quality of the solution:") - # logging.info(solution) - # count the number of satisfied clauses + # Count the number of satisfied clauses nr_satisfied_tests = len(*np.where([test.satisfied_by(solution) for test in self.application["tests"]])) ratio_satisfied = nr_satisfied_tests / self.num_tests @@ -306,13 +313,21 @@ def evaluate(self, solution: dict) -> (float, float): return ratio_satisfied, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Save the constraints and tests to files in CNF format. + + :param path: The directory path where the files will be saved. + :param iter_count: The iteration count to include in the filenames. + """ with open(f"{path}/constraints_iter_{iter_count}.cnf", "w") as f_cons: dump( - obj=self.application["constraints"], fp=f_cons, + obj=self.application["constraints"], + fp=f_cons, var_labels={str(literal): idx + 1 for idx, literal in enumerate(self.literals)} ) with open(f"{path}/tests_iter_{iter_count}.cnf", "w") as f_test: dump( - obj=Or(self.application["tests"]), fp=f_test, + obj=Or(self.application["tests"]), + fp=f_test, var_labels={str(literal): idx + 1 for idx, literal in enumerate(self.literals)} ) diff --git a/src/modules/applications/optimization/SAT/__init__.py b/src/modules/applications/optimization/SAT/__init__.py index cd55763b..0ef93529 100644 --- a/src/modules/applications/optimization/SAT/__init__.py +++ b/src/modules/applications/optimization/SAT/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SAT""" +""" +Module for SAT mappings + +This module initializes the SAT application +""" diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py index a1bcad0e..63f463d7 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiISING.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiISING.py @@ -17,19 +17,19 @@ import numpy as np from dimod import qubo_to_ising -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.SAT.mappings.ChoiQUBO import ChoiQUBO from utils import start_time_measurement, end_time_measurement class ChoiIsing(Mapping): """ - Ising formulation for SAT problem using QUBO by Choi (1004.2226) + Ising formulation for SAT problem using QUBO by Choi (1004.2226). """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA"] @@ -39,48 +39,39 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, *ChoiQUBO.get_requirements() ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return { - "hard_reward": { - "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" - }, - "soft_reward": { - "values": [0.1, 1, 2], - "description": "What Bh/Bs ratio do you want? This value is multiplied with the " - "number of tests." - } - } + Returns the configurable settings for this mapping. + + :return: Dictionary with parameter options + .. code-block:: python + return { + "hard_reward": { + "values": [0.1, 0.5, 0.9, 0.99], + "description": "What Bh/A ratio do you want? (How strongly to enforce hard constraints)" + }, + "soft_reward": { + "values": [0.1, 1, 2], + "description": "What Bh/Bs ratio do you want? This value is multiplied with the " + "number of tests." + } + } """ return { "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + "description": "What Bh/A ratio do you want? (How strongly to enforce hard constraints)" }, "soft_reward": { "values": [0.1, 1, 2], @@ -90,7 +81,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -101,19 +92,17 @@ class Config(TypedDict): hard_reward: float soft_reward: float - def map(self, problem: any, config) -> (dict, float): + def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Uses the ChoiQUBO formulation and converts it to an Ising. - :param problem: the SAT problem - :type problem: any - :param config: dictionary with the mapping config - :type config: Config - :return: dict with the ising, time it took to map it - :rtype: tuple(dict, float) + :param problem: SAT problem + :param config: Dictionary with the mapping config + :return: Dict with the ising, time it took to map it """ start = start_time_measurement() self.problem = problem + # call mapping function self.qubo_mapping = ChoiQUBO() q, _ = self.qubo_mapping.map(problem, config) @@ -132,21 +121,17 @@ def map(self, problem: any, config) -> (dict, float): return {"J": j_matrix, "t": t_vector}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :type: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() # convert raw solution into the right format to use reverse_map() of ChoiQUBO.py - solution_dict = {} - for i, el in enumerate(solution): - solution_dict[i] = el + solution_dict = dict(enumerate(solution)) # reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) @@ -154,7 +139,13 @@ def reverse_map(self, solution: dict) -> (dict, float): return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py index c91b3d86..e8a626ee 100644 --- a/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/ChoiQUBO.py @@ -14,10 +14,11 @@ from itertools import combinations, product from typing import TypedDict +import logging from nnf import Var, And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -28,7 +29,7 @@ class ChoiQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -38,52 +39,51 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "nnf", - "version": "0.4.1" - } - ] + return [{"name": "nnf", "version": "0.4.1"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return { - "hard_reward": { - "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" - }, - "soft_reward": { - "values": [0.1, 1, 2], - "description": "What Bh/Bs ratio do you want? This value is multiplied with the " - "number of tests." - } - } + Returns the configurable settings for this mapping. + + :return: Dictionary with parameter options + .. code-block:: python + return { + "hard_reward": { + "values": [0.1, 0.5, 0.9, 0.99], + "description": "What Bh/A ratio do you want? (How strongly to enforce hard constraints)" + }, + "soft_reward": { + "values": [0.1, 1, 2], + "description": "What Bh/Bs ratio do you want? This value is multiplied with the " + "number of tests." + } + } """ return { "hard_reward": { "values": [0.1, 0.5, 0.9, 0.99], - "description": "What Bh/A ratio do you want? (How strongly to enforce hard cons.)" + "description": ( + "What Bh/A ratio do you want?" + "(How strongly to enforce hard constraints)" + ) }, "soft_reward": { "values": [0.1, 1, 2], - "description": "What Bh/Bs ratio do you want? This value is multiplied with the number of tests." + "description": ( + "What Bh/Bs ratio do you want?" + "This value is multiplied with the number of tests." + ) } } class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -94,156 +94,155 @@ class Config(TypedDict): hard_reward: float soft_reward: float - def map(self, problem: (And, list), config) -> (dict, float): + def map(self, problem: tuple[And, list], config: Config) -> tuple[dict, float]: """ - Converts a MaxSAT instance with hard and soft constraints into a graph problem -- solving MaxSAT then - corresponds to solving an instance of the Maximal Independent Set problem. See Andrew Lucas (2014), - or the original publication by Choi (1004.2226). - - :param problem: - :type problem: (nnf.And, list) - :param config: config with the parameters specified in Config class - :type config: Config - :return: - :rtype: tuple(dict, float) + Converts a MaxSAT instance with hard and soft constraints into a graph problem -- + solving MaxSAT then corresponds to solving an instance of the Maximal Independent Set problem. + See Andrew Lucas (2014), or the original publication by Choi (1004.2226). + + :param problem: A tuple containing hard and soft constraints + :param config: Config with the parameters specified in Config class + :return: Dictionary containing the QUBO representation and the time taken """ start = start_time_measurement() hard_constraints, soft_constraints = problem - # in principle, one could use a different value of A -- it shouldn't play a role though. - A = 1 - Bh = config['hard_reward'] * A - # we divide Bh by the number of test clauses, such that fulfilling a test result is less favourable than - # satisfying a constraint, which we aim to prioritize. - Bs = Bh * config['soft_reward'] / len(soft_constraints) - # we count the number of different variables that appear in the vehicle options problem: + a = 1 + bh = config['hard_reward'] * a + # divide Bh by the number of test clauses, such that fulfilling a test result is less favourable than + # satisfying a constraint, which aim to prioritize. + bs = bh * config['soft_reward'] / len(soft_constraints) + + # Count the number of different variables that appear in the vehicle options problem: self.nr_vars = len(hard_constraints.vars().union(And(soft_constraints).vars())) - # edges variable holds all edges in the resulting graph + # Edges variable holds all edges in the resulting graph edges = {} # lit_occur is a dictionary which will store the information in which clause a certain literal will occur. lit_occur = {} def _add_clause(clause, curr_edges, curr_lit_occ, pos): - # iterating through the clauses, we add nodes corresponding to each literal - # the format is as follows: L12-5, means that literal 12 is present in clause nr. 5. literals = [f"{el}-{pos}" for el in clause.children] - # we connect the literals within one clause + # Connect the literals within one clause for cmb in combinations(literals, 2): - # we add a weight for each edge within clause - curr_edges[cmb] = A - # we add the occurrences of the variables to the occurrences dictionary + # Add a weight for each edge within clause + curr_edges[cmb] = a + # Add the occurrences of the variables to the occurrences dictionary for var in clause.children: if var.name not in curr_lit_occ.keys(): curr_lit_occ[var.name] = {True: [], False: []} - # we add occurrences and mark that they correspond to hard constraints + # Add occurrences and mark that they correspond to hard constraints curr_lit_occ[var.name][var.true].append(pos) return curr_edges, curr_lit_occ - # first convert the hard constraints into the graph + # Convert the hard constraints into the graph for idx, hard_constraint in enumerate(hard_constraints): edges, lit_occur = _add_clause(hard_constraint, edges, lit_occur, idx) - # we save the current total clause count: + # Save the current total clause count: constraints_max_ind = len(hard_constraints) - # we repeat the procedure for the soft constraints: + # Repeat the procedure for the soft constraints: for idx, soft_constraint in enumerate(soft_constraints): edges, lit_occur = _add_clause(soft_constraint, edges, lit_occur, idx + constraints_max_ind) - # we connect conflicting clauses using the lit_occur dict: + # Connect conflicting clauses using the lit_occur dict: for literal, positions_dict in lit_occur.items(): # for every literal lit, we check its occurrences and connect the non-negated and negated occurrences. for pos_true, pos_false in product(positions_dict[True], positions_dict[False]): - # we ensure that we do not add a penalty for contradicting literals in the if pos_true != pos_false: - # we employ the notation from nnf, where the tilde symbol ~ corresponds to negation. + # Employ the notation from nnf, where the tilde symbol ~ corresponds to negation. lit_true, lit_false = f"{literal}-{pos_true}", f"~{literal}-{pos_false}" - # we add a penalty for each such edge: - edges[(lit_true, lit_false)] = A + # Add a penalty for each such edge: + edges[(lit_true, lit_false)] = a - # we collect all different nodes that we have in our graph, omitting repetitions: + # Collect all different nodes that we have in our graph, omitting repetitions: node_set = set([]) for nodes in edges.keys(): node_set = node_set.union(set(nodes)) node_list = sorted(node_set) - # we fix a mapping (node -> binary variable) + # Fix a mapping (node -> binary variable) relabel_dict = {v: i for i, v in enumerate(node_list)} - # we save the reverse mapping, which is later used to decode the solution. + # Save the reverse mapping, which is later used to decode the solution. self.reverse_dict = dict(enumerate(node_list)) def _remap_pair(pair): """Small helper function that maps the nodes of an edge to binary variables""" return relabel_dict[pair[0]], relabel_dict[pair[1]] - # we save the Qubo corresponding to the graph. - Q = {_remap_pair(key): val for key, val in edges.items()} + # Save the QUBO corresponding to the graph. + q = {_remap_pair(key): val for key, val in edges.items()} for v in node_list: - # we add different energy rewards depending on whether it is a hard or a soft constraint - # soft cons. have lower rewards, since we prioritize satisfying hard constraints. + # Add different energy rewards depending on whether it is a hard or a soft constraint if int(v.split('-')[-1]) < constraints_max_ind: - # if hard cons, we add -Bh as the reward - Q[_remap_pair((v, v))] = -Bh + # if hard cons, add -Bh as the reward + q[_remap_pair((v, v))] = -bh else: - # for soft constraints we add -Bs - Q[_remap_pair((v, v))] = -Bs + # for soft constraints, add -Bs + q[_remap_pair((v, v))] = -bs - logging.info(f"Converted to Choi Qubo with {len(node_list)} binary variables. Bh={config['hard_reward']}," - f" Bs={Bs}.") - return {'Q': Q}, end_time_measurement(start) + logging.info(f"Converted to Choi QUBO with {len(node_list)} binary variables. Bh={config['hard_reward']}," + f" Bs={bs}.") + return {'Q': q}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :type solution: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() - # we define the literals list, so that we can check the self-consistency of the solution. That is, we save all + # We define the literals list, so that we can check the self-consistency of the solution. That is, we save all # assignments proposed by the annealer, and see if there is no contradiction. (In principle a solver # could mandate L3 = True and L3 = False, resulting in a contradiction.) literals = [] - # assignments saves the actual solution + # Assignments saves the actual solution assignments = [] + for node, tf in solution.items(): - # we check if node is included in the set (i.e. if tf is True (1)) + # Check if node is included in the set (i.e. if tf is True (1)) if tf: - # convert back to the language of literals + # Convert back to the language of literals lit_str = self.reverse_dict[node] - # we check if the literal is negated: + # Check if the literal is negated: if lit_str.startswith('~'): - # remove the negation symbol + # Remove the negation symbol lit_str = lit_str.replace('~', '') - # save a negated literal object, will be used for self-consistency check + # Save a negated literal object, will be used for self-consistency check lit = Var(lit_str).negate() - # add the negated literal to the assignments, removing the (irrelevant) position part + # Add the negated literal to the assignments, removing the (irrelevant) position part assignments.append(Var(lit_str.split('-')[0]).negate()) else: - # if literal is true, no ~ symbol needs to be removed: + # If literal is true, no ~ symbol needs to be removed: lit = Var(lit_str) assignments.append(Var(lit_str.split('-')[0])) literals.append(lit) - # we check for self-consistency of solution; we check that the assignments of all literals are consistent: + + # Check for self-consistency of solution; Check that the assignments of all literals are consistent: if not And(set(literals)).satisfiable(): logging.error('Generated solution is not self-consistent!') raise ValueError("Inconsistent solution for the ChoiQubo returned.") - # If the solution is consistent, we have to find and add potentially missing variables: + # If the solution is consistent, find and add potentially missing variables: assignments = sorted(set(assignments)) - # find missing vars, or more precisely, their labels: + # Find missing vars, or more precisely, their labels: missing_vars = set(range(self.nr_vars)) - {int(str(a).replace('L', '').replace('~', '')) for a in assignments} - # add the variables that we found were missing: + # Add the variables that found were missing: for nr in missing_vars: assignments.append(Var(f'L{nr}')) return {list(v.vars())[0]: v.true for v in sorted(assignments)}, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py index f1b46410..5cc52451 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenISING.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenISING.py @@ -18,7 +18,7 @@ from dimod import qubo_to_ising from nnf import And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.SAT.mappings.DinneenQUBO import DinneenQUBO from utils import start_time_measurement, end_time_measurement @@ -26,12 +26,11 @@ class DinneenIsing(Mapping): """ Ising formulation for SAT using Dinneen QUBO. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA"] @@ -41,53 +40,42 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, + {"name": "nnf", "version": "0.4.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, *DinneenQUBO.get_requirements() ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python + Returns the configurable settings for this mapping. - return { - "lagrange": { - "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) " - "constraints?" - } - } + :return: Dictionary with parameter options + .. code-block:: python + return { + "lagrange": { + "values": [0.1, 1, 2], + "description": "What Lagrange parameter to multiply with the number of (hard) " + "constraints?" + } + } """ return { "lagrange": { "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) constraints?" + "description": "What Lagrange parameter to multiply with the number of (hard) constraints?" } } class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -96,19 +84,17 @@ class Config(TypedDict): """ lagrange: float - def map(self, problem: any, config) -> (dict, float): + def map(self, problem: any, config: Config) -> tuple[dict, float]: """ Uses the DinneenQUBO formulation and converts it to an Ising. - :param problem: the SAT problem - :type problem: any - :param config: dictionary with the mapping config - :type config: Config - :return: dict with the ising, time it took to map it - :rtype: tuple(dict, float) + :param problem: SAT problem + :param config: Dictionary with the mapping config + :return: Dict with the ising, time it took to map it """ start = start_time_measurement() self.problem = problem + # call mapping function self.qubo_mapping = DinneenQUBO() q, _ = self.qubo_mapping.map(problem, config) @@ -127,28 +113,31 @@ def map(self, problem: any, config) -> (dict, float): return {"J": j_matrix, "t": t_vector}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :type: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() - # convert raw solution into the right format to use reverse_map() of ChoiQUBO.py - solution_dict = {} - for i, el in enumerate(solution): - solution_dict[i] = el - # reverse map + # Convert raw solution into the right format to use reverse_map() of ChoiQUBO.py + solution_dict = dict(enumerate(solution)) + + # Reverse map result, _ = self.qubo_mapping.reverse_map(solution_dict) return result, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py index a429ced1..5a700f29 100644 --- a/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/DinneenQUBO.py @@ -14,10 +14,11 @@ from itertools import combinations from typing import TypedDict +import logging from nnf import And -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -28,7 +29,7 @@ class DinneenQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -37,43 +38,36 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "nnf", - "version": "0.4.1" - } - ] + return [{"name": "nnf", "version": "0.4.1"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python + Returns the configurable settings for this mapping. - return { - "lagrange": { - "values": [0.1, 1, 2], - "description": "What lagrange param. to multiply with the number of (hard) constr.?" - } - } + :return: Dictionary with parameter options + .. code-block:: python + return { + "lagrange": { + "values": [0.1, 1, 2], + "description": "What Lagrange param. to multiply with the number of (hard) constr.?" + } + } """ return { "lagrange": { "values": [0.1, 1, 2], - "description": "What lagrange parameter to multiply with the number of (hard) constraints?" + "description": "What Lagrange parameter to multiply with the number of (hard) constraints?" } } class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -82,47 +76,49 @@ class Config(TypedDict): """ lagrange: float - def map(self, problem: (And, list), config: Config) -> (dict, float): + def map(self, problem: tuple[And, list], config: Config) -> tuple[dict, float]: """ Performs the mapping into a QUBO formulation, as given by Dinneen. See also the QUARK paper. - - :param problem: - :type problem: any - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) + + :param problem: SAT problem + :param config: Config with the parameters specified in Config class + :return: Tuple with the QUBO, time it took to map it """"" start = start_time_measurement() - # extract hard and soft constraints from the generated problem + + # Extract hard and soft constraints from the generated problem hard, soft = problem - # count the variables + + # Count the variables self.nr_vars = len(hard.vars().union(And(soft).vars())) lagrange = config['lagrange'] - # lagrange parameter is a factor of the number of soft constraints. + # Lagrange parameter is a factor of the number of soft constraints. lagrange *= len(soft) - def _add_clause(curr_qubo_dict, clause, pos, weight): + def _add_clause(curr_qubo_dict: dict[tuple[int, int], float], + clause: any, + pos: int, + weight: float) -> dict[tuple[int, int], float]: """ Function that adds the QUBO terms corresponding to the clause and updates the QUBO dictionary accordingly. Additionally, the weight of the clause is taken into account. - :param curr_qubo_dict: - :param clause: - :param pos: - :param weight: - :return: + :param curr_qubo_dict: Current QUBO dictionary + :param clause: Clause to be added + :param pos: Position of the auxiliary variable + :param weight: Weight of the clause + :return: Updated QUBO dictionary """ - def _check_and_add(dictionary, key, value): + def _check_and_add(dictionary: dict, key: tuple[int, int], value: float) -> dict: """ Helper function that checks if key is present or not in dictionary and adds a value, adding the key if missing. - :param dictionary: - :param key: - :param value: - :return: + :param dictionary: Dictionary to be updated + :param key: Key to check in the dictionary + :param value: Value to add to the key + :return: Updated dictionary """ key = tuple(sorted(key)) if key not in dictionary.keys(): @@ -134,45 +130,47 @@ def _check_and_add(dictionary, key, value): cl_dict = {} for variable in clause.children: for variable_name in variable.vars(): - # transforms the negations (0,1) into signs (-1, 1) + # Transforms the negations (0,1) into signs (-1, 1) cl_dict[int(variable_name[1:])] = (int(variable.true) - 1 / 2) * 2 - # add the linear term of the auxiliary variable w + # Add the linear term of the auxiliary variable w curr_qubo_dict = _check_and_add(curr_qubo_dict, (pos, pos), 2 * weight) - # add x linear terms and xw terms. + # Add x linear terms and xw terms. for qvar, val in cl_dict.items(): # qvar is the name of the var, val is the sign corresponding to whether the variable is negated or not. # linear x term: curr_qubo_dict = _check_and_add(curr_qubo_dict, (qvar, qvar), -weight * val) # x * w (aux. var.) term curr_qubo_dict = _check_and_add(curr_qubo_dict, (qvar, pos), -weight * val) - # add combinations + # Add combinations for q1, q2 in combinations(cl_dict.keys(), 2): curr_qubo_dict = _check_and_add(curr_qubo_dict, (q1, q2), weight * cl_dict[q1] * cl_dict[q2]) return curr_qubo_dict qubo_dict = {} - # first we add the hard constraints -- we add the lagrange parameter as weight + # Add the hard constraints and add the lagrange parameter as weight for clause_ind, hard_clause in enumerate(hard): qubo_dict = _add_clause(qubo_dict, hard_clause, self.nr_vars + clause_ind, lagrange) - # next, we add the soft constraints -- we start the enumeration at the final index corresponding to hard cons. + + # Add the soft constraints and start the enumeration at the final index corresponding to hard cons. for clause_ind, soft_clause in enumerate(soft): qubo_dict = _add_clause(qubo_dict, soft_clause, self.nr_vars + clause_ind + len(hard), 1) - logging.info(f"Generate Dinneen QUBO with {self.nr_vars + len(hard) + len(soft)} binary variables." - f" Lagrange parameter used was: {config['lagrange']}.") + logging.info( + f"Generate Dinneen QUBO with {self.nr_vars + len(hard) + len(soft)} binary variables." + f" Lagrange parameter used was: {config['lagrange']}." + ) + return {"Q": qubo_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Reverse mapping of the solution obtained from the Dinneen QUBO. - :param solution: dictionary containing the solution - :type solution: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() mapped_sol = {} @@ -182,10 +180,17 @@ def reverse_map(self, solution: dict) -> (dict, float): mapped_sol[f'L{i}'] = True else: mapped_sol[f'L{i}'] = bool(solution[i]) + return mapped_sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/SAT/mappings/Direct.py b/src/modules/applications/optimization/SAT/mappings/Direct.py index d140e9b9..fb889e10 100644 --- a/src/modules/applications/optimization/SAT/mappings/Direct.py +++ b/src/modules/applications/optimization/SAT/mappings/Direct.py @@ -14,12 +14,13 @@ import io from typing import TypedDict +import logging from nnf import And from nnf.dimacs import dump from pysat.formula import CNF, WCNF -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -30,7 +31,7 @@ class Direct(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["ClassicalSAT", "RandomSAT"] @@ -38,32 +39,22 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "python-sat", - "version": "1.8.dev13" - } + {"name": "nnf", "version": "0.4.1"}, + {"name": "python-sat", "version": "1.8.dev13"} ] - def get_parameter_options(self): + def get_parameter_options(self) -> dict: """ Returns empty dict as this mapping has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ @@ -71,47 +62,62 @@ class Config(TypedDict): """ pass - def map(self, problem: (And, list), config: Config) -> (WCNF, float): + def map(self, problem: tuple[And, list], config: Config) -> tuple[WCNF, float]: """ - We map from the nnf library into the python-sat library. - - :param problem: - :type problem: (nnf.And, list) - :param config: empty dict - :type config: Config - :return: mapped problem and the time it took to map it - :rtype: tuple(WCNF, float) + Map from the nnf library into the python-sat library. + + :param problem: SAT problem + :param config: Config with the parameters specified in Config class + :return: Mapped problem and the time it took to map it """ start = start_time_measurement() hard_constraints, soft_constraints = problem - # get number of vars. The union is required in case not all vars are present in either tests/constraints. + + # Get number of vars. The union is required in case not all vars are present in either tests/constraints. nr_vars = len(hard_constraints.vars().union(And(soft_constraints).vars())) - # create a var_labels dictionary that will be used when mapping to pysat + + # Create a var_labels dictionary that will be used when mapping to pysat litdic = {f'L{i - 1}': i for i in range(1, nr_vars + 1)} + # The most convenient way to map between nnf and pysat was to use the native nnf dump function, which exports # the problem as a string, which we can then quickly reload from a buffer. - # create buffers for dumping: + + # Create buffers for dumping: hard_buffer = io.StringIO() soft_buffer = io.StringIO() - # dump constraints and tests to their respective buffers + + # Dump constraints and tests to their respective buffers dump(hard_constraints, hard_buffer, var_labels=litdic, mode='cnf') # tests have to be conjoined, since we will add them as soft constraints. dump(And(soft_constraints), soft_buffer, var_labels=litdic, mode='cnf') - # load the cnfs from the buffers: + # Load the cnfs from the buffers: hard_cnf = CNF(from_string=hard_buffer.getvalue()) soft_cnf = CNF(from_string=soft_buffer.getvalue()) - # create wcnf instance. + + # Create wcnf instance. total_wcnf = WCNF() - # add hard constraints: + + # Add hard constraints: total_wcnf.extend(hard_cnf) - # add soft constraints, with weights. + + # Add soft constraints, with weights. total_wcnf.extend(soft_cnf, weights=[1] * len(soft_cnf.clauses)) - logging.info(f'Generated pysat wcnf with {len(total_wcnf.hard)} constraints and {len(total_wcnf.soft)} tests.') + + logging.info( + f'Generated pysat wcnf with {len(total_wcnf.hard)} constraints and {len(total_wcnf.soft)} tests.' + ) + return total_wcnf, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "ClassicalSAT": from modules.solvers.ClassicalSAT import ClassicalSAT # pylint: disable=C0415 return ClassicalSAT() @@ -121,16 +127,13 @@ def get_default_submodule(self, option: str) -> Core: else: raise NotImplementedError(f"Solver Option {option} not implemented") - def reverse_map(self, solution: list) -> (dict, float): + def reverse_map(self, solution: list) -> tuple[dict, float]: """ Maps the solution returned by the pysat solver into the reference format. - :param solution: dictionary containing the solution - :type solution: list - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: List containing the solution + :return: Solution mapped accordingly, time it took to map it """ - start = start_time_measurement() # converts from (3 / -3) -> (L2 : True / L2: False) mapped_sol = {f'L{abs(lit) - 1}': (lit > 0) for lit in solution} diff --git a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py index 452132d3..4d763d3b 100644 --- a/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py +++ b/src/modules/applications/optimization/SAT/mappings/QubovertQUBO.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TypedDict + from qubovert.sat import NOT, OR, AND from nnf import And -from modules.applications.Mapping import * + +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -26,7 +29,7 @@ class QubovertQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -36,47 +39,39 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "nnf", - "version": "0.4.1" - }, - { - "name": "qubovert", - "version": "1.2.5" - } + {"name": "nnf", "version": "0.4.1"}, + {"name": "qubovert", "version": "1.2.5"} ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. - :return: - .. code-block:: python - - return { - "lagrange": { - "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], - "description": "What lagrange for the qubo mapping? 1 the number of tests." - } - } + :return: Dict with configurable settings + .. code-block:: python + return { + "lagrange": { + "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], + "description": "By which factor would you like to multiply your Lagrange?" + } + } """ return { "lagrange": { "values": [0.1, 1, 1.5, 2, 5, 10, 1000, 10000], - "description": "What lagrange for the qubo mapping? 1 the number of tests." + "description": "By which factor would you like to multiply your Lagrange?" } } class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -86,14 +81,12 @@ class Config(TypedDict): lagrange: float @staticmethod - def _constraints2qubovert(constraints: dict) -> AND: + def _constraints2qubovert(constraints: any) -> AND: """ - Converts the constraints nnf to a pubo in the qubovert library. + Converts the constraints nnf to a PUBO in the qubovert library. - :param constraints: - :type constraints: dict - :return: - :rtype: AND + :param constraints: Constraints in nnf format + :return: Constraints in qubovert format """ clauses = [] for c in constraints.children: @@ -104,12 +97,10 @@ def _constraints2qubovert(constraints: dict) -> AND: @staticmethod def _tests2qubovert(test_clauses: dict) -> sum: """ - Converts the list of test clauses in the nnf format to a pubo. + Converts the list of test clauses in the nnf format to a PUBO. - :param test_clauses: - :type test_clauses: dict - :return: - :rtype: sum + :param test_clauses: Test clauses in nnf format + :return: Sum of mapped test clauses """ mapped_tests = [] @@ -118,42 +109,40 @@ def _tests2qubovert(test_clauses: dict) -> sum: return sum(mapped_tests) - def map(self, problem: any, config: Config) -> (dict, float): + def map(self, problem: any, config: Config) -> tuple[dict, float]: """ - Converts the problem to a Qubo in dictionary format. Problem is a CNF formula from the nnf library. + Converts the problem to a QUBO in dictionary format. Problem is a CNF formula from the nnf library. - :param problem: - :type problem: any - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the QUBO, time it took to map it - :rtype: tuple(dict, float) + :param problem: SAT problem + :param config: Config with the parameters specified in Config class + :return: Dict with the QUBO, time it took to map it """ start = start_time_measurement() lagrange = config['lagrange'] constraints, test_clauses = problem - # find number of the variables that appear in the tests and constraints, to verify the reverse mapping. + # Find number of the variables that appear in the tests and constraints, to verify the reverse mapping. self.nr_vars = len(constraints.vars().union(And(test_clauses).vars())) - # first we convert the constraints to qubovert: + # Convert the constraints to qubovert: constraints_pubo = self._constraints2qubovert(constraints) - # next, we convert the tests into qubovert: + # Convert the tests into qubovert: tests_pubo = self._tests2qubovert(test_clauses) logging.info(f'{tests_pubo.to_qubo().num_terms} number of terms in tests qubo') lagrange *= len(test_clauses) - # define the total pubo problem: + # Define the total PUBO problem: self.pubo_problem = -(tests_pubo + lagrange * constraints_pubo) - # convert to qubo: + + # Convert to qubo: qubo_problem = self.pubo_problem.to_qubo() qubo_problem.normalize() logging.info(f"Converted to QUBO with {qubo_problem.num_binary_variables} Variables." f" Lagrange parameter: {config['lagrange']}.") - # now we need to convert it to the right format to be accepted by Braket / Dwave + # Convert it to the right format to be accepted by Braket / Dwave q_dict = {} for k, v in qubo_problem.items(): @@ -172,26 +161,33 @@ def map(self, problem: any, config: Config) -> (dict, float): return {"Q": q_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (dict, float): + def reverse_map(self, solution: dict) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the SAT class for validation/evaluation. - :param solution: dictionary containing the solution - :type solution: dict - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: Dictionary containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() pubo_sol = self.pubo_problem.convert_solution(solution) - # Let's check if all variables appear in the solution. + + # Check if all variables appear in the solution. missing_vars = {f'L{i}' for i in range(self.nr_vars)} - set(pubo_sol.keys()) - # add values for the missing variables -- if they do not appear, then their assignment does not matter. + + # Add values for the missing variables -- if they do not appear, then their assignment does not matter. for missing_var in missing_vars: pubo_sol[missing_var] = True + return pubo_sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/SAT/mappings/__init__.py b/src/modules/applications/optimization/SAT/mappings/__init__.py index ba060a30..0ef93529 100644 --- a/src/modules/applications/optimization/SAT/mappings/__init__.py +++ b/src/modules/applications/optimization/SAT/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SAT mappings""" +""" +Module for SAT mappings + +This module initializes the SAT application +""" diff --git a/src/modules/applications/optimization/SCP/SCP.py b/src/modules/applications/optimization/SCP/SCP.py index 7f06b043..7ac11ada 100644 --- a/src/modules/applications/optimization/SCP/SCP.py +++ b/src/modules/applications/optimization/SCP/SCP.py @@ -14,23 +14,36 @@ from typing import TypedDict import pickle +import os -from modules.applications.Application import * +from modules.applications.Application import Application from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement class SCP(Optimization): """ - The set cover problem (SCP) is an optimization problem where the goal is to find the smallest subset of a given set - of elements that covers all the elements. This can be formulated as selecting the minimum number of sets from a - collection of sets, such that the union of the selected sets contains all the elements of the problem instance. - This problem has applications in areas like sensor positioning, resource allocation, and network design. + The set cover problem (SCP) is a classical combinatorial optimization problem where the objective is to find the + smallest subset of given elements that covers all required elements in a collection. This can be formulated as + selecting the minimum number of sets from a collection such that the union of the selected sets contains all + elements from the universe of the problem instance. + + SCP has widespread applications in various fields, including sensor positioning, resource allocation, and network + design. For example, in sensor positioning, SCP can help determine the fewest number of sensors required to cover + a given area. Similarly, in resource allocation, SCP helps to allocate resources in an optimal way, ensuring + coverage of all demand points while minimizing costs. Network design also uses SCP principles to efficiently place + routers or gateways in a network to ensure full coverage with minimal redundancy. + + This implementation of SCP provides configurable problem instances of different sizes, such as "Tiny," "Small," + and "Large," allowing the user to explore solutions with varying complexities. We employ various quantum-inspired + methods to solve SCP, including a mapping to the QUBO (Quadratic Unconstrained Binary Optimization) formulation + using Qubovert. These approaches allow us to explore how different optimization algorithms and frameworks perform + when applied to this challenging problem, offering insights into both classical and emerging quantum methods. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("SCP") self.submodule_options = ["qubovertQUBO"] @@ -38,8 +51,14 @@ def __init__(self): def get_solution_quality_unit(self) -> str: return "Number of selected subsets" - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> Application: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "qubovertQUBO": from modules.applications.optimization.SCP.mappings.qubovertQUBO import QubovertQUBO # pylint: disable=C0415 return QubovertQUBO() @@ -50,16 +69,16 @@ def get_parameter_options(self): """ Returns the configurable settings for this application - :return: - .. code-block:: python + :return: Dictionary containing parameter options + .. code-block:: python - return { - "model_select": { - "values": list(["Tiny", "Small", "Large"]), - "description": "Please select the problem size(s). Tiny: 4 elements, 3 subsets. Small: - 15 elements, 8 subsets. Large: 100 elements, 100 subsets" - } - } + return { + "model_select": { + "values": list(["Tiny", "Small", "Large"]), + "description": "Please select the problem size(s). Tiny: 4 elements, 3 subsets. Small: + 15 elements, 8 subsets. Large: 100 elements, 100 subsets" + } + } """ return { "model_select": { @@ -72,14 +91,12 @@ def get_parameter_options(self): class Config(TypedDict): model_select: str - def generate_problem(self, config: Config) -> (set, list): + def generate_problem(self, config: Config) -> tuple[set, list]: """ Generates predefined instances of the SCP. :param config: Config specifying the selected problem instances - :type config: Config - :return: the union of all elements of an instance and a set of subsets, each covering a part of the union - :rtype: tuple(set, list) + :return: The union of all elements of an instance and a set of subsets, each covering a part of the union """ model_select = config['model_select'] self.application = {} @@ -87,13 +104,14 @@ def generate_problem(self, config: Config) -> (set, list): if model_select == "Tiny": self.application["elements_to_cover"] = set(range(1, 4)) self.application["subsets"] = [{1, 2}, {1, 3}, {3, 4}] - elif model_select == "Small": self.application["elements_to_cover"] = set(range(1, 15)) - self.application["subsets"] = [{1, 3, 4, 6, 7, 13}, {4, 6, 8, 12}, {2, 5, 9, 11, 13}, {1, 2, 7, 14, 15}, - {3, 10, 12, 14}, {7, 8, 14, 15}, {1, 2, 6, 11}, {1, 2, 4, 6, 8, 12}] + self.application["subsets"] = [ + {1, 3, 4, 6, 7, 13}, {4, 6, 8, 12}, {2, 5, 9, 11, 13}, {1, 2, 7, 14, 15}, + {3, 10, 12, 14}, {7, 8, 14, 15}, {1, 2, 6, 11}, {1, 2, 4, 6, 8, 12} + ] - else: + elif model_select == "Large": self.application["elements_to_cover"] = set(range(1, 100)) self.application["subsets"] = [] path = os.path.join(os.path.dirname(__file__)) @@ -105,43 +123,40 @@ def generate_problem(self, config: Config) -> (set, list): new_set = set(new_set) self.application["subsets"].append(new_set) + else: + raise ValueError(f"Unknown model_select value: {model_select}") + return self.application["elements_to_cover"], self.application["subsets"] - def process_solution(self, solution: list) -> (list, float): + def process_solution(self, solution: list) -> tuple[list, float]: """ Returns list of selected subsets and the time it took to process the solution. :param solution: Unprocessed solution - :type solution: list :return: Processed solution and the time it took to process it - :rtype: tuple(list, float) """ start_time = start_time_measurement() selected_subsets = [list(self.application["subsets"][i]) for i in solution] return selected_subsets, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: list) -> tuple[bool, float]: """ Checks if the elements of the subsets that are part of the solution cover every element of the instance. - :param solution: list containing all subsets that are part of the solution - :type solution: list + :param solution: List containing all subsets that are part of the solution :return: Boolean whether the solution is valid and time it took to validate - :rtype: tuple(bool, float) """ start = start_time_measurement() covered = set.union(*[set(subset) for subset in solution]) return covered == self.application["elements_to_cover"], end_time_measurement(start) - def evaluate(self, solution: list) -> (int, float): + def evaluate(self, solution: list) -> tuple[int, float]: """ Calculates the number of subsets that are of the solution. :param solution: List containing all subsets that are part of the solution - :type solution: list :return: Number of subsets and the time it took to calculate it - :rtype: tuple(int, float) """ start = start_time_measurement() selected_num = len(solution) @@ -149,5 +164,11 @@ def evaluate(self, solution: list) -> (int, float): return selected_num, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Saves the SCP instance to a file. + + :param path: Path to save the SCP instance + :param iter_count: Iteration count + """ with open(f"{path}/SCP_instance", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/SCP/__init__.py b/src/modules/applications/optimization/SCP/__init__.py index 9596ba2b..863c0dfe 100644 --- a/src/modules/applications/optimization/SCP/__init__.py +++ b/src/modules/applications/optimization/SCP/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing the SCP""" +""" +Module for SCP mappings + +This module initializes the SCP application +""" diff --git a/src/modules/applications/optimization/SCP/data/__init__.py b/src/modules/applications/optimization/SCP/data/__init__.py index 47c23f57..5826cf71 100644 --- a/src/modules/applications/optimization/SCP/data/__init__.py +++ b/src/modules/applications/optimization/SCP/data/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SCP data""" +""" +Module for SCP mappings + +This module initializes the SCP application +""" diff --git a/src/modules/applications/optimization/SCP/mappings/__init__.py b/src/modules/applications/optimization/SCP/mappings/__init__.py index 85fad453..863c0dfe 100644 --- a/src/modules/applications/optimization/SCP/mappings/__init__.py +++ b/src/modules/applications/optimization/SCP/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for SCP mappings""" +""" +Module for SCP mappings + +This module initializes the SCP application +""" diff --git a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py index a5a27daf..d6e0e312 100644 --- a/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py +++ b/src/modules/applications/optimization/SCP/mappings/qubovertQUBO.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TypedDict + from qubovert.problems import SetCover -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement @@ -25,7 +27,7 @@ class QubovertQUBO(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -33,36 +35,29 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "qubovert", - "version": "1.2.5" - } - ] + return [{"name": "qubovert", "version": "1.2.5"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return { - "penalty_weight": { - "values": [2, 5, 10, 25, 50, 100], - "custom_input": True, - "custom_range": True, - "postproc": float, - "description": "Please choose the weight of the penalties in the QUBO representation of - the problem" - } - } + Returns the configurable settings for this mapping. + + :return: Dictionary containing configurable settings + .. code-block:: python + return { + "penalty_weight": { + "values": [2, 5, 10, 25, 50, 100], + "custom_input": True, + "custom_range": True, + "postproc": float, + "description": "Please choose the weight of the penalties in the QUBO representation of + the problem" + } + } """ return { "penalty_weight": { @@ -76,7 +71,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -85,17 +80,14 @@ class Config(TypedDict): """ penalty_weight: float - def map(self, problem: tuple, config: Config) -> (dict, float): + def map(self, problem: tuple, config: Config) -> tuple[dict, float]: """ Maps the SCP to a QUBO matrix. - :param problem: tuple containing the set of all elements of an instance and a list of subsets each covering some - of these elements - :type problem: tuple - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with QUBO matrix, time it took to map it - :rtype: tuple(dict, float) + :param problem: Tuple containing the set of all elements of an instance and a list of subsets, + each covering some of these elements + :param config: Config with the parameters specified in Config class + :return: Dict with QUBO matrix, time it took to map it """ start = start_time_measurement() penalty_weight = config['penalty_weight'] @@ -107,17 +99,17 @@ def map(self, problem: tuple, config: Config) -> (dict, float): logging.info(f"Converted to QUBO with {self.SCP_qubo.num_binary_variables} Variables.") - # now we need to convert it to the right format to be accepted by Braket / Dwave + # Convert it to the right format to be accepted by Braket / Dwave q_dict = {} for key, val in self.SCP_qubo.items(): - # interaction (quadratic) terms + # Interaction (quadratic) terms if len(key) == 2: if (key[0], key[1]) not in q_dict: q_dict[(key[0], key[1])] = float(val) else: q_dict[(key[0], key[1])] += float(val) - # local (linear) fields + # Local (linear) fields elif len(key) == 1: if (key[0], key[0]) not in q_dict: q_dict[(key[0], key[0])] = float(val) @@ -126,21 +118,26 @@ def map(self, problem: tuple, config: Config) -> (dict, float): return {"Q": q_dict}, end_time_measurement(start) - def reverse_map(self, solution: dict) -> (set, float): + def reverse_map(self, solution: dict) -> tuple[set, float]: """ Maps the solution of the QUBO to a set of subsets included in the solution. :param solution: QUBO matrix in dict form - :type solution: dict - :return: tuple with set of subsets that are part of the solution and the time it took to map it - :rtype: tuple(set, float) + :return: Tuple with set of subsets that are part of the solution and the time it took to map it """ start = start_time_measurement() sol = self.SCP_problem.convert_solution(solution) + return sol, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + :param option: Option specifying the submodule + :return: Instance of the corresponding submodule + :raises NotImplementedError: If the option is not recognized + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 return Annealer() diff --git a/src/modules/applications/optimization/TSP/TSP.py b/src/modules/applications/optimization/TSP/TSP.py index 46ef45bd..4f16c0df 100644 --- a/src/modules/applications/optimization/TSP/TSP.py +++ b/src/modules/applications/optimization/TSP/TSP.py @@ -14,18 +14,20 @@ from typing import TypedDict import pickle +import logging +import os import networkx as nx import numpy as np -from modules.applications.Application import * +from modules.applications.Application import Core from modules.applications.optimization.Optimization import Optimization from utils import start_time_measurement, end_time_measurement class TSP(Optimization): """ - \"The famous travelling salesman problem (also called the travelling salesperson problem or in short TSP) is a + "The famous travelling salesman problem (also called the travelling salesperson problem or in short TSP) is a well-known NP-hard problem in combinatorial optimization, asking for the shortest possible route that visits each node exactly once, given a list of nodes and the distances between each pair of nodes. Applications of the TSP can be found in planning, logistics, and the manufacture of microchips. In these applications, the general @@ -33,37 +35,47 @@ class TSP(Optimization): TSP as graph problem: The solution to the TSP can be viewed as a specific ordering of the vertices in a weighted graph. Taking an undirected weighted graph, nodes correspond to the graph's nodes, with paths corresponding to the - graph's edges, and a path's distance is the edge's weight. Typically, the graph is complete where each pair of nodes - is connected by an edge. If no connection exists between two nodes, one can add an arbitrarily long edge to complete - the graph without affecting the optimal tour.\" + graph's edges, and a path's distance is the edge's weight." (source: https://github.com/aws/amazon-braket-examples/tree/main/examples) """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("TSP") - self.submodule_options = ["Ising", "QUBO", "GreedyClassicalTSP", "ReverseGreedyClassicalTSP", "RandomTSP"] + self.submodule_options = [ + "Ising", "QUBO", "GreedyClassicalTSP", "ReverseGreedyClassicalTSP", "RandomTSP" + ] @staticmethod def get_requirements() -> list: + """ + Return requirements of this module. + + :return: List of dict with requirements of this module + """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"} ] def get_solution_quality_unit(self) -> str: + """ + Returns the unit of measurement for the solution quality. + + :return: Unit of measurement for the solution quality + """ return "Tour cost" def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the given option. + :param option: The chosen submodule option + :return: The corresponding submodule instance + :raises NotImplemented: If the provided option is not implemented + """ if option == "Ising": from modules.applications.optimization.TSP.mappings.ISING import Ising # pylint: disable=C0415 return Ising() @@ -86,18 +98,17 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for this application - :return: - .. code-block:: python - - return { - "nodes": { - "values": list([3, 4, 6, 8, 10, 14, 16]), - "allow_ranges": True, - "description": "How many nodes does your graph need?", - "postproc": int - } - } + :return: Dictionary with configurable settings. + .. code-block:: python + return { + "nodes": { + "values": list([3, 4, 6, 8, 10, 14, 16]), + "allow_ranges": True, + "description": "How many nodes does your graph need?", + "postproc": int + } + } """ return { "nodes": { @@ -110,7 +121,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -124,10 +135,8 @@ def _get_tsp_matrix(graph: nx.Graph) -> np.ndarray: """ Creates distance matrix out of given coordinates. - :param graph: - :type graph: networkx.Graph - :return: - :rtype: np.ndarray + :param graph: The input graph + :return: Distance matrix """ number_of_nodes = len(graph) matrix = np.zeros((number_of_nodes, number_of_nodes)) @@ -136,16 +145,15 @@ def _get_tsp_matrix(graph: nx.Graph) -> np.ndarray: for j in distance_dist.items(): matrix[i[0] - 1][j[0] - 1] = j[1] matrix[j[0] - 1][i[0] - 1] = matrix[i[0] - 1][j[0] - 1] + return matrix def generate_problem(self, config: Config) -> nx.Graph: """ Uses the reference graph to generate a problem for a given config. - :param config: - :type config: Config - :return: graph with the problem - :rtype: networkx.Graph + :param config: Configuration dictionary + :return: Graph with the problem """ if config is None: @@ -158,7 +166,6 @@ def generate_problem(self, config: Config) -> nx.Graph: graph = pickle.load(file) # Remove seams until the target number of seams is reached - # Get number of seam in graph nodes_in_graph = list(graph.nodes) nodes_in_graph.sort() @@ -167,6 +174,7 @@ def generate_problem(self, config: Config) -> nx.Graph: unwanted_nodes = nodes_in_graph[-len(nodes_in_graph) + nodes:] unwanted_nodes = [x for x in graph.nodes if x in unwanted_nodes] + # Remove one node after another for node in unwanted_nodes: graph.remove_node(node) @@ -175,29 +183,28 @@ def generate_problem(self, config: Config) -> nx.Graph: logging.error("Graph is not connected!") raise ValueError("Graph is not connected!") - # normalize graph + # Normalize graph cost_matrix = self._get_tsp_matrix(graph) graph = nx.from_numpy_array(cost_matrix) self.application = graph + return graph - def process_solution(self, solution: dict) -> (list, float): + def process_solution(self, solution: dict) -> tuple[list, float]: """ Convert dict to list of visited nodes. - :param solution: - :type solution: dict - :return: processed solution and the time it took to process it - :rtype: tuple(list, float) + :param solution: Dictionary with solution + :return: Processed solution and the time it took to process it """ start_time = start_time_measurement() nodes = self.application.nodes() start = np.min(nodes) # fill route with None values route: list = [None] * len(self.application) - # get nodes from sample - # NOTE: Prevent duplicate node entries by enforcing only one occurrence per node along route + + # Get nodes from sample logging.info(str(solution.items())) for (node, timestep), val in solution.items(): @@ -206,7 +213,7 @@ def process_solution(self, solution: dict) -> (list, float): if val and (node not in route): route[timestep] = node - # check whether every timestep has only 1 node flagged + # Check whether every timestep has only 1 node flagged for i in nodes: relevant_nodes = [] relevant_timesteps = [] @@ -219,12 +226,12 @@ def process_solution(self, solution: dict) -> (list, float): # timestep or nodes have more than 1 or 0 flags return None, end_time_measurement(start_time) - # check validity of solution + # Check validity of solution if sum(value == 1 for value in solution.values()) > len(route): logging.warning("Result is longer than route! This might be problematic!") return None, end_time_measurement(start_time) - # run heuristic replacing None values + # Run heuristic replacing None values if None in route: # get not assigned nodes nodes_unassigned = [node for node in list(nodes) if node not in route] @@ -234,25 +241,24 @@ def process_solution(self, solution: dict) -> (list, float): route[idx] = nodes_unassigned[0] nodes_unassigned.remove(route[idx]) - # cycle solution to start at provided start location + # Cycle solution to start at provided start location if start is not None and route[0] != start: - # rotate to put the start in front + # Rotate to put the start in front idx = route.index(start) route = route[idx:] + route[:idx] - # print route + # Log route parsed_route = ' ->\n'.join([f' Node {visit}' for visit in route]) logging.info(f"Route found:\n{parsed_route}") + return route, end_time_measurement(start_time) - def validate(self, solution: list) -> (bool, float): + def validate(self, solution: list) -> tuple[bool, float]: """ Checks if it is a valid TSP tour. - :param solution: list containing the nodes of the solution - :type solution: list + :param solution: List containing the nodes of the solution :return: Boolean whether the solution is valid, time it took to validate - :rtype: tuple(bool, float) """ start = start_time_measurement() nodes = self.application.nodes() @@ -266,17 +272,15 @@ def validate(self, solution: list) -> (bool, float): logging.error(f"{len([node for node in list(nodes) if node not in solution])} nodes were NOT visited") return False, end_time_measurement(start) - def evaluate(self, solution: list) -> (float, float): + def evaluate(self, solution: list) -> tuple[float, float]: """ - Find distance for given route e.g. [0, 4, 3, 1, 2] and original data. + Find distance for given route and original data. - :param solution: - :type solution: list + :param solution: List containing the nodes of the solution :return: Tour cost and the time it took to calculate it - :rtype: tuple(float, float) """ start = start_time_measurement() - # get the total distance without return + # Get the total distance without return total_dist = 0 for idx, _ in enumerate(solution[:-1]): dist = self.application[solution[idx + 1]][solution[idx]] @@ -284,16 +288,21 @@ def evaluate(self, solution: list) -> (float, float): logging.info(f"Total distance (without return): {total_dist}") - # add distance between start and end point to complete cycle + # Add distance between start and end point to complete cycle return_distance = self.application[solution[0]][solution[-1]]['weight'] - # logging.info('Distance between start and end: ' + return_distance) - # get distance for full cycle + # Get distance for full cycle distance_with_return = total_dist + return_distance logging.info(f"Total distance (including return): {distance_with_return}") return distance_with_return, end_time_measurement(start) def save(self, path: str, iter_count: int) -> None: + """ + Save the current application state to a file. + + :param path: The directory path where the file will be saved + :param iter_count: The iteration count to include in the filename + """ with open(f"{path}/graph_iter_{iter_count}.gpickle", "wb") as file: pickle.dump(self.application, file, pickle.HIGHEST_PROTOCOL) diff --git a/src/modules/applications/optimization/TSP/__init__.py b/src/modules/applications/optimization/TSP/__init__.py index 3a943cad..9ddfa960 100644 --- a/src/modules/applications/optimization/TSP/__init__.py +++ b/src/modules/applications/optimization/TSP/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing the TSP""" +""" +Module for TSP mappings + +This module initializes the TSP application +""" diff --git a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py index c9383b29..04abb882 100644 --- a/src/modules/applications/optimization/TSP/data/createReferenceGraph.py +++ b/src/modules/applications/optimization/TSP/data/createReferenceGraph.py @@ -18,19 +18,31 @@ # Source http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/tsp/ filename = "dsj1000.tsp" -print(f"Loading {filename}") -# Load the problem from .tsp file -problem = tsplib95.load(filename) -graph = problem.get_graph() - -# We don't needed edges from e.g. node0 -> node0 -for edge in graph.edges: - if edge[0] == edge[1]: - graph.remove_edge(edge[0], edge[1]) -print("Loaded graph:") -print(nx.info(graph)) - -with open("reference_graph.gpickle", "wb") as file: - pickle.dump(graph, file, pickle.HIGHEST_PROTOCOL) - -print("Saved graph as reference_graph.gpickle") + + +def main(): + """ + Load a TSP problem, remove unnecessary edges, and save the reference graph. + """ + print(f"Loading {filename}") + + # Load the problem from .tsp file + problem = tsplib95.load(filename) + graph = problem.get_graph() + + # We don't needed edges from e.g. node0 -> node0 + for edge in graph.edges: + if edge[0] == edge[1]: + graph.remove_edge(edge[0], edge[1]) + + print("Loaded graph:") + print(nx.info(graph)) + + with open("reference_graph.gpickle", "wb") as file: + pickle.dump(graph, file, pickle.HIGHEST_PROTOCOL) + + print("Saved graph as reference_graph.gpickle") + + +if __name__ == '__main__': + main() diff --git a/src/modules/applications/optimization/TSP/mappings/ISING.py b/src/modules/applications/optimization/TSP/mappings/ISING.py index 88553ad3..131ea279 100644 --- a/src/modules/applications/optimization/TSP/mappings/ISING.py +++ b/src/modules/applications/optimization/TSP/mappings/ISING.py @@ -14,6 +14,7 @@ import re from typing import TypedDict +import logging import networkx as nx import numpy as np @@ -22,9 +23,8 @@ from pyqubo import Array, Placeholder, Constraint from qiskit_optimization.applications import Tsp from qiskit_optimization.converters import QuadraticProgramToQubo -from qiskit.quantum_info import SparsePauliOp -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from modules.applications.optimization.TSP.mappings.QUBO import QUBO from utils import start_time_measurement, end_time_measurement @@ -36,7 +36,7 @@ class Ising(Mapping): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["QAOA", "PennylaneQAOA", "QiskitQAOA"] @@ -47,57 +47,37 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "dimod", - "version": "0.12.17" - }, - { - "name": "more-itertools", - "version": "10.5.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, - { - "name": "pyqubo", - "version": "1.4.0" - }, + {"name": "networkx", "version": "3.2.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "dimod", "version": "0.12.17"}, + {"name": "more-itertools", "version": "10.5.0"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, + {"name": "pyqubo", "version": "1.4.0"}, *QUBO.get_requirements() ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping - - :return: - .. code-block:: python - - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?" - }, - "mapping": { - "values": ["ocean", "qiskit", "pyqubo"], - "description": "Which Ising formulation of the TSP problem should be used?" - } - } + Returns the configurable settings for this mapping. + :return: Dictionary containing parameter options. + .. code-block:: python + + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your lagrange?" + }, + "mapping": { + "values": ["ocean", "qiskit", "pyqubo"], + "description": "Which Ising formulation of the TSP problem should be used?" + } + } """ return { "lagrange_factor": { @@ -112,7 +92,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -123,20 +103,17 @@ class Config(TypedDict): lagrange_factor: float mapping: str - def map(self, problem: nx.Graph, config: Config) -> (dict, float): + def map(self, problem: nx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to an Ising formulation. - :param problem: networkx graph - :type problem: networkx.Graph - :param config: config with the parameters specified in Config class - :param config: Config - :return: dict with Ising, time it took to map it - :rtype: tuple(dict, float) + :param problem: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with Ising, time it took to map it """ self.graph = problem self.config = config - # call mapping function defined in configuration + # Call mapping function defined in configuration mapping = self.config["mapping"] if mapping == "ocean": return self._map_ocean(problem, config) @@ -153,10 +130,8 @@ def _create_pyqubo_model(cost_matrix: list) -> any: """ This PyQubo formulation of the TSP was kindly provided by AWS. - :param cost_matrix: - :type cost_matrix: list - :return: - :rtype: any + :param cost_matrix: Cost matrix of the TSP + :return: Compiled PyQubo model """ n = len(cost_matrix) x = Array.create('c', (n, n), 'BINARY') @@ -186,25 +161,17 @@ def _create_pyqubo_model(cost_matrix: list) -> any: # Compile model model = H.compile() + return model @staticmethod def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: """ - Converts dictionary index (e.g. 'c[0][2]') in PyQubo to matrix index. - - (e.g. 2 - {('c[0][2]', 'c[2][1]'): 0.06161479507592913, - ('c[0][0]', 'c[0][1]'): 20.0, - ('c[1][0]', 'c[2][1]'): 0.720033199087941, - ... } - - :param ising_index_string: - :type ising_index_string: any - :param number_nodes: - :type number_nodes: any - :return: - :rtype: any + Converts dictionary index in PyQubo to matrix index. + + :param ising_index_string: Index string from PyQubo + :param number_nodes: Number of nodes in the graph + :return: Matrix index """ x = 0 y = 0 @@ -217,20 +184,15 @@ def _get_matrix_index(ising_index_string: any, number_nodes: any) -> any: return idx - def _map_pyqubo(self, graph: nx.Graph, config: Config) -> (dict, float): + def _map_pyqubo(self, graph: nx.Graph, config: Config) -> tuple[dict, float]: """ Use Qubo / Ising model defined in PyQubo. - :param graph: networkx graph - :type graph: networkx.Graph - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) + :param graph: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ start = start_time_measurement() - # cost_matrix = nx.to_numpy_matrix(graph) #self.get_tsp_matrix(graph) - # cost_matrix = self.get_tsp_matrix(graph) cost_matrix = np.array(nx.to_numpy_array(graph, weight="weight")) model = self._create_pyqubo_model(cost_matrix) feed_dict = {'A': 2.0} @@ -254,25 +216,17 @@ def _map_pyqubo(self, graph: nx.Graph, config: Config) -> (dict, float): x = self._get_matrix_index(key[0], graph.number_of_nodes()) y = self._get_matrix_index(key[1], graph.number_of_nodes()) j_matrix[x][y] = value - # j_matrix = np.triu(j_matrix, k=1).astype(np.float64) - # print("Number items in Ising dict: {} Number of non-zero entries in matrix: {}".\ - # format(len(quad.items()), len(j_matrix.nonzero()))) return {"J": j_matrix, "J_dict": quad, "t_dict": linear, "t": t_matrix}, end_time_measurement(start) - def _map_ocean(self, graph: nx.Graph, config: Config) -> (dict, float): - """ - Use D-Wave/Ocean TSP QUBO/Ising model: - https://docs.ocean.dwavesys.com/en/stable/docs_dnx/reference/algorithms/generated/dwave_networkx.algorithms.tsp.traveling_salesperson_qubo.html#dwave_networkx.algorithms.tsp.traveling_salesperson_qubo - - :param graph: networkx graph - :type graph: networkx.Graph - :param config: config with the parameters specified in Config class - :param config: Config - :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) + def _map_ocean(self, graph: nx.Graph, config: Config) -> tuple[dict, float]: """ + Use D-Wave/Ocean TSP QUBO/Ising model. + :param graph: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it + """ start = start_time_measurement() qubo_mapping = QUBO() q, _ = qubo_mapping.map(graph, config) @@ -280,9 +234,6 @@ def _map_ocean(self, graph: nx.Graph, config: Config) -> (dict, float): # Convert ISING dict to matrix timesteps = graph.number_of_nodes() - - # number_of_edges = graph.number_of_edges() - # timesteps = len(Q)/number_of_edges matrix_size = graph.number_of_nodes() * timesteps j_matrix = np.zeros((matrix_size, matrix_size), dtype=float) @@ -300,27 +251,18 @@ def _map_ocean(self, graph: nx.Graph, config: Config) -> (dict, float): v = self.key_mapping[key[1]] j_matrix[u][v] = value - logging.info(j_matrix) - logging.info(j_matrix.shape) - # j_matrix = self.convert_to_upper_triangular_form(j_matrix) - # logging.info("Upper triangle form: ") - # logging.info(j_matrix) - return {"J": j_matrix, "t": np.array(list(t.values())), "J_dict": j}, end_time_measurement(start) @staticmethod - def _map_qiskit(graph: nx.Graph, config: Config) -> (dict, float): + def _map_qiskit(graph: nx.Graph, config: Config) -> tuple[dict, float]: """ Use Ising Mapping of Qiskit Optimize: TSP class: https://qiskit.org/documentation/optimization/stubs/qiskit_optimization.applications.Tsp.html Example notebook: https://qiskit.org/documentation/tutorials/optimization/6_examples_max_cut_and_tsp.html - :param graph: networkx graph - :type graph: networkx.Graph - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with the Ising, time it took to map it - :rtype: tuple(dict, float) + :param graph: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with the Ising, time it took to map it """ start = start_time_measurement() tsp = Tsp(graph) @@ -329,30 +271,28 @@ def _map_qiskit(graph: nx.Graph, config: Config) -> (dict, float): qp2qubo = QuadraticProgramToQubo() qubo = qp2qubo.convert(qp) qubitOp, _ = qubo.to_ising() - # reverse generate J and t out of qubit PauliSumOperator from qiskit + + # Reverse generate J and t out of qubit PauliSumOperator from qiskit t_matrix = np.zeros(qubitOp.num_qubits, dtype=complex) j_matrix = np.zeros((qubitOp.num_qubits, qubitOp.num_qubits), dtype=complex) pauli_list = qubitOp.to_list() + for pauli_str, coeff in pauli_list: - logging.info((pauli_str, coeff)) pauli_str_list = list(pauli_str) index_pos_list = list(locate(pauli_str_list, lambda a: a == 'Z')) if len(index_pos_list) == 1: - # update t t_matrix[index_pos_list[0]] = coeff elif len(index_pos_list) == 2: j_matrix[index_pos_list[0]][index_pos_list[1]] = coeff return {"J": j_matrix, "t": t_matrix}, end_time_measurement(start) - def reverse_map(self, solution: any) -> (dict, float): + def reverse_map(self, solution: any) -> tuple[dict, float]: """ Maps the solution back to the representation needed by the TSP class for validation/evaluation. - :param solution: list or array containing the solution - :type solution: any - :return: solution mapped accordingly, time it took to map it - :rtype: tuple(dict, float) + :param solution: List or array containing the solution + :return: Solution mapped accordingly, time it took to map it """ start = start_time_measurement() if -1 in solution: # ising model output from Braket QAOA @@ -377,30 +317,47 @@ def reverse_map(self, solution: any) -> (dict, float): for key, value in self.key_mapping.items(): result[key] = 1 if solution[value] == 1 else 0 - logging.info(result) return result, end_time_measurement(start) @staticmethod def _flip_bits_in_bitstring(solution: any) -> any: - # depending on used solver 0 or 1 can indicate a node per timestep - # if np.count_nonzero(solution) > n: + """ + Flip bits in the solution bitstring to unify different mappings. + + :param solution: Solution bitstring + :return: Flipped solution bitstring + """ solution = np.array(solution) with np.nditer(solution, op_flags=['readwrite']) as it: for x in it: x[...] = 1 - x + return solution @staticmethod def _convert_ising_to_qubo(solution: any) -> any: + """ + Convert Ising model output to QUBO format. + + :param solution: Ising model output + :return: QUBO format solution + """ solution = np.array(solution) with np.nditer(solution, op_flags=['readwrite']) as it: for x in it: if x == -1: x[...] = 0 + return solution def get_default_submodule(self, option: str) -> Core: + """ + Get the default submodule based on the given option. + :param option: Submodule option + :return: Corresponding submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QAOA": from modules.solvers.QAOA import QAOA # pylint: disable=C0415 return QAOA() diff --git a/src/modules/applications/optimization/TSP/mappings/QUBO.py b/src/modules/applications/optimization/TSP/mappings/QUBO.py index e1d762d2..ca22fce1 100644 --- a/src/modules/applications/optimization/TSP/mappings/QUBO.py +++ b/src/modules/applications/optimization/TSP/mappings/QUBO.py @@ -13,23 +13,23 @@ # limitations under the License. from typing import TypedDict +import logging import dwave_networkx as dnx import networkx -from modules.applications.Mapping import * +from modules.applications.Mapping import Mapping, Core from utils import start_time_measurement, end_time_measurement class QUBO(Mapping): """ QUBO formulation for the TSP. - """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Annealer"] @@ -37,44 +37,36 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "networkx", - "version": "3.2.1" - }, - { - "name": "dwave_networkx", - "version": "0.8.15" - } + {"name": "networkx", "version": "3.2.1"}, + {"name": "dwave_networkx", "version": "0.8.15"} ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this mapping + Returns the configurable settings for this mapping. - :return: - .. code-block:: python - - return { - "lagrange_factor": { - "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your " - "lagrange?", - "custom_input": True, - "postproc": float - } - } + :return: Dictionary with configurable settings + .. code-block:: python + return { + "lagrange_factor": { + "values": [0.75, 1.0, 1.25], + "description": "By which factor would you like to multiply your " + "Lagrange?", + "custom_input": True, + "postproc": float + } + } """ return { "lagrange_factor": { "values": [0.75, 1.0, 1.25], - "description": "By which factor would you like to multiply your lagrange?", + "description": "By which factor would you like to multiply your Lagrange?", "custom_input": True, "allow_ranges": True, "postproc": float # Since we allow custom input here we need to parse it to float (input is str) @@ -83,7 +75,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -92,22 +84,18 @@ class Config(TypedDict): """ lagrange_factor: float - def map(self, problem: networkx.Graph, config: Config) -> (dict, float): + def map(self, problem: networkx.Graph, config: Config) -> tuple[dict, float]: """ Maps the networkx graph to a QUBO formulation. - :param problem: networkx graph - :type problem: networkx.Graph - :param config: config with the parameters specified in Config class - :type config: Config - :return: dict with QUBO, time it took to map it - :rtype: tuple(dict, float) + :param problem: Networkx graph + :param config: Config with the parameters specified in Config class + :return: Dict with QUBO, time it took to map it """ start = start_time_measurement() lagrange = None lagrange_factor = config['lagrange_factor'] weight = 'weight' - # get corresponding QUBO step by step if lagrange is None: # If no lagrange parameter provided, set to 'average' tour length. @@ -129,6 +117,13 @@ def map(self, problem: networkx.Graph, config: Config) -> (dict, float): return {"Q": q}, end_time_measurement(start) def get_default_submodule(self, option: str) -> Core: + """ + Get the default submodule based on the given option. + + :param option: Submodule option + :return: Corresponding submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "Annealer": from modules.solvers.Annealer import Annealer # pylint: disable=C0415 diff --git a/src/modules/applications/optimization/TSP/mappings/__init__.py b/src/modules/applications/optimization/TSP/mappings/__init__.py index 432113b3..9ddfa960 100644 --- a/src/modules/applications/optimization/TSP/mappings/__init__.py +++ b/src/modules/applications/optimization/TSP/mappings/__init__.py @@ -12,4 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for TSP mappings""" +""" +Module for TSP mappings + +This module initializes the TSP application +""" diff --git a/src/modules/applications/optimization/__init__.py b/src/modules/applications/optimization/__init__.py index 7bbc966c..658c9cb8 100644 --- a/src/modules/applications/optimization/__init__.py +++ b/src/modules/applications/optimization/__init__.py @@ -12,4 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" Module containing all optimization applications""" +""" +Module containing all optimization applications +""" diff --git a/src/modules/applications/qml/Circuit.py b/src/modules/applications/qml/Circuit.py new file mode 100644 index 00000000..0a7b4c3f --- /dev/null +++ b/src/modules/applications/qml/Circuit.py @@ -0,0 +1,33 @@ +# 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.Core import Core + + +class Circuit(Core, ABC): + """ + This module is abstract base class for the library-agnostic gate sequence, that define a quantum circuit. + """ + + @abstractmethod + def generate_gate_sequence(self, input_data: dict, config: any) -> dict: + """ + Generates the library agnostic gate sequence, a well-defined definition of the quantum circuit. + + :param input_data: Input data required to generate the gate sequence + :param config: Configuration for the gate sequence + :return: Generated gate sequence + """ + pass diff --git a/src/modules/applications/qml/DataHandler.py b/src/modules/applications/qml/DataHandler.py new file mode 100644 index 00000000..da318932 --- /dev/null +++ b/src/modules/applications/qml/DataHandler.py @@ -0,0 +1,69 @@ +# 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 + +import pandas as pd +from tensorboard.backend.event_processing.event_accumulator import EventAccumulator + + +class DataHandler(ABC): + """ + Abstract base class for DataHandler. This class defines the + necessary methods that both supervised and unsupervised QML applciations + must implement. + """ + + @abstractmethod + def data_load(self, gen_mod: dict, config: dict) -> tuple[any, float]: + """ + Helps to ensure that the model can effectively learn the underlying + patterns and structure of the data, and produce high-quality outputs. + + :param gen_mod: Dictionary with collected information of the previous modules + :param config: Config specifying the parameters of the data handler + :return: Mapped problem and the time it took to create the mapping + """ + pass + + @abstractmethod + def evaluate(self, solution: any) -> tuple[any, float]: + """ + Computes the best loss values. + + :param solution: Solution data + :return: Evaluation data and the time it took to create it + """ + pass + + @staticmethod + def tb_to_pd(logdir: str, rep: str) -> None: + """ + Converts TensorBoard event files in the specified log directory + into a pandas DataFrame and saves it as a pickle file. + + :param logdir: Path to the log directory containing TensorBoard event files + :param rep: Repetition counter + """ + event_acc = EventAccumulator(logdir) + event_acc.Reload() + tags = event_acc.Tags() + data = [] + tag_data = {} + for tag in tags['scalars']: + data = event_acc.Scalars(tag) + tag_values = [d.value for d in data] + tag_data[tag] = tag_values + data = pd.DataFrame(tag_data, index=[d.step for d in data]) + data.to_pickle(f"{logdir}/data_{rep}.pkl") diff --git a/src/modules/applications/qml/Model.py b/src/modules/applications/qml/Model.py new file mode 100644 index 00000000..f0f11f18 --- /dev/null +++ b/src/modules/applications/qml/Model.py @@ -0,0 +1,59 @@ +# 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 + +from abc import ABC, abstractmethod + + +class Model(ABC): + """ + Abstract base class for any quantum model. This class defines the necessary methods + that models like 'LibraryGenerative' must implement. + """ + + @abstractmethod + def sequence_to_circuit(self, input_data: dict) -> dict: + """ + Abstract method to convert a sequence into a quantum circuit. + + :param input_data: Input data representing the gate sequence + :return: A dictionary representing the quantum circuit + """ + pass + + @staticmethod + @abstractmethod + def get_execute_circuit(circuit: any, backend: any, config: str, config_dict: dict) -> tuple[any, any]: + """ + This method combines the circuit implementation and the selected backend and returns a function that will be + called during training. + + :param circuit: Implementation of the quantum circuit + :param backend: Configured qiskit backend + :param config: Name of a backend + :param config_dict: Dictionary including the number of shots + :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the + transpiled circuit + """ + pass + + @staticmethod + @abstractmethod + def select_backend(config: str, n_qubits: int) -> any: + """ + This method configures the backend. + + :param config: Name of a backend + :param n_qubits: Number of qubits + :return: Configured backend + """ + pass diff --git a/src/modules/applications/QML/QML.py b/src/modules/applications/qml/QML.py similarity index 60% rename from src/modules/applications/QML/QML.py rename to src/modules/applications/qml/QML.py index 15ab5699..91f34fc5 100644 --- a/src/modules/applications/QML/QML.py +++ b/src/modules/applications/qml/QML.py @@ -12,25 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.applications.Application import * +from abc import ABC, abstractmethod + +from modules.applications.Application import Application class QML(Application, ABC): """ - QML Module for QUARK, is used by all QML applications + qml Module for QUARK, is used by all qml applications. """ @abstractmethod - def generate_problem(self, config) -> any: + def generate_problem(self, config: dict) -> any: """ - Creates a concrete problem and returns it - :param config: - :type config: dict - :return: - :rtype: any + Creates a concrete problem and returns it. + + :param config: Configuration dictionary + :return: Generated problem """ pass def save(self, path: str, iter_count: int) -> None: - # Transform tensorboard output file to pandas dataframe + """ + Placeholder method for saving output to a file. + + :param path: Path to save the file + :param iter_count: Iteration count + """ pass diff --git a/src/modules/applications/qml/Training.py b/src/modules/applications/qml/Training.py new file mode 100644 index 00000000..17ea21de --- /dev/null +++ b/src/modules/applications/qml/Training.py @@ -0,0 +1,33 @@ +# 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 + + +class Training(ABC): + """ + Abstract base class for training QML models. + """ + + @abstractmethod + def start_training(self, input_data: dict, config: any, **kwargs: dict) -> dict: + """ + This function starts the training of QML model or deploys a pretrained model. + + :param input_data: A representation of the quantum machine learning model that will be trained + :param config: Config specifying the parameters of the training (dict-like Config type defined in children) + :param kwargs: Optional additional settings + :return: Solution, the time it took to compute it and some optional additional information + """ + pass diff --git a/src/modules/applications/QML/__init__.py b/src/modules/applications/qml/__init__.py similarity index 100% rename from src/modules/applications/QML/__init__.py rename to src/modules/applications/qml/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py b/src/modules/applications/qml/generative_modeling/GenerativeModeling.py similarity index 69% rename from src/modules/applications/QML/generative_modeling/GenerativeModeling.py rename to src/modules/applications/qml/generative_modeling/GenerativeModeling.py index 5a71ef51..b1c2852d 100644 --- a/src/modules/applications/QML/generative_modeling/GenerativeModeling.py +++ b/src/modules/applications/qml/generative_modeling/GenerativeModeling.py @@ -15,10 +15,10 @@ from typing import Union from utils import start_time_measurement, end_time_measurement -from modules.applications.Application import * -from modules.applications.QML.QML import QML -from modules.applications.QML.generative_modeling.data.data_handler.DiscreteData import DiscreteData -from modules.applications.QML.generative_modeling.data.data_handler.ContinuousData import ContinuousData +from modules.applications.Application import Application +from modules.applications.qml.QML import QML +from modules.applications.qml.generative_modeling.data.data_handler.DiscreteData import DiscreteData +from modules.applications.qml.generative_modeling.data.data_handler.ContinuousData import ContinuousData class GenerativeModeling(QML): @@ -31,7 +31,7 @@ class GenerativeModeling(QML): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("GenerativeModeling") self.submodule_options = ["Continuous Data", "Discrete Data"] @@ -40,10 +40,9 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dicts with requirements of this module - :rtype: list[dict] + :return: List of dicts with requirements of this module """ return [] @@ -51,6 +50,13 @@ def get_solution_quality_unit(self) -> str: return "minimum KL" def get_default_submodule(self, option: str) -> Union[ContinuousData, DiscreteData]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "Continuous Data": self.data = ContinuousData() elif option == "Discrete Data": @@ -61,18 +67,17 @@ def get_default_submodule(self, option: str) -> Union[ContinuousData, DiscreteDa def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. - :return: - .. code-block:: python - - return { - "n_qubits": { - "values": [4, 6, 8, 10, 12], - "description": "How many qubits do you want to use?" - } - } + :return: Dictionary of configurable parameters + .. code-block:: python + return { + "n_qubits": { + "values": [4, 6, 8, 10, 12], + "description": "How many qubits do you want to use?" + } + } """ return { "n_qubits": { @@ -82,32 +87,23 @@ def get_parameter_options(self) -> dict: } def generate_problem(self, config: dict) -> dict: - """ The number of qubits is chosen for this problem. - :param config: dictionary including the number of qubits - :type config: dict - :return: dictionary with the number of qubits - :rtype: dict + :param config: Dictionary including the number of qubits + :return: Dictionary with the number of qubits """ - application_config = {"n_qubits": config["n_qubits"]} return application_config def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: """ Generate the actual problem instance in the preprocess function. - :param input_data: Usually not used for this method. - :type input_data: dict - :param config: config for the problem creation. - :type config: dict - :param kwargs: Optional additional arguments - :type kwargs: dict - :param kwargs: optional additional arguments. - :return: tuple containing qubit number and the function's computation time - :rtype: tuple[dict, float] + :param input_data: Usually not used for this method + :param config: Config for the problem creation + :param kwargs: Optional additional arguments + :return: Tuple containing qubit number and the function's computation time """ start = start_time_measurement() output = self.generate_problem(config) @@ -119,14 +115,9 @@ def postprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[d Process the solution here, then validate and evaluate it. :param input_data: A representation of the quantum machine learning model that will be trained - :type input_data: dict :param config: Config specifying the parameters of the training - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict - :return: tuple with input_data and the function's computation time - :rtype: tuple[dict, float] + :param kwargs: Optional keyword arguments + :return: Tuple with input_data and the function's computation time """ - start = start_time_measurement() return input_data, end_time_measurement(start) diff --git a/src/modules/applications/QML/generative_modeling/__init__.py b/src/modules/applications/qml/generative_modeling/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/__init__.py rename to src/modules/applications/qml/generative_modeling/__init__.py diff --git a/src/modules/circuits/CircuitCardinality.py b/src/modules/applications/qml/generative_modeling/circuits/CircuitCardinality.py similarity index 74% rename from src/modules/circuits/CircuitCardinality.py rename to src/modules/applications/qml/generative_modeling/circuits/CircuitCardinality.py index 2be59116..06c73c3d 100644 --- a/src/modules/circuits/CircuitCardinality.py +++ b/src/modules/applications/qml/generative_modeling/circuits/CircuitCardinality.py @@ -12,48 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from typing import TypedDict +from typing import Union, TypedDict -from modules.circuits.Circuit import Circuit -from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit -from modules.applications.QML.generative_modeling.mappings.LibraryPennylane import LibraryPennylane -from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend -from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend +from modules.applications.qml.generative_modeling.circuits.CircuitGenerative import CircuitGenerative +from modules.applications.qml.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.qml.generative_modeling.mappings.LibraryPennylane import LibraryPennylane +from modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend -class CircuitCardinality(Circuit): +class CircuitCardinality(CircuitGenerative): """ This class generates a library-agnostic gate sequence, i.e. a list containing information - about the gates and the wires they act on. + about the gates and the wires they act on. The circuit follows the implementation by Gili et al. https://arxiv.org/abs/2207.13645 """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("CircuitCardinality") self.submodule_options = [ "LibraryQiskit", "LibraryPennylane", "CustomQiskitNoisyBackend", - "PresetQiskitNoisyBackend"] + "PresetQiskitNoisyBackend" + ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for this circuit. - :return: - .. code-block:: python - - return { - "depth": { - "values": [2, 4, 8, 16], - "description": "What depth do you want?" - } - } + :return: Dictionary with parameter options + .. code-block:: python + return { + "depth": { + "values": [2, 4, 8, 16], + "description": "What depth do you want?" + } + } """ return { @@ -63,8 +62,15 @@ def get_parameter_options(self) -> dict: }, } - def get_default_submodule(self, option: str) ->\ - Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule(self, option: str) \ + -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": @@ -78,7 +84,7 @@ def get_default_submodule(self, option: str) ->\ class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -89,14 +95,11 @@ class Config(TypedDict): def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ - Returns gate sequence of cardinality circuit architecture - + Returns gate sequence of cardinality circuit architecture. + :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :return: Dictionary including the gate sequence of the Cardinality Circuit - :rtype: dict """ n_qubits = input_data["n_qubits"] depth = config["depth"] // 2 @@ -139,7 +142,7 @@ def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: "store_dir_iter": input_data["store_dir_iter"], "train_size": input_data["train_size"], "dataset_name": input_data["dataset_name"], - "binary_train":input_data["binary_train"] + "binary_train": input_data["binary_train"] } return output_dict diff --git a/src/modules/circuits/CircuitCopula.py b/src/modules/applications/qml/generative_modeling/circuits/CircuitCopula.py similarity index 77% rename from src/modules/circuits/CircuitCopula.py rename to src/modules/applications/qml/generative_modeling/circuits/CircuitCopula.py index 461c7be5..7d943831 100644 --- a/src/modules/circuits/CircuitCopula.py +++ b/src/modules/applications/qml/generative_modeling/circuits/CircuitCopula.py @@ -12,58 +12,50 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from typing import TypedDict +from typing import Union, TypedDict from itertools import combinations - from scipy.special import binom -from modules.circuits.Circuit import Circuit -from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit -from modules.applications.QML.generative_modeling.mappings.LibraryPennylane import LibraryPennylane -from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend -from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend +from modules.applications.qml.generative_modeling.circuits.CircuitGenerative import CircuitGenerative +from modules.applications.qml.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.qml.generative_modeling.mappings.LibraryPennylane import LibraryPennylane +from modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend -class CircuitCopula(Circuit): +class CircuitCopula(CircuitGenerative): """ This class generates a library-agnostic gate sequence, i.e. a list containing information - about the gates and the wires they act on. The marginal ditribtions generated by the copula + about the gates and the wires they act on. The marginal distributions generated by the copula are uniformaly distributed. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("DiscreteCopula") self.submodule_options = [ "LibraryQiskit", "LibraryPennylane", "CustomQiskitNoisyBackend", - "PresetQiskitNoisyBackend"] + "PresetQiskitNoisyBackend" + ] @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "scipy", - "version": "1.12.0" - } - ] + return [{"name": "scipy", "version": "1.12.0"}] def get_parameter_options(self) -> dict: """ Returns the configurable settings for this Copula Circuit. - :return: - + :return: Dictionary of parameter options .. code-block:: python return { @@ -81,8 +73,15 @@ def get_parameter_options(self) -> dict: }, } - def get_default_submodule(self, option: str) -> \ - Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule(self, option: str) \ + -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": @@ -96,7 +95,7 @@ def get_default_submodule(self, option: str) -> \ class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -107,14 +106,11 @@ class Config(TypedDict): def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ - Returns gate sequence of copula architecture - + Returns gate sequence of copula architecture. + :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :return: Dictionary including the gate sequence of the Copula Circuit - :rtype: dict """ n_registers = input_data["n_registers"] n_qubits = input_data["n_qubits"] diff --git a/src/modules/circuits/Circuit.py b/src/modules/applications/qml/generative_modeling/circuits/CircuitGenerative.py similarity index 74% rename from src/modules/circuits/Circuit.py rename to src/modules/applications/qml/generative_modeling/circuits/CircuitGenerative.py index 15b3e571..f6824ffa 100644 --- a/src/modules/circuits/Circuit.py +++ b/src/modules/applications/qml/generative_modeling/circuits/CircuitGenerative.py @@ -12,64 +12,54 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod +from abc import ABC from modules.Core import Core from utils import start_time_measurement, end_time_measurement +from modules.applications.qml.Circuit import Circuit -class Circuit(Core, ABC): + +class CircuitGenerative(Circuit, Core, ABC): """ This module is abstract base class for the library-agnostic gate sequence, that define a quantum circuit. """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. + + :param name: The name of the circuit architecture """ super().__init__() self.architecture_name = name - @abstractmethod - def generate_gate_sequence(self, input_data: dict, config: any) -> dict: - """ - Generates the library agnostic gate sequence, a well-defined definition of the quantum circuit. - """ - pass - def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ Library-agnostic implementation of the gate sequence, that will be mapped to backend such as Qiskit in the subsequent module. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param kwargs: Optional keyword arguments :return: Dictionary including the dataset, the gate sequence needed for circuit construction, and the time it took generate the gate sequence. - :rtype: tuple[dict, float] """ start = start_time_measurement() circuit_constr = self.generate_gate_sequence(input_data, config) - if "generalization_metrics" in list(input_data.keys()): + if "generalization_metrics" in input_data: circuit_constr["generalization_metrics"] = input_data["generalization_metrics"] + return circuit_constr, end_time_measurement(start) def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - Method that passes back information of the subsequent modules to the preceding modules. + Method that passes back information of the subsequent modules to the preceding modules. :param input_data: Collected information of the benchmarking process - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict - :return: Same dictionary like input_data with architecture_name - :rtype: tuple[dict, float] + :param kwargs: Optional keyword arguments + :return: Same dictionary like input_data with architecture_name and execution time """ start = start_time_measurement() input_data["architecture_name"] = self.architecture_name diff --git a/src/modules/circuits/CircuitStandard.py b/src/modules/applications/qml/generative_modeling/circuits/CircuitStandard.py similarity index 70% rename from src/modules/circuits/CircuitStandard.py rename to src/modules/applications/qml/generative_modeling/circuits/CircuitStandard.py index fb4a5d91..6d419de9 100644 --- a/src/modules/circuits/CircuitStandard.py +++ b/src/modules/applications/qml/generative_modeling/circuits/CircuitStandard.py @@ -12,17 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union -from typing import TypedDict +from typing import Union, TypedDict -from modules.circuits.Circuit import Circuit -from modules.applications.QML.generative_modeling.mappings.LibraryQiskit import LibraryQiskit -from modules.applications.QML.generative_modeling.mappings.LibraryPennylane import LibraryPennylane -from modules.applications.QML.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend -from modules.applications.QML.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend +from modules.applications.qml.generative_modeling.circuits.CircuitGenerative import CircuitGenerative +from modules.applications.qml.generative_modeling.mappings.LibraryQiskit import LibraryQiskit +from modules.applications.qml.generative_modeling.mappings.LibraryPennylane import LibraryPennylane +from modules.applications.qml.generative_modeling.mappings.PresetQiskitNoisyBackend import PresetQiskitNoisyBackend +from modules.applications.qml.generative_modeling.mappings.CustomQiskitNoisyBackend import CustomQiskitNoisyBackend -class CircuitStandard(Circuit): +class CircuitStandard(CircuitGenerative): """ This class generates a library-agnostic gate sequence, i.e. a list containing information about the gates and the wires they act on. @@ -30,22 +29,22 @@ class CircuitStandard(Circuit): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("DiscreteStandard") self.submodule_options = [ "LibraryQiskit", "LibraryPennylane", "CustomQiskitNoisyBackend", - "PresetQiskitNoisyBackend"] + "PresetQiskitNoisyBackend" + ] @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [] @@ -53,15 +52,15 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for this standard circuit. - :return: - .. code-block:: python + :return: Dictionary of parameter options. + .. code-block:: python - return { - "depth": { - "values": [1, 2, 3], - "description": "What depth do you want?" - } - } + return { + "depth": { + "values": [1, 2, 3], + "description": "What depth do you want?" + } + } """ return { @@ -72,8 +71,15 @@ def get_parameter_options(self) -> dict: } } - def get_default_submodule(self, option: str) -> \ - Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + def get_default_submodule(self, option: str) \ + -> Union[LibraryQiskit, LibraryPennylane, PresetQiskitNoisyBackend, CustomQiskitNoisyBackend]: + """ + Returns the default submodule based on the given option. + + :param option: The submodule option to select + :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "LibraryQiskit": return LibraryQiskit() if option == "LibraryPennylane": @@ -87,7 +93,7 @@ def get_default_submodule(self, option: str) -> \ class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -98,14 +104,11 @@ class Config(TypedDict): def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: """ - Returns gate sequence of standard architecture - + Returns gate sequence of standard architecture. + :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config :return: Dictionary including the gate sequence of the Standard Circuit - :rtype: dict """ n_registers = input_data["n_registers"] n_qubits = input_data["n_qubits"] @@ -138,7 +141,7 @@ def generate_gate_sequence(self, input_data: dict, config: Config) -> dict: "store_dir_iter": input_data["store_dir_iter"], "train_size": input_data["train_size"], "dataset_name": input_data["dataset_name"], - "binary_train":input_data["binary_train"] + "binary_train": input_data["binary_train"] } return output_dict diff --git a/src/modules/circuits/__init__.py b/src/modules/applications/qml/generative_modeling/circuits/__init__.py similarity index 100% rename from src/modules/circuits/__init__.py rename to src/modules/applications/qml/generative_modeling/circuits/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/data/MG_2D.npy b/src/modules/applications/qml/generative_modeling/data/MG_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/MG_2D.npy rename to src/modules/applications/qml/generative_modeling/data/MG_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/O_2D.npy b/src/modules/applications/qml/generative_modeling/data/O_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/O_2D.npy rename to src/modules/applications/qml/generative_modeling/data/O_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/Stocks_2D.npy b/src/modules/applications/qml/generative_modeling/data/Stocks_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/Stocks_2D.npy rename to src/modules/applications/qml/generative_modeling/data/Stocks_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/X_2D.npy b/src/modules/applications/qml/generative_modeling/data/X_2D.npy similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/X_2D.npy rename to src/modules/applications/qml/generative_modeling/data/X_2D.npy diff --git a/src/modules/applications/QML/generative_modeling/data/__init__.py b/src/modules/applications/qml/generative_modeling/data/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/__init__.py rename to src/modules/applications/qml/generative_modeling/data/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py b/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py similarity index 69% rename from src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py index a206ae9c..e463b750 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/ContinuousData.py +++ b/src/modules/applications/qml/generative_modeling/data/data_handler/ContinuousData.py @@ -19,13 +19,12 @@ import pkg_resources from utils import start_time_measurement, end_time_measurement +from modules.applications.qml.generative_modeling.transformations.MinMax import MinMax +from modules.applications.qml.generative_modeling.transformations.PIT import PIT +from modules.applications.qml.generative_modeling.data.data_handler.DataHandlerGenerative import DataHandlerGenerative -from modules.applications.QML.generative_modeling.transformations.MinMax import MinMax -from modules.applications.QML.generative_modeling.transformations.PIT import PIT -from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import * - -class ContinuousData(DataHandler): +class ContinuousData(DataHandlerGenerative): """ A data handler for continuous datasets. This class loads a dataset from a specified path and provides methods for data transformation and evaluation. @@ -34,8 +33,8 @@ class ContinuousData(DataHandler): def __init__(self): """ - The continuous data class loads a dataset from the path - src/modules/applications/QML/generative_modeling/data + The continuous data class loads a dataset from the path + src/modules/applications/qml/generative_modeling/data """ super().__init__("") self.submodule_options = ["PIT", "MinMax"] @@ -48,17 +47,11 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_default_submodule(self, option: str) -> Union[PIT, MinMax]: if option == "MinMax": @@ -71,24 +64,22 @@ def get_default_submodule(self, option: str) -> Union[PIT, MinMax]: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application - - :return: - - .. code-block:: python + Returns the configurable settings for this application. - return { - "data_set": { - "values": ["X_2D", "O_2D", "MG_2D", "Stocks_2D"], - "description": "Which dataset do you want to use?" - }, + :return: Dictionary of parameter options + .. code-block:: python - "train_size": { - "values": [0.1, 0.3, 0.5, 0.7, 1.0], - "description": "What percentage of the dataset do you want to use for training?" - } - } + return { + "data_set": { + "values": ["X_2D", "O_2D", "MG_2D", "Stocks_2D"], + "description": "Which dataset do you want to use?" + }, + "train_size": { + "values": [0.1, 0.3, 0.5, 0.7, 1.0], + "description": "What percentage of the dataset do you want to use for training?" + } + } """ return { "data_set": { @@ -105,7 +96,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -117,22 +108,20 @@ class Config(TypedDict): train_size: float def data_load(self, gen_mod: dict, config: Config) -> dict: - """ The chosen dataset is loaded and split into a training set. :param gen_mod: Dictionary with collected information of the previous modules - :type gen_mod: dict :param config: Config specifying the parameters of the data handler - :type config: Config - :return: dictionary including the mapped problem - :rtype: dict + :return: Dictionary including the mapped problem """ self.dataset_name = config["data_set"] self.n_qubits = gen_mod["n_qubits"] - filename = pkg_resources.resource_filename('modules.applications.QML.generative_modeling.data', - f"{self.dataset_name}.npy") + filename = pkg_resources.resource_filename( + 'modules.applications.qml.generative_modeling.data', + f"{self.dataset_name}.npy" + ) self.dataset = np.load(filename) application_config = { @@ -146,13 +135,11 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: def evaluate(self, solution: dict) -> tuple[float, float]: """ - Calculate KL in original space. + Calculates KL in original space. :param solution: a dictionary containing the solution data, including histogram_generated_original and histogram_train_original - :type solution: dict :return: Kullback-Leibler (KL) divergence for the generated samples and the time it took to calculate it - :rtype: tuple[float, float] """ start = start_time_measurement() @@ -172,13 +159,10 @@ def evaluate(self, solution: dict) -> tuple[float, float]: def kl_divergence(self, target: np.ndarray, q: np.ndarray) -> float: """ - Function to calculate KL divergence + Function to calculate KL divergence. :param target: Probability mass function of the target distribution - :type target: np.ndarray :param q: Probability mass function generated by the quantum circuit - :type q: np.ndarray :return: Kullback-Leibler divergence - :rtype: float """ return np.sum(target * np.log(target / q)) diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py b/src/modules/applications/qml/generative_modeling/data/data_handler/DataHandlerGenerative.py similarity index 60% rename from src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/DataHandlerGenerative.py index 22999cd3..ade83a61 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DataHandler.py +++ b/src/modules/applications/qml/generative_modeling/data/data_handler/DataHandlerGenerative.py @@ -14,25 +14,25 @@ import pickle import os +from abc import ABC from qiskit import qpy import numpy as np -import pandas as pd -from tensorboard.backend.event_processing.event_accumulator import EventAccumulator -from modules.Core import * +from modules.Core import Core +from modules.applications.qml.DataHandler import DataHandler from utils import start_time_measurement, end_time_measurement -class DataHandler(Core, ABC): +class DataHandlerGenerative(Core, DataHandler, ABC): """ The task of the DataHandler module is to translate the application’s data - and problem specification into preproccesed format. + and problem specification into preprocessed format. """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. """ super().__init__() self.dataset_name = name @@ -41,38 +41,24 @@ def __init__(self, name): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "pandas", - "version": "2.2.2" - }, - { - "name": "tensorboard", - "version": "2.17.0" - } + {"name": "numpy", "version": "1.26.4"}, + {"name": "pandas", "version": "2.2.2"}, + {"name": "tensorboard", "version": "2.17.0"} ] - def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: + def preprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[any, float]: """ In this module, the preprocessing step is transforming the data to the correct target format. - :param input_data: collected information of the benchmarking process - :type input_data: dict - :param config: config specifying the parameters of the training - :type config: dict - :param kwargs: optional additional settings - :type kwargs: dict - :return: tuple with transformed problem and the time it took to map it - :rtype: tuple[dict, float] + :param input_data: Collected information of the benchmarking process + :param config: Config specifying the parameters of the training + :param kwargs: Optional additional settings + :return: Tuple with transformed problem and the time it took to map it """ start = start_time_measurement() output = self.data_load(input_data, config) @@ -86,14 +72,10 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f """ In this module, the postprocessing step is transforming the data to the correct target format. - :param input_data: any - :type input_data: dict - :param config: config specifying the parameters of the training - :type config: dict - :param kwargs: optional additional settings - :type kwargs: dict - :return: tuple with an output_dictionary and the time it took - :rtype: tuple[dict, float] + :param input_data: Original data + :param config: Config specifying the parameters of the training + :param kwargs: Optional additional settings + :return: Tuple with an output_dictionary and the time it took """ start = start_time_measurement() store_dir_iter = input_data["store_dir_iter"] @@ -107,22 +89,24 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f if self.generalization_mark is not None: self.metrics.add_metric_batch({"KL_best": evaluation["KL_best"]}) - metrics, _ = self.generalisation() + metrics, _ = self.generalization() - # Save generalisation metrics + # Save generalization metrics with open(f"{store_dir_iter}/record_gen_metrics_{kwargs['rep_count']}.pkl", 'wb') as f: pickle.dump(metrics, f) - self.metrics.add_metric_batch({"generalisation_metrics": metrics}) + self.metrics.add_metric_batch({"generalization_metrics": metrics}) else: self.metrics.add_metric_batch({"KL_best": evaluation}) # Save metrics per iteration if "inference" not in input_data.keys(): - DataHandler.tb_to_pd(logdir=store_dir_iter, rep=str(kwargs['rep_count'])) + self.tb_to_pd(logdir=store_dir_iter, rep=str(kwargs['rep_count'])) self.metrics.add_metric_batch( - {"metrics_pandas": os.path.relpath(f"{store_dir_iter}/data.pkl", current_directory)}) + {"metrics_pandas": os.path.relpath(f"{store_dir_iter}/data.pkl", current_directory)} + ) + if self.generalization_mark is not None: np.save(f"{store_dir_iter}/histogram_generated.npy", evaluation["histogram_generated"]) else: @@ -135,17 +119,20 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f histogram_generated = input_data["histogram_generated"] np.save(f"{store_dir_iter}/histogram_generated.npy", histogram_generated) self.metrics.add_metric_batch({"histogram_generated": os.path.relpath( - f"{store_dir_iter}/histogram_generated.npy_{kwargs['rep_count']}.npy", current_directory)}) + f"{store_dir_iter}/histogram_generated.npy_{kwargs['rep_count']}.npy", current_directory)} + ) # Save histogram generated dataset np.save(f"{store_dir_iter}/histogram_train.npy", input_data.pop("histogram_train")) self.metrics.add_metric_batch({"histogram_train": os.path.relpath( - f"{store_dir_iter}/histogram_train.npy_{kwargs['rep_count']}.npy", current_directory)}) + f"{store_dir_iter}/histogram_train.npy_{kwargs['rep_count']}.npy", current_directory)} + ) # Save best parameters np.save(f"{store_dir_iter}/best_parameters_{kwargs['rep_count']}.npy", input_data.pop("best_parameter")) self.metrics.add_metric_batch({"best_parameter": os.path.relpath( - f"{store_dir_iter}/best_parameters_{kwargs['rep_count']}.npy", current_directory)}) + f"{store_dir_iter}/best_parameters_{kwargs['rep_count']}.npy", current_directory)} + ) # Save training results input_data.pop("circuit_transpiled") @@ -167,66 +154,13 @@ def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, f return input_data, end_time_measurement(start) - @abstractmethod - def data_load(self, gen_mod: dict, config: dict) -> tuple[any, float]: - """ - Helps to ensure that the model can effectively learn the underlying - patterns and structure of the data, and produce high-quality outputs. - - :param gen_mod: dictionary with collected information of the previous modules - :type gen_mod: dict - :param config: config specifying the parameters of the data handler - :type config: dict - :return: mapped problem and the time it took to create the mapping - :rtype: tuple[any, float] - """ - pass - - def generalisation(self) -> tuple[dict, float]: + def generalization(self) -> tuple[dict, float]: """ - Compute generalisation metrics + Computes generalization metrics. :return: Evaluation and the time it took to create it - :rtype: tuple[dict, float] - """ # Compute your metrics here metrics = {} # Replace with actual metric calculations time_taken = 0.0 # Replace with actual time calculation return metrics, time_taken - - @abstractmethod - def evaluate(self, solution: any) -> tuple[any, float]: - """ - Compute the best loss values. - - :param solution: solution data - :type solution: any - :return: evaluation data and the time it took to create it - :rtype: tuple[any, float] - - """ - return None, 0.0 - - @staticmethod - def tb_to_pd(logdir: str, rep: str) -> None: - """ - Converts TensorBoard event files in the specified log directory - into a pandas DataFrame and saves it as a pickle file. - - :param logdir: path to the log directory containing TensorBoard event files - :type logdir: str - :param rep: repetition counter - :type rep: str - """ - event_acc = EventAccumulator(logdir) - event_acc.Reload() - tags = event_acc.Tags() - data = [] - tag_data = {} - for tag in tags['scalars']: - data = event_acc.Scalars(tag) - tag_values = [d.value for d in data] - tag_data[tag] = tag_values - data = pd.DataFrame(tag_data, index=[d.step for d in data]) - data.to_pickle(f"{logdir}/data_{rep}.pkl") diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py b/src/modules/applications/qml/generative_modeling/data/data_handler/DiscreteData.py similarity index 75% rename from src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/DiscreteData.py index 5df8389c..cf7fdebf 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/DiscreteData.py +++ b/src/modules/applications/qml/generative_modeling/data/data_handler/DiscreteData.py @@ -12,23 +12,24 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TypedDict import itertools import logging from pprint import pformat +from typing import TypedDict import numpy as np -from modules.circuits.CircuitCardinality import CircuitCardinality -from modules.applications.QML.generative_modeling.data.data_handler.DataHandler import * -from modules.applications.QML.generative_modeling.data.data_handler.MetricsGeneralization import MetricsGeneralization +from modules.applications.qml.generative_modeling.circuits.CircuitCardinality import CircuitCardinality +from modules.applications.qml.generative_modeling.data.data_handler.DataHandlerGenerative import DataHandlerGenerative +from modules.applications.qml.generative_modeling.metrics.MetricsGeneralization import MetricsGeneralization +from utils import start_time_measurement, end_time_measurement -class DiscreteData(DataHandler): +class DiscreteData(DataHandlerGenerative): """ A data handler for discrete datasets with cardinality constraints. This class creates a dataset with a cardinality constraint and provides - methods for generalisation metrics computing and evaluation. + methods for generalization metrics computing and evaluation. """ def __init__(self): @@ -47,40 +48,37 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_default_submodule(self, option: str) -> CircuitCardinality: + """ + Get the default submodule based on the given option. + :param option: Submodule option + :return: Corresponding submodule + """ if option == "CircuitCardinality": return CircuitCardinality() else: - raise NotImplementedError( - f"Circuit Option {option} not implemented") + raise NotImplementedError(f"Circuit Option {option} not implemented") def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application - - :return: - .. code-block:: python + Returns the configurable settings for this application. - return { - "train_size": { - "values": [0.1, 0.3, 0.5, 0.7, 0.9, 1.0], - "description": "What percentage of the dataset do you want to use for training?" - } - } + :return: A dictionary of parameter options + .. code-block:: python + return { + "train_size": { + "values": [0.1, 0.3, 0.5, 0.7, 0.9, 1.0], + "description": "What percentage of the dataset do you want to use for training?" + } + } """ return { "train_size": { @@ -91,7 +89,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -106,11 +104,8 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: The cardinality constrained dataset is created and split into a training set. :param gen_mod: Dictionary with collected information of the previous modules - :type gen_mod: dict :param config: Config specifying the parameters of the data handler - :type config: Config - :return: dictionary including the mapped problem - :rtype: dict + :return: Dictionary including the mapped problem """ dataset_name = "Cardinality_Constraint" self.n_qubits = gen_mod["n_qubits"] @@ -152,7 +147,8 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: "n_registers": 2, "histogram_solution": self.histogram_solution, "histogram_train": self.histogram_train, - "store_dir_iter": gen_mod["store_dir_iter"]} + "store_dir_iter": gen_mod["store_dir_iter"] + } if self.train_size != 1: self.generalization_metrics = MetricsGeneralization( @@ -165,12 +161,11 @@ def data_load(self, gen_mod: dict, config: Config) -> dict: return application_config - def generalisation(self) -> tuple[dict, float]: + def generalization(self) -> tuple[dict, float]: """ Calculate generalization metrics for the generated. - :return: a tuple containing a dictionary of generalization metrics and the execution time - :rtype: tuple[dict, float] + :return: Tuple containing a dictionary of generalization metrics and the execution time """ start = start_time_measurement() results = self.generalization_metrics.get_metrics(self.samples) @@ -184,11 +179,9 @@ def evaluate(self, solution: dict) -> tuple[dict, float]: Evaluates a given solution and calculates the histogram of generated samples and the minimum KL divergence value. - :param solution: dictionary containing the solution data, including generated samples and KL divergence values. - :type solution: dict - :return: a tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence - value, and the time it took to evaluate the solution. - :rtype: tuple[dict, float] + :param solution: Dictionary containing the solution data, including generated samples and KL divergence values + :return: Tuple containing a dictionary with the histogram of generated samples and the minimum KL divergence + value, and the time it took to evaluate the solution """ start = start_time_measurement() self.samples = solution["best_sample"] diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/__init__.py b/src/modules/applications/qml/generative_modeling/data/data_handler/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/data/data_handler/__init__.py rename to src/modules/applications/qml/generative_modeling/data/data_handler/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py b/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py similarity index 73% rename from src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py rename to src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py index b5561e0b..0b5e225f 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/CustomQiskitNoisyBackend.py +++ b/src/modules/applications/qml/generative_modeling/mappings/CustomQiskitNoisyBackend.py @@ -11,9 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from typing import Union import logging from time import perf_counter + import numpy as np from qiskit.circuit import QuantumCircuit, Parameter @@ -24,108 +26,98 @@ from qiskit_aer import Aer, AerSimulator, noise from qiskit_aer.noise import NoiseModel -from modules.training.QCBM import QCBM -from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.training.QCBM import QCBM +from modules.applications.qml.generative_modeling.training.Inference import Inference +from modules.applications.qml.generative_modeling.mappings.LibraryGenerative import LibraryGenerative logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) + def split_string(s): return s.split(' ', 1)[0] -class CustomQiskitNoisyBackend(Library): +class CustomQiskitNoisyBackend(LibraryGenerative): """ - This module maps a library-agnostic gate sequence to a qiskit circuit and creates an artificial noise model + This module maps a library-agnostic gate sequence to a qiskit circuit and creates an artificial noise model. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("NoisyQiskit") self.submodule_options = ["QCBM", "Inference"] - - circuit_transpiled = None + self.circuit_transpiled = None @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "qiskit", "version": "1.1.0"}, + {"name": "qiskit_aer", "version": "0.15.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: - .. code-block:: python - - return { - "backend": { - "values": ["aer_simulator_gpu", "aer_simulator_cpu"], - "description": "Which backend do you want to use? " - "In the NoisyQiskit module only aer_simulators can be used." - }, - - "simulation_method": { - "values": ["automatic", "statevector", "density_matrix", "cpu_mps"], # TODO New names! - "description": "What simulation method should be used?" - }, - - "n_shots": { - "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model?" - # (If 'statevector' was selected as simulation_method, 'n_shots' is only relevant for - # studying generalization)" - }, - - "transpile_optimization_level": { - "values": [1, 2, 3, 0], - "description": "Switch between different optimization levels in the Qiskit transpile" - "routine. 1: light optimization, 2: heavy optimization, 3: even heavier optimization," - "0: no optimization. Level 1 recommended as standard option." - }, - - "noise_configuration": { - "values": ['Custom configurations', 'No noise'], - "description": "What noise configuration do you want to use?" - }, - "custom_readout_error": { - "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], - "description": "Add a custom readout error." - }, - "two_qubit_depolarizing_errors": { - "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], - "description": "Add a custom 2-qubit gate depolarizing error." - }, - "one_qubit_depolarizing_errors": { - "values": [0, 0.0001, 0.0005, 0.001, 0.005, 0.007, 0.01, 0.02], - "description": "Add a 1-qubit gate depolarizing error." - }, - "qubit_layout": { - # "values": [None, 'linear', 'circle', 'fully_connected', 'ibm_brisbane'], - "values": [None, 'linear', 'circle', 'fully_connected'], - "description": "How should the qubits be connected in the simulated chip: coupling_map " - } - } + :return: Dictionary with configurable settings + .. code-block:: python + + return { + "backend": { + "values": ["aer_simulator_gpu", "aer_simulator_cpu"], + "description": "Which backend do you want to use? " + "In the NoisyQiskit module only aer_simulators can be used." + }, + + "simulation_method": { + "values": ["automatic", "statevector", "density_matrix", "cpu_mps"], # TODO New names! + "description": "What simulation method should be used?" + }, + + "n_shots": { + "values": [100, 1000, 10000, 1000000], + "description": "How many shots do you want use for estimating the PMF of the model?" + # (If 'statevector' was selected as simulation_method, 'n_shots' is only relevant for + # studying generalization)" + }, + + "transpile_optimization_level": { + "values": [1, 2, 3, 0], + "description": "Switch between different optimization levels in the Qiskit transpile" + "routine. 1: light optimization, 2: heavy optimization, 3: even heavier optimization," + "0: no optimization. Level 1 recommended as standard option." + }, + + "noise_configuration": { + "values": ['Custom configurations', 'No noise'], + "description": "What noise configuration do you want to use?" + }, + "custom_readout_error": { + "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], + "description": "Add a custom readout error." + }, + "two_qubit_depolarizing_errors": { + "values": [0, 0.005, 0.01, 0.02, 0.05, 0.07, 0.1, 0.2], + "description": "Add a custom 2-qubit gate depolarizing error." + }, + "one_qubit_depolarizing_errors": { + "values": [0, 0.0001, 0.0005, 0.001, 0.005, 0.007, 0.01, 0.02], + "description": "Add a 1-qubit gate depolarizing error." + }, + "qubit_layout": { + # "values": [None, 'linear', 'circle', 'fully_connected', 'ibm_brisbane'], + "values": [None, 'linear', 'circle', 'fully_connected'], + "description": "How should the qubits be connected in the simulated chip: coupling_map " + } + } """ return { @@ -178,7 +170,13 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: + """ + Returns the default submodule based on the provided option. + :param option: The option to select the submodule + :return: The selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() elif option == "Inference": @@ -192,14 +190,13 @@ def sequence_to_circuit(self, input_data: dict) -> dict: to its Qiskit implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by it Qiskit implementation - :rtype: dict """ n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] circuit = QuantumCircuit(n_qubits, n_qubits) param_counter = 0 + for gate, wires in gate_sequence: if gate == "Hadamard": circuit.h(wires[0]) @@ -243,55 +240,42 @@ def sequence_to_circuit(self, input_data: dict) -> dict: input_data["circuit"] = circuit input_data.pop("gate_sequence") - logging.info(param_counter) input_data["n_params"] = len(circuit.parameters) + return input_data @staticmethod def select_backend(config: str, n_qubits: int) -> Backend: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ - if config == "aer_simulator_gpu": - # from qiskit import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") - elif config == "aer_simulator_cpu": - # from qiskit import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="CPU") - else: raise NotImplementedError(f"Device Configuration {config} not implemented") return backend - def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pylint: disable=W0221 - config: str, config_dict: dict) -> tuple[any, any]: + def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, config: str, config_dict: dict # pylint: disable=W0221 + ) -> tuple[any, any]: """ This method combines the qiskit circuit implementation and the selected backend and returns a function, that will be called during training. :param circuit: Qiskit implementation of the quantum circuit - :type circuit: qiskit.circuit.QuantumCircuit :param backend: Configured qiskit backend - :type backend: qiskit.providers.Backend :param config: Name of a backend - :type config: str :param config_dict: Contains information about config - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits @@ -299,10 +283,10 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli backend = self.decompile_noisy_config(config_dict, n_qubits) logging.info(f'Backend in Use: {backend=}') optimization_level = self.get_transpile_routine(config_dict['transpile_optimization_level']) - seed_transp = 42 # Remove seed if wanted + seed_transp = 42 # Remove seed if wanted logging.info(f'Using {optimization_level=} with seed: {seed_transp}') coupling_map = self.get_coupling_map(config_dict, n_qubits) - print(f"Generated coupling map: {coupling_map}") + # Create a manual layout if needed (you can customize the layout based on your use case) manual_layout = Layout({circuit.qubits[i]: i for i in range(n_qubits)}) @@ -315,7 +299,7 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli # Now transpile the circuit after running the pass manager circuit_transpiled = transpile(circuit_passed, backend=backend, optimization_level=optimization_level, - seed_transpiler=seed_transp,coupling_map=coupling_map) + seed_transpiler=seed_transp, coupling_map=coupling_map) logging.info(f'Circuit operations before transpilation: {circuit.count_ops()}') logging.info(f'Circuit operations after transpilation: {circuit_transpiled.count_ops()}') logging.info(perf_counter() - start) @@ -357,18 +341,18 @@ def decompile_noisy_config(self, config_dict: dict, num_qubits: int) -> Backend: to the 'aer_simulator' backend. It returns the configured backend. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ backend_config = config_dict['backend'] device = 'GPU' if 'gpu' in backend_config else 'CPU' - simulation_method, device = self.get_simulation_method_and_device(device, config_dict['simulation_method']) + simulation_method, device = self.get_simulation_method_and_device( + device, config_dict['simulation_method'] + ) backend = self.get_custom_config(config_dict, num_qubits) \ - if config_dict['noise_configuration'] == "Custom configurations" else Aer.get_backend("aer_simulator") + if config_dict['noise_configuration'] == "Custom configurations" \ + else Aer.get_backend("aer_simulator") backend.set_options(device=device, method=simulation_method) self.log_backend_options(backend) @@ -379,14 +363,9 @@ def get_simulation_method_and_device(self, device: str, simulation_config: str) """ This method specifies the simulation methode and processing unit. - :param device: Contains information about processing unit - :type device: str - :param simulation_config: Contains information about qiskit simulation method - :type simulation_config: str - - :return: simulation_config: Contains information about qiskit simulation method - device: Contains information about processing unit - :rtype: tuple[str, str] + :param device: Contains information about processing unit + :param simulation_config: Contains information about qiskit simulation method + :return: Tuple containing the simulation method and device """ simulation_method = { "statevector": "statevector", @@ -404,11 +383,8 @@ def get_transpile_routine(self, transpile_config: int) -> int: This method returns the transpile routine based on the provided configuration. :param transpile_config: Configuration for transpile routine - :type transpile_config: int :return: Transpile routine level - :rtype: int """ - return transpile_config if transpile_config in [0, 1, 2, 3] else 1 def get_custom_config(self, config_dict: dict, num_qubits: int) -> Backend: @@ -416,11 +392,8 @@ def get_custom_config(self, config_dict: dict, num_qubits: int) -> Backend: This method creates a custom backend configuration based on the provided configuration dictionary. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Custom configured qiskit backend - :rtype: qiskit.providers.Backend """ noise_model = self.build_noise_model(config_dict) coupling_map = self.get_coupling_map(config_dict, num_qubits) @@ -429,6 +402,7 @@ def get_custom_config(self, config_dict: dict, num_qubits: int) -> Backend: backend = AerSimulator(noise_model=noise_model, coupling_map=coupling_map) else: backend = AerSimulator(noise_model=noise_model) + return backend def build_noise_model(self, config_dict: dict) -> NoiseModel: @@ -436,15 +410,15 @@ def build_noise_model(self, config_dict: dict) -> NoiseModel: This method builds a noise model based on the provided configuration dictionary. :param config_dict: Contains information about config - :type config_dict: dict :return: Constructed noise model - :rtype: NoiseModel """ noise_model = noise.NoiseModel() + if config_dict['custom_readout_error']: readout_error = config_dict['custom_readout_error'] noise_model.add_all_qubit_readout_error( - [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]]) + [[1 - readout_error, readout_error], [readout_error, 1 - readout_error]] + ) self.add_quantum_errors(noise_model, config_dict) return noise_model @@ -455,9 +429,7 @@ def add_quantum_errors(self, noise_model: NoiseModel, config_dict: dict) -> None configuration dictionary. :param noise_model: Noise model to which quantum errors are added - :type noise_model: NoiseModel :param config_dict: Contains information about config - :type config_dict: dict """ if config_dict['two_qubit_depolarizing_errors'] is not None: two_qubit_error = noise.depolarizing_error(config_dict['two_qubit_depolarizing_errors'], 2) @@ -474,25 +446,17 @@ def get_coupling_map(self, config_dict: dict, num_qubits: int) -> CouplingMap: This method returns the coupling map based on the provided configuration dictionary and number of qubits. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Coupling map - :rtype: CouplingMap """ layout = config_dict['qubit_layout'] + if layout == 'linear': 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) - # IBM layout will be added with release 2.1 - # elif layout == "ibm_brisbane": - # service = QiskitRuntimeService() - # backend = service.backend("ibm_brisbane") - # logging.info(f'Loaded with IBMQ Account {backend.name}, {backend.version}, {backend.num_qubits}') - # return backend.coupling_map elif layout is None: logging.info('No coupling map specified, using default.') return None diff --git a/src/modules/applications/QML/generative_modeling/mappings/Library.py b/src/modules/applications/qml/generative_modeling/mappings/LibraryGenerative.py similarity index 54% rename from src/modules/applications/QML/generative_modeling/mappings/Library.py rename to src/modules/applications/qml/generative_modeling/mappings/LibraryGenerative.py index cec62f7a..d429f758 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/Library.py +++ b/src/modules/applications/qml/generative_modeling/mappings/LibraryGenerative.py @@ -12,30 +12,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod +from abc import ABC import logging from typing import TypedDict from utils import start_time_measurement, end_time_measurement - from modules.Core import Core +from modules.applications.qml.Model import Model -class Library(Core, ABC): +class LibraryGenerative(Core, Model, ABC): """ - This class is an abstract base class for mapping a library-agnostic gate sequence to a library such as Qiskit + This class is an abstract base class for mapping a library-agnostic gate sequence to a library such as Qiskit. + It provides no concrete implementations of abstract methods and is intended to be extended by specific libraries. """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. + + :param name: Name of the model """ self.name = name super().__init__() class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -51,14 +54,10 @@ def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, Base class for mapping the gate sequence to a library such as Qiskit. :param input_data: Collection of information from the previous modules - :type input_data: dict :param config: Config specifying the number of qubits of the circuit - :type config: Config - :param kwargs: optional keyword arguments - :type kwargs: dict - :return: tuple including dictionary with the function to execute the quantum circuit on a simulator or quantum + :param kwargs: Optional keyword arguments + :return: Tuple including dictionary with the function to execute the quantum circuit on a simulator or quantum hardware and the computation time of the function - :rtype: tuple[dict, float] """ start = start_time_measurement() @@ -68,7 +67,8 @@ def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, output["circuit"], backend, config["backend"], - config) + config + ) output["backend"] = config["backend"] output["n_shots"] = config["n_shots"] logging.info("Library created") @@ -78,58 +78,13 @@ def preprocess(self, input_data: dict, config: Config, **kwargs) -> tuple[dict, def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - This method corresponds to the identity and passes the information of the subsequent module + This method corresponds to the identity and passes the information of the subsequent module back to the preceding module in the benchmarking process. - :param input_data: Collected information of the benchmarking process - :type input_data: dict + :param input_data: Collected information of the benchmarking procesS :param config: Config specifying the number of qubits of the circuit - :type config: Config - :param kwargs: optional keyword arguments - :type kwargs: dict - :return: tuple with input dictionary and the computation time of the function - :rtype: tuple[dict, float] + :param kwargs: Optional keyword arguments + :return: Tuple with input dictionary and the computation time of the function """ start = start_time_measurement() return input_data, end_time_measurement(start) - - @abstractmethod - def sequence_to_circuit(self, input_data): - pass - - @staticmethod - @abstractmethod - def get_execute_circuit(circuit: any, backend: any, config: str, config_dict: dict) -> ( - tuple)[any, any]: - """ - This method combines the circuit implementation and the selected backend and returns a function that will be - called during training. - - :param circuit: Implementation of the quantum circuit - :type circuit: any - :param backend: Configured backend - :type backend: any - :param config: Name of the PennyLane device - :type config: str - :param config_dict: Dictionary including the number of shots - :type config_dict: dict - :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the - transpiled circuit - :rtype: tuple[any, any] - """ - pass - - @staticmethod - @abstractmethod - def select_backend(config: str, n_qubits: int) -> any: - """ - This method configures the backend - - :param config: Name of a backend - :type config: str - :param n_qubits: Number of qubits - :type n_qubits: int - :return: Configured backend - :rtype: any - """ - return diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py b/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py similarity index 74% rename from src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py rename to src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py index 5e6b1690..c947774f 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryPennylane.py +++ b/src/modules/applications/qml/generative_modeling/mappings/LibraryPennylane.py @@ -19,15 +19,15 @@ from jax import numpy as jnp import jax -jax.config.update("jax_enable_x64", True) +from modules.applications.qml.generative_modeling.mappings.LibraryGenerative import LibraryGenerative +from modules.applications.qml.generative_modeling.training.Inference import Inference +from modules.applications.qml.generative_modeling.training.QGAN import QGAN +from modules.applications.qml.generative_modeling.training.QCBM import QCBM -from modules.training.QCBM import QCBM -from modules.training.QGAN import QGAN -from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +jax.config.update("jax_enable_x64", True) -class LibraryPennylane(Library): +class LibraryPennylane(LibraryGenerative): def __init__(self): super().__init__("LibraryPennylane") @@ -36,60 +36,42 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "jax", - "version": "0.4.30" - }, - { - "name": "jaxlib", - "version": "0.4.30" - } + {"name": "pennylane", "version": "0.37.0"}, + {"name": "pennylane-lightning", "version": "0.38.0"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "jax", "version": "0.4.30"}, + {"name": "jaxlib", "version": "0.4.30"} ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for the PennyLane Library. - :return: - .. code-block:: python - - return { - "backend": { - "values": ["default.qubit", "default.qubit.jax", "lightning.qubit", "lightning.gpu"], - "description": "Which backend do you want to use?" - }, + :return: Dictionary with configurable settings. + .. code-block:: python - "n_shots": { - "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model?" - } - } + return { + "backend": { + "values": ["default.qubit", "default.qubit.jax", "lightning.qubit", "lightning.gpu"], + "description": "Which backend do you want to use?" + }, + "n_shots": { + "values": [100, 1000, 10000, 1000000], + "description": "How many shots do you want use for estimating the PMF of the model?" + } + } """ return { "backend": { "values": ["default.qubit", "default.qubit.jax", "lightning.qubit", "lightning.gpu"], "description": "Which device do you want to use?" }, - "n_shots": { "values": [100, 1000, 10000, 1000000], "description": "How many shots do you want use for estimating the PMF of the model?" @@ -97,6 +79,13 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: + """ + Returns the default submodule based on the provided option. + + :param option: The option to select the submodule + :return: The selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() @@ -113,13 +102,13 @@ def sequence_to_circuit(self, input_data: dict) -> dict: to its PennyLane implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by its PennyLane implementation - :rtype: dict """ gate_sequence = input_data["gate_sequence"] n_qubits = input_data["n_qubits"] - num_parameters = sum(1 for gate, _ in gate_sequence if gate in ["RZ", "RX", "RY", "RXX", "RYY", "RZZ", "CRY"]) + num_parameters = sum( + 1 for gate, _ in gate_sequence if gate in ["RZ", "RX", "RY", "RXX", "RYY", "RZZ", "CRY"] + ) def create_circuit(params): param_counter = 0 @@ -167,50 +156,38 @@ def create_circuit(params): @staticmethod def select_backend(config: str, n_qubits: int) -> any: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured backend - :rtype: any """ if config == "lightning.gpu": backend = qml.device(name="lightning.gpu", wires=n_qubits) - elif config == "lightning.qubit": backend = qml.device(name="lightning.qubit", wires=n_qubits) - elif config == "default.qubit": backend = qml.device(name="default.qubit", wires=n_qubits) - elif config == "default.qubit.jax": backend = qml.device(name="default.qubit.jax", wires=n_qubits) - else: raise NotImplementedError(f"Device Configuration {config} not implemented") return backend @staticmethod - def get_execute_circuit(circuit: callable, backend: qml.device, config: str, config_dict: dict) -> tuple[any, any]: + def get_execute_circuit(circuit: callable, backend: qml.device, config: str, config_dict: dict) \ + -> tuple[any, any]: """ This method combines the PennyLane circuit implementation and the selected backend and returns a function that will be called during training. :param circuit: PennyLane implementation of the quantum circuit - :type circuit: callable - :param backend: Configured PennyLane device - :type backend: pennylane.device - :param config: Name of the PennyLane device - :type config: str + :param backend: Configured qiskit backend + :param config: Name of a backend :param config_dict: Dictionary including the number of shots - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters twice - :rtype: tuple[any, any] """ - n_shots = config_dict["n_shots"] if config == "default.qubit.jax": diff --git a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py b/src/modules/applications/qml/generative_modeling/mappings/LibraryQiskit.py similarity index 81% rename from src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py rename to src/modules/applications/qml/generative_modeling/mappings/LibraryQiskit.py index 765a486f..dbe7f903 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/LibraryQiskit.py +++ b/src/modules/applications/qml/generative_modeling/mappings/LibraryQiskit.py @@ -12,30 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union import logging +from typing import Union +import numpy as np + from qiskit import QuantumCircuit, transpile from qiskit.circuit import Parameter from qiskit.providers import Backend from qiskit.quantum_info import Statevector -import numpy as np -from modules.training.QCBM import QCBM -from modules.training.QGAN import QGAN -from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.training.QCBM import QCBM +from modules.applications.qml.generative_modeling.training.QGAN import QGAN +from modules.applications.qml.generative_modeling.training.Inference import Inference +from modules.applications.qml.generative_modeling.mappings.LibraryGenerative import LibraryGenerative logging.getLogger("qiskit").setLevel(logging.WARNING) -class LibraryQiskit(Library): +class LibraryQiskit(LibraryGenerative): """ - This module maps a library-agnostic gate sequence to a qiskit circuit + This module maps a library-agnostic gate sequence to a qiskit circuit. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("LibraryQiskit") self.submodule_options = ["QCBM", "QGAN", "Inference"] @@ -43,30 +44,23 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "qiskit", "version": "1.1.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: - .. code-block:: python + :return: Dictionary with configurable settings + .. code-block:: python - return { + return { "backend": { "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", "cusvaer_simulator (only available in cuQuantum appliance)", "aer_simulator_gpu", @@ -89,18 +83,24 @@ def get_parameter_options(self) -> dict: "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", "cusvaer_simulator (only available in cuQuantum appliance)", "aer_simulator_gpu", "aer_simulator_cpu", "ionQ_Harmony", "Amazon_SV1", "ibm_brisbane IBM Quantum Platform"], - "description": "Which backend do you want to use? (aer_statevector_simulator\ - uses the measurement probability vector, the others are shot based)" + "description": "Which backend do you want to use? (aer_statevector_simulator uses the measurement " + "probability vector, the others are shot based)" }, "n_shots": { "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model?\ - (If the aer_statevector_simulator selected, only relevant for studying generalization)" + "description": "How many shots do you want use for estimating the PMF of the model? " + "(If the aer_statevector_simulator selected, only relevant for studying generalization)" } } def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: + """ + Returns the default submodule based on the provided option. + :param option: The option to select the submodule + :return: The selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() elif option == "QGAN": @@ -113,12 +113,10 @@ def get_default_submodule(self, option: str) -> Union[QCBM, QGAN, Inference]: def sequence_to_circuit(self, input_data: dict) -> dict: """ Maps the gate sequence, that specifies the architecture of a quantum circuit - to its Qiskit implementation. + to its Qiskit implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by its Qiskit implementation - :rtype: dict """ n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] @@ -126,47 +124,35 @@ def sequence_to_circuit(self, input_data: dict) -> dict: circuit = QuantumCircuit(n_qubits, n_qubits) param_counter = 0 for gate, wires in gate_sequence: - if gate == "Hadamard": circuit.h(wires[0]) - elif gate == "CNOT": circuit.cx(wires[0], wires[1]) - elif gate == "RZ": circuit.rz(Parameter(f"x_{param_counter:03d}"), wires[0]) param_counter += 1 - elif gate == "RX": circuit.rx(Parameter(f"x_{param_counter:03d}"), wires[0]) param_counter += 1 - elif gate == "RY": circuit.ry(Parameter(f"x_{param_counter:03d}"), wires[0]) param_counter += 1 - elif gate == "RXX": circuit.rxx(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "RYY": circuit.ryy(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "RZZ": circuit.rzz(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "CRY": circuit.cry(Parameter(f"x_{param_counter:03d}"), wires[0], wires[1]) param_counter += 1 - elif gate == "Barrier": circuit.barrier() - elif gate == "Measure": circuit.measure(wires[0], wires[0]) - else: raise NotImplementedError(f"Gate {gate} not implemented") @@ -179,14 +165,11 @@ def sequence_to_circuit(self, input_data: dict) -> dict: @staticmethod def select_backend(config: str, n_qubits: int) -> any: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured qiskit backend - :rtype: any """ if config == "cusvaer_simulator (only available in cuQuantum appliance)": import cusvaer # pylint: disable=C0415 @@ -205,25 +188,21 @@ def select_backend(config: str, n_qubits: int) -> any: from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="GPU") - elif config == "aer_simulator_cpu": from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend("aer_simulator") backend.set_options(device="CPU") - elif config == "aer_statevector_simulator_gpu": from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend('statevector_simulator') backend.set_options(device="GPU") - elif config == "aer_statevector_simulator_cpu": from qiskit_aer import Aer # pylint: disable=C0415 backend = Aer.get_backend('statevector_simulator') backend.set_options(device="CPU") - elif config == "ionQ_Harmony": - from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 - from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 + from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 + from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 device_wrapper = Ionq("ionQ", "arn:aws:braket:::device/qpu/ionq/ionQdevice") backend = AWSBraketBackend( device=device_wrapper.device, @@ -233,10 +212,9 @@ def select_backend(config: str, n_qubits: int) -> any: online_date=device_wrapper.device.properties.service.updatedAt, backend_version="2", ) - elif config == "Amazon_SV1": - from modules.devices.braket.SV1 import SV1 # pylint: disable=C0415 - from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 + from modules.devices.braket.SV1 import SV1 # pylint: disable=C0415 + from qiskit_braket_provider import AWSBraketBackend, AWSBraketProvider # pylint: disable=C0415 device_wrapper = SV1("SV1", "arn:aws:braket:::device/quantum-simulator/amazon/sv1") backend = AWSBraketBackend( device=device_wrapper.device, @@ -260,16 +238,11 @@ def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, that will be called during training. :param circuit: Qiskit implementation of the quantum circuit - :type circuit: qiskit.circuit.QuantumCircuit :param backend: Configured qiskit backend - :type backend: qiskit.providers.Backend :param config: Name of a backend - :type config: str :param config_dict: Contains information about config - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits @@ -279,7 +252,9 @@ def get_execute_circuit(circuit: QuantumCircuit, backend: Backend, config: str, circuit_transpiled.remove_final_measurements() def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] + all_circuits = [ + circuit_transpiled.assign_parameters(solution) for solution in solutions + ] pmfs = np.asarray([Statevector(c).probabilities() for c in all_circuits]) return pmfs, None @@ -287,13 +262,17 @@ def execute_circuit(solutions): import time as timetest # pylint: disable=C0415 def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] + all_circuits = [ + circuit_transpiled.assign_parameters(solution) for solution in solutions + ] jobs = backend.run(all_circuits, shots=n_shots) while not jobs.in_final_state(): logging.info("Waiting 10 seconds for task to finish") timetest.sleep(10) - samples_dictionary = [jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits] + samples_dictionary = [ + jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits + ] samples = [] for result in samples_dictionary: @@ -309,12 +288,20 @@ def execute_circuit(solutions): return pmfs, samples - elif config in ["cusvaer_simulator (only available in cuQuantum appliance)", "aer_simulator_cpu", - "aer_simulator_gpu"]: + elif config in [ + "cusvaer_simulator (only available in cuQuantum appliance)", + "aer_simulator_cpu", + "aer_simulator_gpu" + ]: + def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] + all_circuits = [ + circuit_transpiled.assign_parameters(solution) for solution in solutions + ] jobs = backend.run(all_circuits, shots=n_shots) - samples_dictionary = [jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits] + samples_dictionary = [ + jobs.result().get_counts(circuit).int_outcomes() for circuit in all_circuits + ] samples = [] for result in samples_dictionary: diff --git a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py b/src/modules/applications/qml/generative_modeling/mappings/PresetQiskitNoisyBackend.py similarity index 77% rename from src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py rename to src/modules/applications/qml/generative_modeling/mappings/PresetQiskitNoisyBackend.py index a6ef4f05..f4d02407 100644 --- a/src/modules/applications/QML/generative_modeling/mappings/PresetQiskitNoisyBackend.py +++ b/src/modules/applications/qml/generative_modeling/mappings/PresetQiskitNoisyBackend.py @@ -24,21 +24,21 @@ from qiskit_aer import Aer, AerSimulator from qiskit_aer.noise import NoiseModel -from modules.training.QCBM import QCBM -from modules.training.Inference import Inference -from modules.applications.QML.generative_modeling.mappings.Library import Library +from modules.applications.qml.generative_modeling.training.QCBM import QCBM +from modules.applications.qml.generative_modeling.training.Inference import Inference +from modules.applications.qml.generative_modeling.mappings.LibraryGenerative import LibraryGenerative logging.getLogger("NoisyQiskit").setLevel(logging.WARNING) -class PresetQiskitNoisyBackend(Library): +class PresetQiskitNoisyBackend(LibraryGenerative): """ This module maps a library-agnostic gate sequence to a qiskit circuit. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("PresetQiskitNoisyBackend") self.submodule_options = ["QCBM", "Inference"] @@ -48,63 +48,58 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit_ibm_runtime", - "version": "0.29.0" - }, - { - "name": "qiskit_aer", - "version": "0.15.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "qiskit", "version": "1.1.0"}, + {"name": "qiskit_ibm_runtime", "version": "0.29.0"}, + {"name": "qiskit_aer", "version": "0.15.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_parameter_options(self) -> dict: """ Returns the configurable settings for the Qiskit Library. - :return: - .. code-block:: python - - return { - "backend": { - "values": ["aer_statevector_simulator_gpu", "aer_statevector_simulator_cpu", - "cusvaer_simulator (only available in cuQuantum applicance)", - "aer_simulator_gpu", - "aer_simulator_cpu", "ionQ_Harmony", "Amazon_SV1"], - "description": "Which backend do you want to use? (aer_statevector_simulator - uses the measurement probability vector, the others are shot based)" - }, - - "n_shots": { - "values": [100, 1000, 10000, 1000000], - "description": "How many shots do you want use for estimating the PMF of the model? - (If the aer_statevector_simulator selected, - only relevant for studying generalization)" - } - } - + :return: Dictionary with configurable settings. + .. code-block:: python + + { + "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?" + }, + + "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?" + } + } """ - provider = FakeProviderForBackendV2() backends = provider.backends() value_list = [] value_list.append('No noise') - # value_list.append('ibm_osaka 127 Qubits') - # value_list.append('ibm_brisbane 127 Qubits') for backend in backends: if backend.num_qubits >= 6: value_list.append(f'{backend.name} V{backend.version} {backend.num_qubits} Qubits') @@ -124,7 +119,6 @@ def get_parameter_options(self) -> dict: "n_shots": { "values": [100, 1000, 10000, 1000000], "description": "How many shots do you want use for estimating the PMF of the model?" - # (If the aer_statevector_simulator selected, only relevant for studying generalization)" }, "transpile_optimization_level": { @@ -141,7 +135,13 @@ def get_parameter_options(self) -> dict: } def get_default_submodule(self, option: str) -> Union[QCBM, Inference]: + """ + Returns the default submodule based on the given option. + :param option: The submodule option to select + :return: Instance of the selected submodule + :raises NotImplemented: If the provided option is not implemented + """ if option == "QCBM": return QCBM() elif option == "Inference": @@ -155,10 +155,9 @@ def sequence_to_circuit(self, input_data: dict) -> dict: to its Qiskit implementation. :param input_data: Collected information of the benchmarking process - :type input_data: dict :return: Same dictionary but the gate sequence is replaced by it Qiskit implementation - :rtype: dict """ + # TODO: Identical to CustomQiskitNoisyBackend.sequence_to_circuit -> move to Library n_qubits = input_data["n_qubits"] gate_sequence = input_data["gate_sequence"] circuit = QuantumCircuit(n_qubits, n_qubits) @@ -213,26 +212,19 @@ def sequence_to_circuit(self, input_data: dict) -> dict: @staticmethod def select_backend(config: str, n_qubits: int) -> Backend: """ - This method configures the backend + This method configures the backend. :param config: Name of a backend - :type config: str :param n_qubits: Number of qubits - :type n_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ - + # TODO: Identical to CustomQiskitNoisyBackend.select_backend -> move to Library 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") @@ -245,17 +237,13 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli that will be called during training. :param circuit: Qiskit implementation of the quantum circuit - :type circuit: qiskit.circuit.QuantumCircuit :param backend: Configured qiskit backend - :type backend: qiskit.providers.Backend :param config: Name of a backend - :type config: str :param config_dict: Contains information about config - :type config_dict: dict :return: Tuple that contains a method that executes the quantum circuit for a given set of parameters and the transpiled circuit - :rtype: tuple[any, any] """ + # TODO: Identical to CustomQiskitNoisyBackend.get_execute_circuit -> move to Library n_shots = config_dict["n_shots"] n_qubits = circuit.num_qubits start = perf_counter() @@ -268,12 +256,11 @@ def get_execute_circuit(self, circuit: QuantumCircuit, backend: Backend, # pyli circuit_transpiled = transpile(circuit, backend=backend, optimization_level=optimization_level, seed_transpiler=seed_transp) logging.info(f'Circuit operations before transpilation: {circuit.count_ops()}') - logging.info(f'Circuit operations before transpilation: {circuit_transpiled.count_ops()}') + logging.info(f'Circuit operations after transpilation: {circuit_transpiled.count_ops()}') logging.info(perf_counter() - start) if config in ["aer_simulator_cpu", "aer_simulator_gpu"]: def execute_circuit(solutions): - all_circuits = [circuit_transpiled.assign_parameters(solution) for solution in solutions] jobs = backend.run(all_circuits, shots=n_shots) samples_dictionary = [jobs.result().get_counts(c).int_outcomes() for c in all_circuits] @@ -287,7 +274,6 @@ def execute_circuit(solutions): samples.append(target_iter) samples = np.asarray(samples) pmfs = samples / n_shots - return pmfs, samples else: @@ -308,11 +294,8 @@ def decompile_noisy_config(self, config_dict: dict, num_qubits: int) -> Backend: to the 'aer_simulator' backend. It returns the configured backend. :param config_dict: Contains information about config - :type config_dict: dict :param num_qubits: Number of qubits - :type num_qubits: int :return: Configured qiskit backend - :rtype: qiskit.providers.Backend """ backend_config = config_dict['backend'] device = 'GPU' if 'gpu' in backend_config else 'CPU' @@ -329,11 +312,8 @@ def select_backend_configuration(self, noise_configuration: str, num_qubits: int This method selects the backend configuration based on the provided noise configuration. :param noise_configuration: Noise configuration type - :type noise_configuration: str :param num_qubits: Number of qubits - :type num_qubits: int :return: Selected backend configuration - :rtype: qiskit.providers.Backend """ if "fake" in noise_configuration: return self.get_FakeBackend(noise_configuration, num_qubits) @@ -342,31 +322,16 @@ def select_backend_configuration(self, noise_configuration: str, num_qubits: int elif noise_configuration in ['ibm_brisbane 127 Qubits', 'ibm_osaka 127 Qubits']: logging.warning("Not yet implemented. Please check upcoming QUARK versions.") raise ValueError(f"Noise configuration '{noise_configuration}' not yet implemented.") - # return self.get_ibm_backend(noise_configuration) else: raise ValueError(f"Unknown noise configuration: {noise_configuration}") - # IBM backend will be added with release 2.1 - # def get_ibm_backend(self, backend_name): - # service = QiskitRuntimeService() - # backend_identifier = backend_name.replace(' 127 Qubits', '').lower() - # backend = service.backend(backend_identifier) - # noise_model = NoiseModel.from_backend(backend) - # logging.info(f'Loaded with IBMQ Account {backend.name}, {backend.version}, {backend.num_qubits}') - # simulator = AerSimulator.from_backend(backend) - # simulator.noise_model = noise_model - # return simulator - def configure_backend(self, backend: Backend, device: str, simulation_method: str) -> None: """ This method configures the backend with the specified device and simulation method. :param backend: Backend to be configured - :type backend: qiskit.providers.Backend :param device: Device type (CPU/GPU) - :type device: str :param simulation_method: Simulation method - :type simulation_method: str """ backend.set_options(device=device) backend.set_options(method=simulation_method) @@ -379,12 +344,9 @@ def get_simulation_method_and_device(self, device: str, simulation_config: str) """ This method determines the simulation method and device based on the provided configuration. - :param device: Contains information about processing unit - :type device: str - :param simulation_config: Contains information about qiskit simulation method - :type simulation_config: str + :param device: Contains information about processing unit + :param simulation_config: Contains information about qiskit simulation method :return: Tuple containing the simulation method and device - :rtype: tuple[str, str] """ simulation_methods = { "statevector": "statevector", @@ -401,9 +363,7 @@ def get_transpile_routine(self, transpile_config: int) -> int: This method returns the transpile routine based on the provided configuration. :param transpile_config: Configuration for transpile routine - :type transpile_config: int :return: Transpile routine level - :rtype: int """ return transpile_config if transpile_config in [0, 1, 2, 3] else 1 @@ -412,11 +372,8 @@ def get_FakeBackend(self, noise_configuration: str, num_qubits: int) -> Backend: This method returns a fake backend based on the provided noise configuration and number of qubits. :param noise_configuration: Noise configuration type - :type noise_configuration: str :param num_qubits: Number of qubits - :type num_qubits: int :return: Fake backend simulator - :rtype: qiskit.providers.Backend """ backend_name = str(self.split_string(noise_configuration)) provider = FakeProviderForBackendV2() @@ -432,8 +389,8 @@ def get_FakeBackend(self, noise_configuration: str, num_qubits: int) -> Backend: backend = filtered_backends[0] if num_qubits > backend.num_qubits: - logging.warning( - f'Requested number of qubits ({num_qubits}) exceeds the backend capacity. Using default aer_simulator.') + logging.warning(f'Requested number of qubits ({num_qubits}) exceeds the backend capacity. ' + f'Using default aer_simulator.') return Aer.get_backend("aer_simulator") noise_model = NoiseModel.from_backend(backend) diff --git a/src/modules/applications/QML/generative_modeling/mappings/__init__.py b/src/modules/applications/qml/generative_modeling/mappings/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/mappings/__init__.py rename to src/modules/applications/qml/generative_modeling/mappings/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py b/src/modules/applications/qml/generative_modeling/metrics/MetricsGeneralization.py similarity index 60% rename from src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py rename to src/modules/applications/qml/generative_modeling/metrics/MetricsGeneralization.py index 68f715f1..d11baf6f 100644 --- a/src/modules/applications/QML/generative_modeling/data/data_handler/MetricsGeneralization.py +++ b/src/modules/applications/qml/generative_modeling/metrics/MetricsGeneralization.py @@ -13,33 +13,21 @@ # limitations under the License. import math - import numpy as np class MetricsGeneralization: """ A class to compute generalization metrics for generated samples based on train and solution sets. - - :param train_set: set of queries in the training set. - :type train_set: np.array - :param train_size: the fraction of queries used for training. - :type train_size: float - :param solution_set: set of queries in the solution set. - :type solution_set: np.array - :param n_qubits: the number of qubits. - :type n_qubits: int """ - def __init__( - - self, - train_set, - train_size, - solution_set, - n_qubits, - - ) -> None: + def __init__(self, train_set: np.array, train_size: float, solution_set: np.array, n_qubits: int): + """ + :param train_set: Set of queries in the training set + :param train_size: The fraction of queries used for training + :param solution_set: Set of queries in the solution set + :param n_qubits: The number of qubits. + """ self.train_set = train_set self.train_size = train_size self.solution_set = solution_set @@ -50,12 +38,10 @@ def __init__( def get_masks(self) -> tuple[np.array, np.array]: """ - Method to determine the masks, on which the generalization metrics are based on + Method to determine the masks, on which the generalization metrics are based. - :return: masks needed to determine the generalization metrics for a given train and solution set - :rtype: tuple[np.array, np.array] + :return: Masks needed to determine the generalization metrics for a given train and solution set """ - mask_new = np.ones(self.n_states, dtype=bool) mask_new[self.train_set] = 0 @@ -67,12 +53,10 @@ def get_masks(self) -> tuple[np.array, np.array]: def get_metrics(self, generated: np.array) -> dict: """ - Method that determines all generalization metrics of a given multiset of generated samples + Method that determines all generalization metrics of a given multiset of generated samples. - :param generated: generated samples - :type generated: np.array - :return: dictionary with generalization metrics - :rtype: dict + :param generated: Generated samples + :return: Dictionary with generalization metrics """ g_new = np.sum(generated[self.mask_new]) g_sol = np.sum(generated[self.mask_sol]) @@ -92,59 +76,47 @@ def get_metrics(self, generated: np.array) -> dict: def fidelity(self, g_new: float, g_sol: float) -> float: """ - Method to determine the fidelity + Method to determine the fidelity. - :param g_new: multi-subset of unseen queries (noisy or valid) - :type g_new: float - :param g_sol: multi-subset of unseen and valid queries - :type g_sol: float - :return: fidelity - :rtype: float + :param g_new: Multi-subset of unseen queries (noisy or valid) + :param g_sol: Multi-subset of unseen and valid queries + :return: Fidelity """ return g_sol / g_new def coverage(self, g_sol_unique: float) -> float: """ - Method to determine the coverage + Method to determine the coverage. - :param g_sol_unique: subset of unique unseen and valid queries - :type g_sol_unique: float - :return: coverage - :rtype: float + :param g_sol_unique: Subset of unique unseen and valid queries + :return: Coverage """ return g_sol_unique / (math.ceil(1 - self.train_size) * len(self.solution_set)) def normalized_rate(self, g_sol: float) -> float: """ - Method to determine the normalized_rate + Method to determine the normalized_rate. - :param g_sol: multi-subset of unseen and valid queries - :type g_sol: float - :return: normalized_rate - :rtype: float + :param g_sol: Multi-subset of unseen and valid queries + :return: Normalized_rate """ return g_sol / ((1 - self.train_size) * self.n_shots) def exploration(self, g_new: float) -> float: """ - Method to determine the exploration + Method to determine the exploration. - :param g_new: multi-subset of unseen queries (noisy or valid) - :type g_new: float - :return: exploration - :rtype: float + :param g_new: Multi-subset of unseen queries (noisy or valid) + :return: Exploration """ return g_new / self.n_shots def precision(self, g_sol: float, g_train: float) -> float: """ - Method to determine the precision + Method to determine the precision. - :param g_sol: multi-subset of unseen and valid queries - :type g_sol: float - :param g_train: number of queries that were memorized from the training set - :type g_train: float - :return: precision - :rtype: float + :param g_sol: Multi-subset of unseen and valid queries + :param g_train: Number of queries that were memorized from the training set + :return: Precision """ return (np.sum(g_sol) + np.sum(g_train)) / self.n_shots diff --git a/src/modules/training/Inference.py b/src/modules/applications/qml/generative_modeling/training/Inference.py similarity index 67% rename from src/modules/training/Inference.py rename to src/modules/applications/qml/generative_modeling/training/Inference.py index 22f6fa00..1330efbb 100644 --- a/src/modules/training/Inference.py +++ b/src/modules/applications/qml/generative_modeling/training/Inference.py @@ -13,17 +13,18 @@ # limitations under the License. from typing import TypedDict -from modules.training.Training import * +import numpy as np +from modules.applications.qml.generative_modeling.training.TrainingGenerative import TrainingGenerative, Core, GPU -class Inference(Training): +class Inference(TrainingGenerative): """ - This module executes a quantum circuit with parameters of a pretrained model. + This module executes a quantum circuit with parameters of a pretrained model. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("Inference") @@ -33,33 +34,27 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module. """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this circuit - - :return: - .. code-block:: python - - return { - "pretrained": { - "values": [False], - "custom_input": True, - "postproc": str, - "description": "Please provide the parameters of a pretrained model." - } - } + Returns the configurable settings for this circuit. + + :return: Configuration settings for the pretrained model + .. code-block:: python + + return { + "pretrained": { + "values": [False], + "custom_input": True, + "postproc": str, + "description": "Please provide the parameters of a pretrained model." + } + } """ return { "pretrained": { @@ -82,20 +77,22 @@ class Config(TypedDict): pretrained: str def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> dict: """ - Method that uses a pretrained model for inference + Method that uses a pretrained model for inference. :param input_data: Dictionary with information needed for inference - :type input_data: dict :param config: Inference settings - :type config: Config :param kwargs: Optional additional arguments - :type kwargs: dict :return: Dictionary including the information of previous modules as well as of this module - :rtype: dict """ self.n_states_range = range(2 ** input_data['n_qubits']) self.target = np.asarray(input_data["histogram_train"]) @@ -105,9 +102,11 @@ def start_training(self, input_data: dict, config: Config, **kwargs: dict) -> di pmfs, samples = execute_circuit([parameters.get() if GPU else parameters]) pmfs = np.asarray(pmfs) - samples = self.sample_from_pmf( - pmf=pmfs[0], - n_shots=input_data["n_shots"]) if samples is None else samples[0] + samples = ( + self.sample_from_pmf(pmf=pmfs[0], n_shots=input_data["n_shots"]) + if samples is None + else samples[0] + ) loss = self.kl_divergence(pmfs.reshape([-1, 1]), self.target) diff --git a/src/modules/training/QCBM.py b/src/modules/applications/qml/generative_modeling/training/QCBM.py similarity index 71% rename from src/modules/training/QCBM.py rename to src/modules/applications/qml/generative_modeling/training/QCBM.py index d8bc49a7..301b3f4d 100644 --- a/src/modules/training/QCBM.py +++ b/src/modules/applications/qml/generative_modeling/training/QCBM.py @@ -14,19 +14,20 @@ from typing import TypedDict import logging +import numpy as np from cma import CMAEvolutionStrategy from tensorboardX import SummaryWriter from matplotlib import figure, axes import matplotlib.pyplot as plt -from modules.training.Training import * +from modules.applications.qml.generative_modeling.training.TrainingGenerative import TrainingGenerative, Core, GPU from utils_mpi import is_running_mpi, get_comm MPI = is_running_mpi() comm = get_comm() -class QCBM(Training): +class QCBM(TrainingGenerative): """ This module optimizes the parameters of quantum circuit using CMA-ES. This training method is referred to as quantum circuit born machine (QCBM). @@ -34,12 +35,12 @@ class QCBM(Training): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("QCBM") self.n_states_range: list - self.target: np.array + self.target: np.ndarray self.study_generalization: bool self.generalization_metrics: dict self.writer: SummaryWriter @@ -50,70 +51,54 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "cma", - "version": "4.0.0" - }, - { - "name": "matplotlib", - "version": "3.7.5" - }, - { - "name": "tensorboard", - "version": "2.17.0" - }, - { - "name": "tensorboardX", - "version": "2.6.2.2" - } + {"name": "numpy", "version": "1.26.4"}, + {"name": "cma", "version": "4.0.0"}, + {"name": "matplotlib", "version": "3.7.5"}, + {"name": "tensorboard", "version": "2.17.0"}, + {"name": "tensorboardX", "version": "2.6.2.2"} ] def get_parameter_options(self) -> dict: """ - Returns the configurable settings for the quantum circuit born machine - - :return: - .. code-block:: python - - return { - - "population_size": { - "values": [5, 10, 100, 200, 10000], - "description": "What population size do you want?" - }, - - "max_evaluations": { - "values": [100, 1000, 20000, 100000], - "description": "What should be the maximum number of evaluations?" - }, - - "sigma": { - "values": [0.01, 0.5, 1, 2], - "description": "Which sigma would you like to use?" - }, - - "pretrained": { - "values": [False], - "custom_input": True, - "postproc": str, - "description": "Please provide the parameters of a pretrained model." - }, - - "loss": { - "values": ["KL", "NLL"], - "description": "Which loss function do you want to use?" - } + This function returns the configurable settings for the quantum circuit born machine. + + :return: Configuration settings for QCBM + .. code-block:: python + + return { + + "population_size": { + "values": [5, 10, 100, 200, 10000], + "description": "What population size do you want?" + }, + + "max_evaluations": { + "values": [100, 1000, 20000, 100000], + "description": "What should be the maximum number of evaluations?" + }, + + "sigma": { + "values": [0.01, 0.5, 1, 2], + "description": "Which sigma would you like to use?" + }, + + "pretrained": { + "values": [False], + "custom_input": True, + "postproc": str, + "description": "Please provide the parameters of a pretrained model." + }, + + "loss": { + "values": ["KL", "NLL"], + "description": "Which loss function do you want to use?" } + } """ return { "population_size": { @@ -146,7 +131,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -164,23 +149,28 @@ class Config(TypedDict): loss: str def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") def setup_training(self, input_data: dict, config: Config) -> tuple[float, dict]: """ Method to configure the training setup including CMA-ES and tensorboard. - :param input_data: a representation of the quantum machine learning model that will be trained - :type input_data: dict + :param input_data: A representation of the quantum machine learning model that will be trained :param config: Config specifying the parameters of the training - :type config: dict - :return: random initial parameter and options for CMA-ES - :rtype: tuple[float, dict] + :return: Random initial parameter and options for CMA-ES """ logging.info( - f"Running config: [backend={input_data['backend']}] [n_qubits={input_data['n_qubits']}] "\ - f"[population_size={config['population_size']}]") + f"Running config: [backend={input_data['backend']}] " + f"[n_qubits={input_data['n_qubits']}] " + f"[population_size={config['population_size']}]" + ) self.study_generalization = "generalization_metrics" in list(input_data.keys()) if self.study_generalization: @@ -220,13 +210,9 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict This function finds the best parameters of the circuit on a transformed problem instance and returns a solution. :param input_data: A representation of the quantum machine learning model that will be trained - :type input_data: dict :param config: Config specifying the parameters of the training - :type config: dict - :param kwargs: optional additional settings - :type kwargs: dict + :param kwargs: Optional additional settings :return: Dictionary including the information of previous modules as well as of the training - :rtype: dict """ size = None @@ -234,19 +220,23 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict input_data["store_dir_iter"] += f"_{input_data['dataset_name']}_qubits{input_data['n_qubits']}" x0, options = self.setup_training(input_data, config) - if comm.Get_rank() == 0: + is_master = comm.Get_rank() == 0 + if is_master: self.target = np.asarray(input_data["histogram_train"]) self.target[self.target == 0] = 1e-8 + self.n_states_range = range(2 ** input_data['n_qubits']) execute_circuit = input_data["execute_circuit"] timing = self.Timing() es = CMAEvolutionStrategy(x0.get() if GPU else x0, config['sigma'], options) + for parameter in ["best_parameters", "time_circuit", "time_loss", "KL", "best_sample"]: input_data[parameter] = [] best_loss = float("inf") self.fig, self.ax = plt.subplots() + while not es.stop(): solutions = es.ask() epoch = es.result[4] @@ -258,10 +248,11 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict time_circ = timing.stop_recording() timing.start_recording() - if comm.Get_rank() == 0: + if is_master: loss_epoch = self.loss_func(pmfs_model.reshape([config['population_size'], -1]), self.target) else: loss_epoch = np.empty(config["population_size"]) + comm.Bcast(loss_epoch, root=0) comm.Barrier() @@ -277,28 +268,39 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict input_data["KL"].append(float(es.result[1])) logging.info( - f"[Iteration {es.result[4]}] " - f"[{config['loss']}: {es.result[1]:.5f}] "\ - f"[Circuit processing: {(time_circ):.3f} ms] "\ - f"[{config['loss']} processing: {(time_loss):.3f} ms] "\ - f"[sigma: {sigma:.5f}]") + f"[Iteration {epoch}] " + f"[{config['loss']}: {es.result[1]:.5f}] " + f"[Circuit processing: {(time_circ):.3f} ms] " + f"[{config['loss']} processing: {(time_loss):.3f} ms] " + f"[sigma: {sigma:.5f}]" + ) plt.close() self.writer.flush() self.writer.close() input_data["best_parameter"] = es.result[0] - best_sample = self.sample_from_pmf(best_pmf.get() if GPU else best_pmf, # pylint: disable=E0606 + best_sample = self.sample_from_pmf(best_pmf.get() if GPU else best_pmf, # pylint: disable=E0606 n_shots=input_data["n_shots"]) - input_data["best_sample"] = best_sample.get() if GPU else best_sample # pylint: disable=E1101 + input_data["best_sample"] = best_sample.get() if GPU else best_sample # pylint: disable=E1101 return input_data - def data_visualization(self, loss_epoch, pmfs_model, samples, epoch): + def data_visualization(self, loss_epoch: np.ndarray, pmfs_model: np.ndarray, samples: any, epoch: int) -> ( + np.ndarray): + """ + Visualizes the data and metrics for training. + + :param loss_epoch: Loss for the current epoch + :param pmfs_model: The probability mass functions from the model + :param samples: The samples from the model + :param epoch: The current epoch number + :return: Best probability mass function for visualization + """ index = loss_epoch.argmin() best_pmf = pmfs_model[index] / pmfs_model[index].sum() - if self.study_generalization: + if self.study_generalization: if samples is None: counts = self.sample_from_pmf( pmf=best_pmf.get() if GPU else best_pmf, @@ -307,23 +309,24 @@ def data_visualization(self, loss_epoch, pmfs_model, samples, epoch): counts = samples[int(index)] metrics = self.generalization_metrics.get_metrics(counts if GPU else counts) - for (key, value) in metrics.items(): + for key, value in metrics.items(): self.writer.add_scalar(f"metrics/{key}", value, epoch) nll = self.nll(best_pmf.reshape([1, -1]), self.target) kl = self.kl_divergence(best_pmf.reshape([1, -1]), self.target) mmd = self.mmd(best_pmf.reshape([1, -1]), self.target) + self.writer.add_scalar("metrics/NLL", nll.get() if GPU else nll, epoch) self.writer.add_scalar("metrics/KL", kl.get() if GPU else kl, epoch) self.writer.add_scalar("metrics/MMD", mmd.get() if GPU else mmd, epoch) self.ax.clear() self.ax.imshow( - best_pmf.reshape(int(np.sqrt(best_pmf.size)), int(np.sqrt(best_pmf.size))).get() if GPU - else best_pmf.reshape(int(np.sqrt(best_pmf.size)), - int(np.sqrt(best_pmf.size))), + best_pmf.reshape(int(np.sqrt(best_pmf.size)), int(np.sqrt(best_pmf.size))).get() + if GPU else best_pmf.reshape(int(np.sqrt(best_pmf.size)), int(np.sqrt(best_pmf.size))), cmap='binary', - interpolation='none') + interpolation='none' + ) self.ax.set_title(f'Iteration {epoch}') self.writer.add_figure('grid_figure', self.fig, global_step=epoch) diff --git a/src/modules/training/QGAN.py b/src/modules/applications/qml/generative_modeling/training/QGAN.py similarity index 74% rename from src/modules/training/QGAN.py rename to src/modules/applications/qml/generative_modeling/training/QGAN.py index bb6161a1..647fa91d 100644 --- a/src/modules/training/QGAN.py +++ b/src/modules/applications/qml/generative_modeling/training/QGAN.py @@ -16,28 +16,28 @@ import logging import torch -from torch.utils.data import DataLoader +from torch.utils.data import DataLoader from torch import nn -import torch.nn.functional as F +import torch.nn.functional as funct from tensorboardX import SummaryWriter import numpy as np import matplotlib.pyplot as plt -from modules.training.Training import * -from modules.applications.QML.generative_modeling.transformations.Transformation import * - +from modules.applications.qml.generative_modeling.training.TrainingGenerative import TrainingGenerative, Core from utils_mpi import is_running_mpi, get_comm + MPI = is_running_mpi() comm = get_comm() -class QGAN(Training): # pylint: disable=R0902 + +class QGAN(TrainingGenerative): # pylint: disable=R0902 """ Class for QGAN """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__("QGAN") @@ -79,69 +79,53 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "numpy", - "version": "1.26.4" + {"name": "numpy", "version": "1.26.4"}, + {"name": "torch", "version": "2.2.0"}, + {"name": "matplotlib", "version": "3.7.5"}, + {"name": "tensorboard", "version": "2.17.0"}, + {"name": "tensorboardX", "version": "2.6.2.2"} + ] + + def get_parameter_options(self) -> dict: + """ + Returns the configurable settings for this circuit. + + :return: Configuration settings for QGAN + .. code-block:: python + return { + "epochs": { + "values": [2, 100, 200, 10000], + "description": "How many epochs do you want?" + }, + "batch_size": { + "values": [10, 20, 100, 2000], + "description": "What batch size do you want?" }, - { - "name": "torch", - "version": "2.2.0" + "learning_rate_generator": { + "values": [0.1, 0.2], + "description": "What learning rate do you want to set for the generator?" + }, + "learning_rate_discriminator": { + "values": [0.1, 0.05], + "description": "What learning rate do you want to set for the discriminator?" }, - { - "name": "matplotlib", - "version": "3.7.5" + "device": { + "values": ["cpu", "gpu"], + "description": "Where do you want to run the discriminator?" }, - { - "name": "tensorboard", - "version": "2.17.0" + "pretrained": { + "values": [True, False], + "description": "Do you want to use parameters of a pretrained model?" }, - { - "name": "tensorboardX", - "version": "2.6.2.2" + "loss": { + "values": ["KL", "NLL"], + "description": "Which loss function do you want to use?" } - ] - - def get_parameter_options(self) -> dict: - """ - Returns the configurable settings for this circuit - - :return: - .. code-block:: python - return { - "epochs": { - "values": [2, 100, 200, 10000], - "description": "How many epochs do you want?" - }, - "batch_size": { - "values": [10, 20, 100, 2000], - "description": "What batch size do you want?" - }, - "learning_rate_generator": { - "values": [0.1, 0.2], - "description": "What learning rate do you want to set for the generator?" - }, - "learning_rate_discriminator": { - "values": [0.1, 0.05], - "description": "What learning rate do you want to set for the discriminator?" - }, - "device": { - "values": ["cpu", "gpu"], - "description": "Where do you want to run the discriminator?" - }, - "pretrained": { - "values": [True, False], - "description": "Do you want to use parameters of a pretrained model?" - }, - "loss": { - "values": ["KL", "NLL"], - "description": "Which loss function do you want to use?" - } } """ return { @@ -199,16 +183,21 @@ class Config(TypedDict): loss: str def get_default_submodule(self, option: str) -> Core: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") def setup_training(self, input_data: dict, config: dict) -> None: """ + Sets up the training configuration. + :param input_data: dictionary with the variables from the circuit needed to start the training - :type input_data: dict - :param config: - :type config: dict + :param config: Configurations for the QGAN training. """ - self.beta_1 = 0.5 self.real_label = 1. self.fake_label = 0. @@ -234,7 +223,8 @@ def setup_training(self, input_data: dict, config: dict) -> None: self.bins_train = input_data["binary_train"] if input_data["dataset_name"] == "Cardinality_Constraint": new_size = 1000 - self.bins_train = np.repeat(self.bins_train,new_size,axis=0) + self.bins_train = np.repeat(self.bins_train, new_size, axis=0) + self.study_generalization = "generalization_metrics" in list(input_data.keys()) if self.study_generalization: self.generalization_metrics = input_data["generalization_metrics"] @@ -248,15 +238,15 @@ def setup_training(self, input_data: dict, config: dict) -> None: self.discriminator.apply(Discriminator.weights_init) self.params = np.random.rand(self.n_params) * np.pi - self.generator = QuantumGenerator(self.n_qubits, self.execute_circuit, self.batch_size) - self.accuracy = [] + self.accuracy = [] self.criterion = torch.nn.BCELoss() self.optimizer_discriminator = torch.optim.Adam( self.discriminator.parameters(), lr=config["learning_rate_discriminator"], - betas=(self.beta_1, 0.999)) + betas=(self.beta_1, 0.999) + ) self.real_labels = torch.full((self.batch_size,), 1.0, dtype=torch.float, device=self.device) self.fake_labels = torch.full((self.batch_size,), 0.0, dtype=torch.float, device=self.device) @@ -272,16 +262,12 @@ def setup_training(self, input_data: dict, config: dict) -> None: def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict: # pylint: disable=R0915 """ - This function starts the training of the QGAN + This function starts the training of the QGAN. - :param input_data: dictionary with the variables from the circuit needed to start the training - :type input_data: dict - :param config: annealing settings - :type config: dict - :param kwargs: optional additional arguments - :type kwargs: dict - :return: dictionary including the solution - :rtype: dict + :param input_data: Dictionary with the variables from the circuit needed to start the training + :param config: Training settings + :param kwargs: Optional additional arguments + :return: Dictionary including the solution """ self.setup_training(input_data, config) generator_losses = [] @@ -297,47 +283,45 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict for batch, data in enumerate(self.dataloader): # Training the discriminator # Data from real distribution for training the discriminator - real_data = data.float().to(self.device) self.discriminator.zero_grad() - outD_real = self.discriminator(real_data).view(-1) - errD_real = self.criterion(outD_real, self.real_labels) - errD_real.backward() + out_d_real = self.discriminator(real_data).view(-1) + err_d_real = self.criterion(out_d_real, self.real_labels) + err_d_real.backward() # Use Quantum Variational Circuit to generate fake samples fake_data, _ = self.generator.execute(self.params, self.batch_size) fake_data = fake_data.float().to(self.device) + out_d_fake = self.discriminator(fake_data).view(-1) + err_d_fake = self.criterion(out_d_fake, self.fake_labels) + err_d_fake.backward() - outD_fake = self.discriminator(fake_data).view(-1) - errD_fake = self.criterion(outD_fake, self.fake_labels) - errD_fake.backward() - - errD = errD_real + errD_fake + err_d = err_d_real + err_d_fake self.optimizer_discriminator.step() - outD_fake = self.discriminator(fake_data).view(-1) - errG = self.criterion(outD_fake, self.real_labels) - fake_data, _ = self.generator.execute(self.params,self.batch_size) - gradients= self.generator.compute_gradient( + out_d_fake = self.discriminator(fake_data).view(-1) + err_g = self.criterion(out_d_fake, self.real_labels) + fake_data, _ = self.generator.execute(self.params, self.batch_size) + gradients = self.generator.compute_gradient( self.params, self.discriminator, self.criterion, self.real_labels, - self.device) + self.device + ) updated_params = self.params - self.learning_rate_generator * gradients self.params = updated_params self.discriminator_weights = self.discriminator.state_dict() - - generator_losses.append(errG.item()) - discriminator_losses.append(errD.item()) + generator_losses.append(err_g.item()) + discriminator_losses.append(err_d.item()) # Calculate loss _, pmfs_model = self.generator.execute(self.params, self.n_shots) pmfs_model = np.asarray(pmfs_model.copy()) - loss= self.loss_func(pmfs_model[None,], self.target) + loss = self.loss_func(pmfs_model[None,], self.target) self.accuracy.append(loss) self.writer.add_scalar("metrics/KL", loss, epoch * n_batches + batch) @@ -345,8 +329,8 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict self.writer.add_scalar("metrics/KL_circuit_evals", loss, circuit_evals) # Calculate and log the loss values at the end of each epoch - self.writer.add_scalar('Loss/GAN_Generator', errG.item(), circuit_evals) - self.writer.add_scalar('Loss/GAN_Discriminator', errD.item(), circuit_evals) + self.writer.add_scalar('Loss/GAN_Generator', err_g.item(), circuit_evals) + self.writer.add_scalar('Loss/GAN_Discriminator', err_d.item(), circuit_evals) if loss < best_kl_divergence: best_kl_divergence = loss @@ -360,7 +344,7 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict log_message = ( f"Epoch: {epoch + 1}/{self.n_epochs}, " f"Batch: {batch + 1}/{len(self.bins_train) // self.batch_size}, " - f"Discriminator Loss: {errD.item()}, Generator Loss: {errG.item()}, KL Divergence: {loss} " + f"Discriminator Loss: {err_d.item()}, Generator Loss: {err_g.item()}, KL Divergence: {loss} " ) logging.info(log_message) @@ -399,7 +383,6 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict input_data["best_parameter"] = best_generator_params input_data["best_sample"] = best_sample - input_data["KL"] = self.accuracy input_data["generator_loss"] = generator_losses input_data["discriminator_loss"] = discriminator_losses @@ -409,8 +392,9 @@ def start_training(self, input_data: dict, config: dict, **kwargs: dict) -> dict class Discriminator(nn.Module): """ - This class defines the discriminator of the QGAN + This class defines the discriminator of the QGAN. """ + def __init__(self, input_length: int): super().__init__() self.dense1 = nn.Linear(int(input_length), 2 * int(input_length)) @@ -418,29 +402,25 @@ def __init__(self, input_length: int): def forward(self, x: torch.Tensor) -> float: """ - This function initialized the weight tensor of the linear - layers with values using a Xavier uniform distribution. + Initializes the weight tensor of the linear layers with values using a Xavier uniform distribution. :param x: Input of the discriminator :type x: torch.Tensor :return: Probability fake/real sample :rtype: float """ - h = F.leaky_relu(self.dense1(x)) - h = F.leaky_relu(self.dense2(h)) - return F.sigmoid(h) + h = funct.leaky_relu(self.dense1(x)) + h = funct.leaky_relu(self.dense2(h)) + return funct.sigmoid(h) @staticmethod def weights_init(m: nn.Linear) -> None: """ - This function initialized the weight tensor of the linear + Initializes the weight tensor of the linear layers with values using a Xavier uniform distribution. :param m: Neural network layer - :type m: nn.Linear """ - print(type(m)) - print(m) if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight.data, gain=10) nn.init.constant_(m.bias.data, 1) @@ -448,8 +428,9 @@ def weights_init(m: nn.Linear) -> None: class QuantumGenerator: """ - This class defines the generator of the QGAN + This class defines the generator of the QGAN. """ + def __init__(self, n_qubits, execute_circuit, batch_size): self.n_qubits = n_qubits self.execute_circuit = execute_circuit @@ -457,14 +438,11 @@ def __init__(self, n_qubits, execute_circuit, batch_size): def execute(self, params: np.ndarray, n_shots: int) -> tuple[torch.Tensor, np.ndarray]: """ - This function defines the forward pass of the generator + Forward pass of the generator. :param params: Parameters of the quantum circuit - :type params: np.ndarray :param n_shots: Number of shots - :type n_shots: int :return: samples and the probability distribution generated by the quantum circuit - :rtype: tuple[torch.Tensor, np.ndarray] """ # Call the quantum circuit and obtain probability distributions @@ -487,20 +465,14 @@ def execute(self, params: np.ndarray, n_shots: int) -> tuple[torch.Tensor, np.nd def compute_gradient(self, params: np.ndarray, discriminator: torch.nn.Module, criterion: callable, label: torch.Tensor, device: str) -> np.ndarray: """ - This function defines the forward pass of the generator + This function defines the forward pass of the generator. :param params: Parameters of the quantum circuit - :type params: np.ndarray :param discriminator: Discriminator of the QGAN - :type discriminator: torch.nn.Module :param criterion: Loss function - :type criterion: callable :param label: Label indicating of sample is true or fake - :type label: torch.Tensor - :param device: torch device (e.g. CPU or CUDA) - :type device: str - :return: samples and the probability distribution generated by the quantum circuit - :rtype: np.ndarray + :param device: Torch device (e.g., CPU or CUDA) + :return: Samples and the probability distribution generated by the quantum circuit """ shift = 0.5 * np.pi gradients = np.zeros(len(params)) # Initialize gradients as an array of zeros diff --git a/src/modules/training/Training.py b/src/modules/applications/qml/generative_modeling/training/TrainingGenerative.py similarity index 66% rename from src/modules/training/Training.py rename to src/modules/applications/qml/generative_modeling/training/TrainingGenerative.py index e483892b..bcd1d39b 100644 --- a/src/modules/training/Training.py +++ b/src/modules/applications/qml/generative_modeling/training/TrainingGenerative.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from abc import ABC, abstractmethod +from abc import ABC import time try: @@ -26,17 +26,20 @@ logging.info("CuPy not available, using vanilla numpy, data processing on CPU") from modules.Core import Core +from modules.applications.qml.Training import Training from utils import start_time_measurement, end_time_measurement -class Training(Core, ABC): +class TrainingGenerative(Core, Training, ABC): """ - The Training module is the base class fot both finding (QCBM) and executing trained models (Inference) + The Training module is the base class fot both finding (QCBM) and executing trained models (Inference). """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. + + :param name: Name of the training instance """ self.name = name super().__init__() @@ -45,30 +48,20 @@ def __init__(self, name): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. :return: list of dict with requirements of this module - :rtype: list[dict] """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] - def postprocess(self, input_data: dict, config: dict, **kwargs): + def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - Here, the actual training of the machine learning model is done + Perform the actual training of the machine learning model. :param input_data: Collected information of the benchmarking process - :type input_data: dict :param config: Training settings - :type config: dict :param kwargs: Optional additional arguments - :type kwargs: dict - :return: - :rtype: + :return: Training results and the postprocessing time """ start = start_time_measurement() logging.info("Start training") @@ -83,90 +76,62 @@ def postprocess(self, input_data: dict, config: dict, **kwargs): logging.info(f"Training finished in {postprocessing_time / 1000} s.") return training_results, postprocessing_time - @abstractmethod - def start_training(self, input_data: dict, config: any, **kwargs: dict) -> dict: - """ - This function starts the training of QML model or deploys a pretrained model. - - :param input_data: A representation of the quantum machine learning model that will be trained - :type input_data: dict - :param config: Config specifying the parameters of the training (dict-like Config type defined in children) - :type config: any - :param kwargs: optional additional settings - :type kwargs: dict - :return: Solution, the time it took to compute it and some optional additional information - :rtype: dict - """ - pass - def sample_from_pmf(self, pmf: np.ndarray, n_shots: int) -> np.ndarray: """ - Function to sample from the probability mass function generated by the quantum circuit + This function samples from the probability mass function generated by the quantum circuit. :param pmf: Probability mass function generated by the quantum circuit - :type pmf: np.ndarray :param n_shots: Number of shots - :type n_shots: int - :return: number of counts in the 2**n_qubits bins - :rtype: np.ndarray + :return: Number of counts in the 2**n_qubits bins """ samples = np.random.choice(self.n_states_range, size=n_shots, p=pmf) counts = np.bincount(samples, minlength=len(self.n_states_range)) return counts - def kl_divergence(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: + def kl_divergence(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: """ - Kullback-Leibler divergence, that is used as a loss function + This function calculates the Kullback-Leibler divergence, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit - :type pmf_model: np.ndarray :param pmf_target: Probability mass function of the target distribution - :type pmf_target: np.ndarray :return: Kullback-Leibler divergence - :rtype: float """ pmf_model[pmf_model == 0] = 1e-8 return np.sum(pmf_target * np.log(pmf_target / pmf_model), axis=1) - def nll(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: + def nll(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: """ - Negative log likelihood, that is used as a loss function + This function calculates th negative log likelihood, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit - :type pmf_model: np.ndarray :param pmf_target: Probability mass function of the target distribution - :type pmf_target: np.ndarray :return: Negative log likelihood - :rtype: float """ pmf_model[pmf_model == 0] = 1e-8 return -np.sum(pmf_target * np.log(pmf_model), axis=1) - def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> float: + def mmd(self, pmf_model: np.ndarray, pmf_target: np.ndarray) -> np.ndarray: """ - Maximum mean discrepancy, that is used as a loss function + This function calculates the maximum mean discrepancy, that is used as a loss function. :param pmf_model: Probability mass function generated by the quantum circuit - :type pmf_model: np.ndarray :param pmf_target: Probability mass function of the target distribution - :type pmf_target: np.ndarray :return: Maximum mean discrepancy - :rtype: float """ pmf_model[pmf_model == 0] = 1e-8 - sigma = 1/pmf_model.shape[1] + sigma = 1 / pmf_model.shape[1] kernel_distance = np.exp((-np.square(pmf_model - pmf_target) / (sigma ** 2))) mmd = 2 - 2 * np.mean(kernel_distance, axis=1) return mmd class Timing: """ - This module is an abstraction of time measurement for both CPU and GPU processes + This module is an abstraction of time measurement for both CPU and GPU processes. """ def __init__(self): """ - Constructor method + Constructor method. """ if GPU: @@ -178,29 +143,33 @@ def __init__(self): self.start_recording = self.start_recording_gpu if GPU else self.start_recording_cpu self.stop_recording = self.stop_recording_gpu if GPU else self.stop_recording_cpu - def start_recording_cpu(self): + def start_recording_cpu(self) -> None: """ - Function to start time measurement on the CPU + This is a function to start time measurement on the CPU. """ self.start_cpu = start_time_measurement() - def stop_recording_cpu(self): + def stop_recording_cpu(self) -> float: """ - Function to stop time measurement on the CPU + This is a function to stop time measurement on the CPU. + + .return: Elapsed time in milliseconds """ return end_time_measurement(self.start_cpu) - def start_recording_gpu(self): + def start_recording_gpu(self) -> None: """ - Function to start time measurement on the GPU + This is a function to start time measurement on the GPU. """ self.start_gpu = np.cuda.Event() self.end_gpu = np.cuda.Event() self.start_gpu.record() - def stop_recording_gpu(self): + def stop_recording_gpu(self) -> float: """ - Function to stop time measurement on the GPU + This is a function to stop time measurement on the GPU. + + :return: Elapsed time in milliseconds """ self.end_gpu.record() self.end_gpu.synchronize() diff --git a/src/modules/training/__init__.py b/src/modules/applications/qml/generative_modeling/training/__init__.py similarity index 100% rename from src/modules/training/__init__.py rename to src/modules/applications/qml/generative_modeling/training/__init__.py diff --git a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py b/src/modules/applications/qml/generative_modeling/transformations/MinMax.py similarity index 84% rename from src/modules/applications/QML/generative_modeling/transformations/MinMax.py rename to src/modules/applications/qml/generative_modeling/transformations/MinMax.py index 991101aa..1b895f23 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/MinMax.py +++ b/src/modules/applications/qml/generative_modeling/transformations/MinMax.py @@ -13,18 +13,17 @@ # limitations under the License. from typing import Union - import numpy as np -from modules.applications.QML.generative_modeling.transformations.Transformation import * -from modules.circuits.CircuitStandard import CircuitStandard -from modules.circuits.CircuitCardinality import CircuitCardinality +from modules.applications.qml.generative_modeling.transformations.Transformation import Transformation +from modules.applications.qml.generative_modeling.circuits.CircuitStandard import CircuitStandard +from modules.applications.qml.generative_modeling.circuits.CircuitCardinality import CircuitCardinality class MinMax(Transformation): # pylint: disable=R0902 """ In min-max normalization each data point is shifted - such that it lies between 0 and 1 + such that it lies between 0 and 1. """ def __init__(self): @@ -43,20 +42,13 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def get_default_submodule(self, option: str) -> Union[CircuitStandard, CircuitCardinality]: - if option == "CircuitStandard": return CircuitStandard() elif option == "CircuitCardinality": @@ -66,12 +58,10 @@ def get_default_submodule(self, option: str) -> Union[CircuitStandard, CircuitCa def get_parameter_options(self) -> dict: """ - Returns empty dict as this transformation has no configurable settings + Returns empty dict as this transformation has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return {} def transform(self, input_data: dict, config: dict) -> dict: @@ -80,11 +70,8 @@ def transform(self, input_data: dict, config: dict) -> dict: of the training dataset in the transformed space. :param input_data: A dictionary containing information about the dataset and application configuration. - :type input_data: dict :param config: A dictionary with parameters specified in the Config class. - :type config: dict :return: A tuple containing a dictionary with MinMax-transformed data. - :rtype: dict """ self.dataset_name = input_data["dataset_name"] self.dataset = input_data["dataset"] @@ -110,7 +97,7 @@ def transform(self, input_data: dict, config: dict) -> dict: value = 0 for count in histogram_transformed_1d: if count > 0: - solution_space[position:position+int(count)] = value + solution_space[position:position + int(count)] = value position += int(count) value += 1 @@ -140,10 +127,8 @@ def reverse_transform(self, input_data: dict) -> dict: """ Transforms the solution back to the representation needed for validation/evaluation. - :param input_data: dictionary containing the solution - :type input_data: dict - :return: solution transformed accordingly - :rtype: dict + :param input_data: Dictionary containing the solution + :return: Solution transformed accordingly """ best_results = input_data["best_sample"] depth = input_data["depth"] @@ -198,29 +183,25 @@ def reverse_transform(self, input_data: dict) -> dict: def fit_transform(self, data: np.ndarray) -> np.ndarray: """ - Method that performs the min max normalization + Method that performs the min max normalization. :param data: Data to be fitted - :type data: np.ndarray - :return: fitted data - :rtype: np.ndarray + :return: Fitted data """ - self.min = data.min() - self.max = data.max() - data.min() - data = (data - self.min) / self.max + data_min = data.min() + data_max = data.max() - data_min + data = (data - data_min) / data_max return data def inverse_transform(self, data: np.ndarray) -> np.ndarray: """ - Method that performs the inverse min max normalization + Method that performs the inverse min max normalization. :param data: Data to be fitted - :type data: np.ndarray - :return: data in original space - :rtype: np.ndarray + :return: Data in original space """ - self.min = data.min() - self.max = data.max() - data.min() + data_min = data.min() + data_max = data.max() - data_min - return data * self.max + self.min + return data * data_max + data_min diff --git a/src/modules/applications/QML/generative_modeling/transformations/PIT.py b/src/modules/applications/qml/generative_modeling/transformations/PIT.py similarity index 86% rename from src/modules/applications/QML/generative_modeling/transformations/PIT.py rename to src/modules/applications/qml/generative_modeling/transformations/PIT.py index f37fc269..41532995 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/PIT.py +++ b/src/modules/applications/qml/generative_modeling/transformations/PIT.py @@ -15,13 +15,13 @@ import numpy as np import pandas as pd -from modules.applications.QML.generative_modeling.transformations.Transformation import * -from modules.circuits.CircuitCopula import CircuitCopula +from modules.applications.qml.generative_modeling.transformations.Transformation import Transformation +from modules.applications.qml.generative_modeling.circuits.CircuitCopula import CircuitCopula class PIT(Transformation): # pylint disable=R0902 """ - The transformation of the original probability distribution to + The transformation of the original probability distribution to the distribution of its uniformly distributed cumulative marginals is known as the copula. """ @@ -41,33 +41,24 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "pandas", - "version": "2.2.2" - } + {"name": "numpy", "version": "1.26.4"}, + {"name": "pandas", "version": "2.2.2"} ] def get_parameter_options(self) -> dict: """ - Returns empty dict as this transformation has no configurable settings + Returns empty dict as this transformation has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ return {} def get_default_submodule(self, option: str) -> CircuitCopula: - if option == "CircuitCopula": return CircuitCopula() else: @@ -78,13 +69,11 @@ def transform(self, input_data: dict, config: dict) -> dict: Transforms the input dataset using PIT transformation and computes histograms of the training dataset in the transformed space. - :param input_data: dataset - :type input_data: dict - :param config: config with the parameters specified in Config class - :type config: dict - :return: dict with PIT transformation, time it took to map it - :rtype: dict + :param input_data: Dataset + :param config: Config with the parameters specified in Config class + :return: Dict with PIT transformation, time it took to map it """ + # TODO: PIT.transform is almost identical to MinMax.transform -> function should be moved to Transformation.py self.dataset_name = input_data["dataset_name"] self.dataset = input_data["dataset"] self.n_qubits = input_data["n_qubits"] @@ -111,7 +100,7 @@ def transform(self, input_data: dict, config: dict) -> dict: value = 0 for count in histogram_transformed_1d: if count > 0: - solution_space[position:position+int(count)] = value + solution_space[position:position + int(count)] = value position += int(count) value += 1 @@ -145,10 +134,8 @@ def reverse_transform(self, input_data: dict) -> dict: """ Transforms the solution back to the representation needed for validation/evaluation. - :param input_data: dictionary containing the solution - :type input_data: dict - :return: dictionary with solution transformed accordingly - :rtype: dict + :param input_data: Dictionary containing the solution + :return: Dictionary with solution transformed accordingly """ depth = input_data["depth"] architecture_name = input_data["architecture_name"] @@ -202,14 +189,11 @@ def reverse_transform(self, input_data: dict) -> dict: def fit_transform(self, data: np.ndarray) -> np.ndarray: """ - Takes the data points and applies the PIT + Takes the data points and applies the PIT. - :param data: data samples - :type data: np.ndarray + :param data: Data samples :return: Transformed data points - :rtype: np.ndarray """ - df = pd.DataFrame(data) epit = df.copy(deep=True).transpose() self.reverse_epit_lookup = epit.copy(deep=True) @@ -224,18 +208,18 @@ def fit_transform(self, data: np.ndarray) -> np.ndarray: def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> list[float]: """ - Takes one data point and applies the inverse PIT + Takes one data point and applies the inverse PIT. - :param values: data point - :type values: np.ndarray + :param values: Data point :return: Data point after applying the inverse transformation - :rtype: list[float] """ values = values * (np.shape(self.reverse_epit_lookup)[1] - 1) rows = np.shape(self.reverse_epit_lookup)[0] + # if we are an integer do not use linear interpolation values_l = np.floor(values).astype(int) values_h = np.ceil(values).astype(int) + # if we are an integer then floor and ceiling are the same is_int_mask = 1 - (values_h - values_l) row_indexer = np.arange(rows) @@ -248,18 +232,22 @@ def _reverse_emp_integral_trans_single(self, values: np.ndarray) -> list[float]: def inverse_transform(self, data: np.ndarray) -> np.ndarray: """ - Applies the inverse transformation to the full data set + Applies the inverse transformation to the full data set. - :param data: data set - :type data: np.ndarray + :param data: Data set :return: Data set after applying the inverse transformation - :rtype: np.ndarray """ - res = [self._reverse_emp_integral_trans_single(row) for row in data] + return np.array(res)[:, 0, :] def emp_integral_trans(self, data: np.ndarray) -> np.ndarray: + """ + Applies the empirical integral transformation to the given data. + + :param data: Data points + :return: Empirically transformed data points + """ rank = np.argsort(data).argsort() length = data.size ecdf = np.linspace(0, 1, length, dtype=np.float64) diff --git a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py b/src/modules/applications/qml/generative_modeling/transformations/Transformation.py similarity index 69% rename from src/modules/applications/QML/generative_modeling/transformations/Transformation.py rename to src/modules/applications/qml/generative_modeling/transformations/Transformation.py index c53320ce..d76f4a2c 100644 --- a/src/modules/applications/QML/generative_modeling/transformations/Transformation.py +++ b/src/modules/applications/qml/generative_modeling/transformations/Transformation.py @@ -13,22 +13,23 @@ # limitations under the License. from itertools import product +from abc import ABC, abstractmethod import numpy as np -from modules.Core import * +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class Transformation(Core, ABC): """ - The task of the transformation module is to translate data and problem specification of the application into - preprocessed format. + The task of the transformation module is to translate data and problem + specification of the application into preprocessed format. """ def __init__(self, name): """ - Constructor method + Constructor method. """ super().__init__() self.transformation_name = name @@ -36,32 +37,21 @@ def __init__(self, name): @staticmethod def get_requirements() -> list[dict]: """ - Returns requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "numpy", - "version": "1.26.4" - } - ] + return [{"name": "numpy", "version": "1.26.4"}] def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[dict, float]: """ In this module, the preprocessing step is transforming the data to the correct target format. :param input_data: Collected information of the benchmarking process - :type input_data: dict :param config: Config specifying the parameters of the transformation - :type config: dict :param kwargs: Additional optional arguments - :type kwargs: dict - :return: tuple with transformed problem and the time it took to map it - :rtype: tuple[dict, float] + :return: Tuple with transformed problem and the time it took to map it """ - start = start_time_measurement() output = self.transform(input_data, config) @@ -69,37 +59,30 @@ def preprocess(self, input_data: dict, config: dict, **kwargs: dict) -> tuple[di def postprocess(self, input_data: dict, config: dict, **kwargs) -> tuple[dict, float]: """ - Does the reverse transformation + Does the reverse transformation. :param input_data: Dictionary containing information of previously executed modules - :type input_data: dict :param config: Dictionary containing additional information - :type config: dict :param kwargs: Dictionary containing additional information - :type kwargs: dict - :return: tuple with the dictionary and the time the postprocessing took - :rtype: tuple[dict, float] + :return: Tuple with the dictionary and the time the postprocessing took """ start = start_time_measurement() - output = self.reverse_transform(input_data) output["Transformation"] = True if "inference" in input_data: output["inference"] = input_data["inference"] + return output, end_time_measurement(start) @abstractmethod def transform(self, input_data: dict, config: dict) -> dict: """ - Helps to ensure that the model can effectively learn the underlying + Helps to ensure that the model can effectively learn the underlying patterns and structure of the data, and produce high-quality outputs. - :param input_data: Input data for transformation. - :type input_data: dict - :param config: Configuration parameters for the transformation. - :type config: dict + :param input_data: Input data for transformation + :param config: Configuration parameters for the transformation :return: Transformed data. - :rtype: dict """ return input_data @@ -109,10 +92,8 @@ def reverse_transform(self, input_data: dict) -> dict: This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. - :param input_data: The input data to be transformed. - :type input_data: dict - :return: Transformed data. - :rtype: dict + :param input_data: The input data to be transformed + :return: Transformed data """ return input_data @@ -121,12 +102,9 @@ def compute_discretization(n_qubits: int, n_registered: int) -> np.ndarray: """ Compute discretization for the grid. - :param n_qubits: Total number of qubits. - :type n_qubits: int - :param n_registered: Number of qubits to be registered. - :type n_registered: int - :return: Discretization data. - :rtype: np.ndarray + :param n_qubits: Total number of qubits + :param n_registered: Number of qubits to be registered + :return: Discretization data """ n = 2 ** (n_qubits // n_registered) n_bins = n ** n_registered @@ -143,12 +121,9 @@ def compute_discretization_efficient(n_qubits: int, n_registers: int) -> np.ndar """ Compute grid discretization. - :param n_qubits: Total number of qubits. - :type n_qubits: int - :param n_registers: Number of qubits to be registered. - :type n_registers: int - :return: Discretization data. - :rtype: np.ndarray + :param n_qubits: Total number of qubits + :param n_registers: Number of qubits to be registered + :return: Discretization data """ n = 2 ** (n_qubits // n_registers) n_bins = n ** n_registers @@ -169,16 +144,11 @@ def generate_samples(results: np.ndarray, bin_data: np.ndarray, n_registers: int """ Generate samples based on measurement results and the grid bins. - :param results: Results of measurements. - :type results: np.ndarray - :param bin_data: Binned data. - :type bin_data: np.ndarray - :param n_registers: Number of registers. - :type n_registers: int - :param noisy: Flag indicating whether to add noise. - :type noisy: bool, optional - :return: Generated samples. - :rtype: np.ndarray + :param results: Results of measurements + :param bin_data: Binned data + :param n_registers: Number of registers + :param noisy: Flag indicating whether to add noise + :return: Generated samples """ n_shots = np.sum(results) width = 1 / len(bin_data) ** (1 / n_registers) @@ -199,25 +169,23 @@ def generate_samples(results: np.ndarray, bin_data: np.ndarray, n_registers: int @staticmethod def generate_samples_efficient(results, bin_data: np.ndarray, n_registers: int, noisy: bool = True) -> np.ndarray: """ - Generate samples efficiently using numpy arrays based on measurement results and the grid bins + Generate samples efficiently using numpy arrays based on measurement results and the grid bins. - :param results: Results of measurements. - :type results: np.ndarray - :param bin_data: Binned data. - :type bin_data: np.ndarray - :param n_registers: Number of registers. - :type n_registers: int - :param noisy: Flag indicating whether to add noise. - :type noisy: bool, optional - :return: Generated samples. - :rtype: np.ndarray + :param results: Results of measurements + :param bin_data: Binned data + :param n_registers: Number of registers + :param noisy: Flag indicating whether to add noise + :return: Generated samples """ n_shots = np.sum(results) width = 1 / len(bin_data) ** (1 / n_registers) # Generate random noise or zeros - noise = 0.5 * width * np.random.uniform(low=-1, high=1, size=(n_shots, n_registers)) if noisy else np.zeros( - (n_shots, n_registers)) + noise = ( + 0.5 * width * np.random.uniform(low=-1, high=1, size=(n_shots, n_registers)) + if noisy + else np.zeros((n_shots, n_registers)) + ) # Create an array of bin_coords for each result, then stack them vertically bin_coords = bin_data[:, 1:] diff --git a/src/modules/applications/QML/generative_modeling/transformations/__init__.py b/src/modules/applications/qml/generative_modeling/transformations/__init__.py similarity index 100% rename from src/modules/applications/QML/generative_modeling/transformations/__init__.py rename to src/modules/applications/qml/generative_modeling/transformations/__init__.py diff --git a/src/modules/devices/Device.py b/src/modules/devices/Device.py index dcf71103..a7d1b76e 100644 --- a/src/modules/devices/Device.py +++ b/src/modules/devices/Device.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from modules.Core import * +from abc import ABC +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -23,7 +24,9 @@ class Device(Core, ABC): def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: Name of the device """ super().__init__(device_name) self.device = None @@ -32,10 +35,9 @@ def __init__(self, device_name: str): def get_parameter_options(self) -> dict: """ - Returns the parameters to fine-tune the device + Returns the parameters to fine-tune the device. Should always be in this format: - .. code-block:: json { @@ -46,42 +48,38 @@ def get_parameter_options(self) -> dict: } :return: Available device settings for this device - :rtype: dict """ return {} def set_config(self, config): + """ + Sets the device configuration. + + :param config: Configuration settings for the device + """ self.config = config - def preprocess(self, input_data, config, **kwargs): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - Returns instance of device class (self) and time it takes to call config + Returns instance of device class (self) and time it takes to call config. :param input_data: Input data (not used) - :type input_data: any :param config: Config for the device - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: Output and time needed - :rtype: (any, float) """ start = start_time_measurement() self.config = config return self, end_time_measurement(start) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - Returns input data and adds device name to the metrics class instance + Returns input data and adds device name to the metrics class instance. :param input_data: Input data passed by the parent module - :type input_data: any - :param config: solver config - :type config: dict + :param config: Solver config :param kwargs: Optional keyword arguments - :type kwargs: dict :return: Output and time needed - :rtype: (any, float) """ start = start_time_measurement() self.metrics.add_metric("device", self.get_device_name()) @@ -89,18 +87,16 @@ def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): def get_device(self) -> any: """ - Returns device + Returns device. :return: Instance of the device class - :rtype: any """ return self.device def get_device_name(self) -> str: """ - Returns device name + Returns the device name. :return: Name of the device - :rtype: str """ return self.device_name diff --git a/src/modules/devices/HelperClass.py b/src/modules/devices/HelperClass.py index 47e04502..ae4cf744 100644 --- a/src/modules/devices/HelperClass.py +++ b/src/modules/devices/HelperClass.py @@ -18,13 +18,16 @@ class HelperClass(Device): """ - Some Solvers like Pennylane only needs strings for setting up the device and not a standalone class + Some solvers like Pennylane, only needs strings for setting up the device and not a standalone class. + TODO: Maybe refactor this once we think of a better structure for this """ def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: The name of the device """ super().__init__(device_name=device_name) self.device = device_name @@ -32,14 +35,17 @@ def __init__(self, device_name: str): def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/Local.py b/src/modules/devices/Local.py index 2ec48d6e..ab2101ce 100644 --- a/src/modules/devices/Local.py +++ b/src/modules/devices/Local.py @@ -18,13 +18,12 @@ class Local(Device): """ - Some Solvers (often classical) also can run on a normal local environment without any specific device or - setting needed. + Some solvers (often classical) run on a local environment without any specific device or setting needed. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__(device_name="local") self.device = None @@ -32,14 +31,17 @@ def __init__(self): def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/SimulatedAnnealingSampler.py b/src/modules/devices/SimulatedAnnealingSampler.py index 8e43bb6f..79e929a3 100644 --- a/src/modules/devices/SimulatedAnnealingSampler.py +++ b/src/modules/devices/SimulatedAnnealingSampler.py @@ -20,12 +20,12 @@ class SimulatedAnnealingSampler(Device): """ - Class for D-Waves neal simulated annealer + Class for D-Waves neal simulated annealer. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__(device_name="simulated annealer") self.device = dwave.samplers.SimulatedAnnealingSampler() @@ -34,28 +34,25 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "dwave-samplers", - "version": "1.3.0" - } - ] + return [{"name": "dwave-samplers", "version": "1.3.0"}] def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/Braket.py b/src/modules/devices/braket/Braket.py index 98cfe745..eb068ea4 100644 --- a/src/modules/devices/braket/Braket.py +++ b/src/modules/devices/braket/Braket.py @@ -33,94 +33,124 @@ class Braket(Device, ABC): def __init__(self, device_name: str, region: str = None, arn: str = None): """ - Constructor method + Constructor method. """ super().__init__(device_name) self.device = None self.arn = arn self.s3_destination_folder = None + self.boto_session = None + self.aws_session = None if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return + if device_name != "LocalSimulator": - if 'HTTP_PROXY' in os.environ: - proxy_definitions = { - 'http': os.environ['HTTP_PROXY'], - 'https': os.environ['HTTP_PROXY'] - } - os.environ['HTTPS_PROXY'] = os.environ['HTTP_PROXY'] - else: - logging.warning( - 'No HTTP_PROXY was set as env variable! This might cause trouble if you are using a vpn') - proxy_definitions = None - - if region is not None: - pass - elif 'AWS_REGION' in os.environ: - region = os.environ['AWS_REGION'] - else: - region = 'us-east-1' - logging.info(f"No AWS_REGION specified, using default region: {region}") - logging.info(region) - my_config = Config( - region_name=region, - proxies=proxy_definitions - ) - if 'AWS_PROFILE' in os.environ: - profile_name = os.environ['AWS_PROFILE'] - elif "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" in os.environ: - logging.info("Assuming you are running on AWS container, getting credentials from " - "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") - profile_name = None + self._configure_aws_session(region) + + def _configure_aws_session(self, region: str) -> None: + """ + Configures the AWS session for the Braket device. + + :param region: AWS region to use + """ + proxy_definitions = self._setup_proxy() + region = self._set_region(region) + my_config = Config(region_name=region, proxies=proxy_definitions) + + profile_name = self._set_profile() + self._initialize_aws_session(profile_name, region, my_config) + + @staticmethod + def _setup_proxy() -> any: + """ + Sets up proxy configuration if available in the environment variables. + + :return: Proxy definitions + """ + if 'HTTP_PROXY' in os.environ: + proxy_definitions = { + 'http': os.environ['HTTP_PROXY'], + 'https': os.environ['HTTP_PROXY'] + } + os.environ['HTTPS_PROXY'] = os.environ['HTTP_PROXY'] + else: + logging.warning('No HTTP_PROXY set as an environment variable. ' + 'This might cause trouble if you are using a VPN.') + proxy_definitions = None + return proxy_definitions + + @staticmethod + def _set_region(region: str) -> str: + """ + Sets the AWS region from the environment variable or defaults to 'us-east-1'. + + :param region: Provided region + :return: Final region to be used + """ + if region is None: + region = os.environ.get('AWS_REGION', 'us-east-1') + logging.info(f"No AWS_REGION specified, using default region: {region}") + return region + + @staticmethod + def _set_profile() -> str: + """ + Determines the AWS profile to use for the session. + + :return: AWS profile name + """ + if 'AWS_PROFILE' in os.environ: + return os.environ['AWS_PROFILE'] + elif "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" in os.environ: + logging.info("Assuming AWS container environment, using container credentials.") + return None + else: + profile_name = 'quantum_computing' + os.environ['AWS_PROFILE'] = profile_name + logging.info(f"No AWS_PROFILE specified, using default profile: {profile_name}") + return profile_name + + def _initialize_aws_session(self, profile_name: str, region: str, my_config: Config) -> None: + """ + Initializes the AWS session for interacting with Amazon Braket. + + :param profile_name: AWS profile name + :param region: AWS region + :param my_config: Boto3 configuration + :raises Exception: If the AWS profile is not found + """ + try: + if profile_name is None: + self.boto_session = boto3.Session(region_name=region) else: - profile_name = 'quantum_computing' - os.environ['AWS_PROFILE'] = profile_name - logging.info(f"No AWS_PROFILE specified, using default profile: {profile_name}") - - try: - if profile_name is None: - self.boto_session = boto3.Session(region_name=region) - else: - self.boto_session = boto3.Session(profile_name=profile_name, region_name=region) - self.aws_session = AwsSession(boto_session=self.boto_session, config=my_config) - except ProfileNotFound as exc: - logging.error(f"AWS-Profile {profile_name} could not be found! Please set env-variable AWS_PROFILE. " - f"Only LocalSimulator is available.") - raise Exception("Please refer to logged error message.") from exc + self.boto_session = boto3.Session(profile_name=profile_name, region_name=region) + self.aws_session = AwsSession(boto_session=self.boto_session, config=my_config) + except ProfileNotFound as exc: + logging.error(f"AWS-Profile {profile_name} could not be found! Please set the AWS_PROFILE env variable. " + "Only LocalSimulator is available.") + raise Exception("Please refer to the logged error message.") from exc @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dictionaries with requirements """ return [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "botocore", - "version": "1.35.20" - }, - { - "name": "boto3", - "version": "1.35.20" - } + {"name": "amazon-braket-sdk", "version": "1.87.0"}, + {"name": "botocore", "version": "1.35.20"}, + {"name": "boto3", "version": "1.35.20"} ] def init_s3_storage(self, folder_name: str) -> None: """ - Calls function to create a s3 folder that is needed for Amazon Braket. + Initializes an S3 storage bucket for Amazon Braket. - :param folder_name: Name of the s3 folder - :type folder_name: str - :return: - :rtype: None + :param folder_name: Name of the s3 bucket """ run_timestamp = datetime.today().date() username = getpass.getuser() @@ -131,17 +161,22 @@ def init_s3_storage(self, folder_name: str) -> None: @staticmethod def _create_s3_bucket(boto3_session: boto3.Session, bucket_name: str = 'quark-benchmark-framework', - region: str = 'us-east-1'): + region: str = 'us-east-1') -> None: + """ + Creates an S3 bucket with specific configurations. + + :param boto3-session: Boto3 session + :param bucket_name: Name of the s3 bucket + :param region: AWS region + """ s3_client = boto3_session.client('s3', region_name=region) - # https://github.com/boto/boto3/issues/125 + if region == "us-east-1": s3_client.create_bucket(Bucket=bucket_name) else: location = {"LocationConstraint": region} - s3_client.create_bucket( - Bucket=bucket_name, - CreateBucketConfiguration=location - ) + s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) + s3_client.put_public_access_block( Bucket=bucket_name, PublicAccessBlockConfiguration={ diff --git a/src/modules/devices/braket/Ionq.py b/src/modules/devices/braket/Ionq.py index bfdb1b0d..8f87e687 100644 --- a/src/modules/devices/braket/Ionq.py +++ b/src/modules/devices/braket/Ionq.py @@ -12,40 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class Ionq(Braket): """ - Class for using the IonQ devices on Amazon Braket + Class for using the IonQ devices on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-east-1::device/qpu/ionq/Aria-1'): """ - Constructor method + Constructor method for initializing IonQ device on Amazon Braket. + + :param device_name: Name of the device + :param arn: Amazon resource name for the IonQ device """ super().__init__(region="us-east-1", device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("ionq") self.device = AwsDevice(arn, aws_session=self.aws_session) def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: An empty dictionary """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/LocalSimulator.py b/src/modules/devices/braket/LocalSimulator.py index 7d509040..12fe5a15 100644 --- a/src/modules/devices/braket/LocalSimulator.py +++ b/src/modules/devices/braket/LocalSimulator.py @@ -20,12 +20,14 @@ class LocalSimulator(Braket): """ - Class for using the local Amazon Braket simulator + Class for using the local Amazon Braket simulator. """ def __init__(self, device_name: str): """ - Constructor method + Constructor method for initializing the LocalSimulator class. + + :param device_name: Name of the device. """ super().__init__(device_name=device_name) self.device = LocalSimulatorBraket() @@ -33,14 +35,17 @@ def __init__(self, device_name: str): def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: An empty dictionary """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/OQC.py b/src/modules/devices/braket/OQC.py index 58bbe734..59e475e2 100644 --- a/src/modules/devices/braket/OQC.py +++ b/src/modules/devices/braket/OQC.py @@ -12,40 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class OQC(Braket): """ - Class for using the Oxford Quantum Circuits (OQC) devices on Amazon Braket + Class for using the Oxford Quantum Circuits (OQC) devices on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy'): """ - Constructor method + Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the OQC device """ super().__init__(region="eu-west-2", device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("oqc") self.device = AwsDevice(arn, aws_session=self.aws_session) def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns an empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/Rigetti.py b/src/modules/devices/braket/Rigetti.py index ae16a68d..032a16f4 100644 --- a/src/modules/devices/braket/Rigetti.py +++ b/src/modules/devices/braket/Rigetti.py @@ -12,40 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class Rigetti(Braket): """ - Class for using the Rigetti devices on Amazon Braket + Class for using the Rigetti devices on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:us-west-1::device/qpu/rigetti/Ankaa-2'): """ - Constructor method + Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the Rigetti device """ super().__init__(region="us-west-1", device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("rigetti") self.device = AwsDevice(arn, aws_session=self.aws_session) def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns an empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/SV1.py b/src/modules/devices/braket/SV1.py index 8099b20f..ee54b16c 100644 --- a/src/modules/devices/braket/SV1.py +++ b/src/modules/devices/braket/SV1.py @@ -12,40 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class SV1(Braket): """ - Class for using the SV1 simulator on Amazon Braket + Class for using the SV1 simulator on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum-simulator/amazon/sv1'): """ - Constructor method + Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the SV1 simulator """ super().__init__(device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("sv1") self.device = AwsDevice(arn, aws_session=self.aws_session) def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/braket/TN1.py b/src/modules/devices/braket/TN1.py index 3db4cd3b..c346b4d8 100644 --- a/src/modules/devices/braket/TN1.py +++ b/src/modules/devices/braket/TN1.py @@ -12,40 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from braket.aws import AwsDevice -from modules.devices.braket.Braket import * +from modules.devices.braket.Braket import Braket from modules.Core import Core class TN1(Braket): """ - Class for using the TN1 simulator on Amazon Braket + Class for using the TN1 simulator on Amazon Braket. """ def __init__(self, device_name: str, arn: str = 'arn:aws:braket:::device/quantum-simulator/amazon/tn1'): """ - Constructor method + Constructor method. + + :param device_name: Name of the device + :param arn: Amazon resource name for the TN1 simulator """ super().__init__(device_name=device_name, arn=arn) self.submodule_options = [] + if 'SKIP_INIT' in os.environ: # TODO: This is currently needed so create_module_db in the Installer does not execute the rest - # of this section, which would be unnecessary. However, this should be done better in the future! + # of this section, which would be unnecessary. However, this should be done better in the future! return + self.init_s3_storage("tn1") self.device = AwsDevice(arn, aws_session=self.aws_session) def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { + return {} - } + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. - def get_default_submodule(self, option: str) -> Core: + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/pulser/MockNeutralAtomDevice.py b/src/modules/devices/pulser/MockNeutralAtomDevice.py index 97d93cda..0397a796 100644 --- a/src/modules/devices/pulser/MockNeutralAtomDevice.py +++ b/src/modules/devices/pulser/MockNeutralAtomDevice.py @@ -24,12 +24,12 @@ class MockNeutralAtomDevice(Pulser): """ - Class for using the local mock Pulser simulator for neutral atom devices + Class for using the local mock Pulser simulator for neutral atom devices. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__(device_name="mock neutral atom device") self.device = MockDevice @@ -38,7 +38,9 @@ def __init__(self): def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this application + Returns the configurable settings for this application. + + :return: Configurable settings for the mock neutral atom device """ return { "doppler": { @@ -61,7 +63,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. """ doppler: bool amplitude: bool @@ -70,15 +72,20 @@ class Config(TypedDict): def get_backend_config(self) -> pulser.backend.config.EmulatorConfig: """ - Returns backend configurations + Returns backend configurations. - :return: backend config for the emulator - :rtype: pulser.backend.config.EmulatorConfig + :return: Backend config for the emulator """ noise_types = [key for key, value in self.config.items() if value] noise_model = pulser.backend.noise_model.NoiseModel(noise_types=noise_types) emulator_config = pulser.backend.config.EmulatorConfig(noise_model=noise_model) return emulator_config - def get_default_submodule(self, option: str) -> Core: + def get_default_submodule(self, option: str) -> None: + """ + Raises ValueError as this module has no submodules. + + :param option: Option name + :raises ValueError: If called, since this module has no submodules + """ raise ValueError("This module has no submodules.") diff --git a/src/modules/devices/pulser/Pulser.py b/src/modules/devices/pulser/Pulser.py index 950d167a..5edd43fe 100644 --- a/src/modules/devices/pulser/Pulser.py +++ b/src/modules/devices/pulser/Pulser.py @@ -24,7 +24,9 @@ class Pulser(Device, ABC): def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: Name of the Pulser device. """ super().__init__(device_name) self.device = None @@ -32,34 +34,26 @@ def __init__(self, device_name: str): def get_backend(self) -> any: """ - Returns backend + Returns backend. :return: Instance of the backend class - :rtype: any """ return self.backend @abstractmethod def get_backend_config(self) -> any: """ - Returns backend configurations + 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 requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "pulser", - "version": "0.19.0" - }, - ] + return [{"name": "pulser", "version": "0.19.0"}] diff --git a/src/modules/solvers/Annealer.py b/src/modules/solvers/Annealer.py index e9e5293f..ac08e540 100644 --- a/src/modules/solvers/Annealer.py +++ b/src/modules/solvers/Annealer.py @@ -13,8 +13,10 @@ # limitations under the License. from typing import TypedDict +import logging -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -25,12 +27,18 @@ class Annealer(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Simulated Annealer"] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Simulated Annealer": from modules.devices.SimulatedAnnealingSampler import SimulatedAnnealingSampler # pylint: disable=C0415 return SimulatedAnnealingSampler() @@ -39,17 +47,17 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this solver + Returns the configurable settings for this solver. - :return: - .. code-block:: python + :return: Dictionary of parameter options + .. code-block:: python - return { - "number_of_reads": { - "values": [100, 250, 500, 750, 1000], - "description": "How many reads do you need?" - } - } + return { + "number_of_reads": { + "values": [100, 250, 500, 750, 1000], + "description": "How many reads do you need?" + } + } """ return { @@ -70,56 +78,34 @@ class Config(TypedDict): """ number_of_reads: int - def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ - Annealing Solver. + Run the annealing solver. - :param mapped_problem: dictionary with the key 'Q' where its value should be the QUBO - :type mapped_problem: dict + :param mapped_problem: Dict with the key 'Q' where its value should be the QUBO :param device_wrapper: Annealing device - :type device_wrapper: any :param config: Annealing settings - :type config: Config - :param kwargs: - :type kwargs: any + :param kwargs: Additional keyword arguments :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - Q = mapped_problem['Q'] + q = mapped_problem['Q'] additional_solver_information = {} device = device_wrapper.get_device() start = start_time_measurement() + if device_wrapper.device_name != "simulated annealer": logging.error("Only simulated annealer available at the moment!") logging.error("Please select another solver module.") logging.error("The benchmarking run terminates with exception.") - # TODO: Check what to do with this.. - # This section was used to leverage the D-Wave devices previously available on Amazon Braket - - # Embed QUBO - # start_embedding = time() * 1000 - # __, target_edgelist, target_adjacency = device.structure - # emb = find_embedding(Q, target_edgelist, verbose=1) - # sampler = FixedEmbeddingComposite(device, emb) - # additional_solver_information["embedding_time"] = round(time() * 1000 - start_embedding, 3) - # - # additional_solver_information["logical_qubits"] = len(emb.keys()) - # additional_solver_information["physical_qubits"] = sum(len(chain) for chain in emb.values()) - # logging.info(f"Number of logical variables: {additional_solver_information['logical_qubits']}") - # logging.info(f"Number of physical qubits used in embedding: " - # f"{additional_solver_information['physical_qubits']}") - # - # response = sampler.sample_qubo(Q, num_reads=config['number_of_reads'], answer_mode="histogram") - # # Add timings https://docs.dwavesys.com/docs/latest/c_qpu_timing.html - # additional_solver_information.update(response.info["additionalMetadata"]["dwaveMetadata"]["timing"]) raise Exception("Please refer to the logged error message.") - response = device.sample_qubo(Q, num_reads=config['number_of_reads']) + + response = device.sample_qubo(q, num_reads=config['number_of_reads']) time_to_solve = end_time_measurement(start) - # take the result with the lowest energy: + # Take the result with the lowest energy: sample = response.lowest().first.sample - # logging.info("Result:" + str({k: v for k, v in sample.items() if v == 1})) logging.info(f'Annealing finished in {time_to_solve} ms.') return sample, time_to_solve, additional_solver_information diff --git a/src/modules/solvers/ClassicalSAT.py b/src/modules/solvers/ClassicalSAT.py index d4ae66e2..afc70f53 100644 --- a/src/modules/solvers/ClassicalSAT.py +++ b/src/modules/solvers/ClassicalSAT.py @@ -13,11 +13,13 @@ # limitations under the License. from typing import TypedDict +import logging from pysat.examples.rc2 import RC2 from pysat.formula import WCNF -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -28,7 +30,7 @@ class ClassicalSAT(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -36,19 +38,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "python-sat", - "version": "1.8.dev13" - } - ] + return [{"name": "python-sat", "version": "1.8.dev13"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -57,36 +59,28 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: dict) -> (list, float): + def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: dict) -> tuple[list, float, 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: WCNF + :param mapped_problem: Problem instance from the pysat library :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ logging.info( @@ -95,7 +89,7 @@ def run(self, mapped_problem: WCNF, device_wrapper: any, config: any, **kwargs: ) start = start_time_measurement() - # we use rc2 solver to compute the optimal solution + # We use rc2 solver to compute the optimal solution with RC2(mapped_problem) as rc2: sol = rc2.compute() diff --git a/src/modules/solvers/GreedyClassicalPVC.py b/src/modules/solvers/GreedyClassicalPVC.py index e31ec4e1..2520f1ab 100644 --- a/src/modules/solvers/GreedyClassicalPVC.py +++ b/src/modules/solvers/GreedyClassicalPVC.py @@ -14,9 +14,10 @@ from typing import TypedDict -import networkx +import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,7 +28,7 @@ class GreedyClassicalPVC(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -35,19 +36,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -56,63 +57,58 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: any, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the PVC graph in a greedy fashion. - :param mapped_problem: graph representing a PVC problem - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() - # We always start at the base node + # Start at the base node current_node = ((0, 0), 1, 1) idx = 1 tour = {current_node + (0,): 1} # Tour needs to cover all nodes, if there are 2 nodes left we can finish since these 2 nodes belong - # to the same seam while len(mapped_problem.nodes) > 2: # Get the minimum neighbor edge from the current node - next_node = min((x for x in mapped_problem.edges(current_node[0], data=True) if - x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2]), - key=lambda x: x[2]['weight']) + next_node = min( + ( + x for x in mapped_problem.edges(current_node[0], data=True) + if x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] + ), + key=lambda x: x[2]['weight']) + next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) - # Make the step - add distance to cost, add the best node to tour, + # Make the step - add distance to cost, add the best node to tour tour[next_node + (idx,)] = 1 # Remove all node of that seam to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) + current_node = next_node idx += 1 diff --git a/src/modules/solvers/GreedyClassicalTSP.py b/src/modules/solvers/GreedyClassicalTSP.py index e1028c01..42ffd0e8 100644 --- a/src/modules/solvers/GreedyClassicalTSP.py +++ b/src/modules/solvers/GreedyClassicalTSP.py @@ -14,10 +14,11 @@ from typing import TypedDict -import networkx +import networkx as nx from networkx.algorithms import approximation as approx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -28,7 +29,7 @@ class GreedyClassicalTSP(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -36,19 +37,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Returns requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -57,51 +58,40 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: any, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: any, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the TSP graph in a greedy fashion. - :param mapped_problem: graph representing a TSP - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a TSP :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Deep copy to ensure modification don't affect future repetitions mapped_problem = mapped_problem.copy() start = start_time_measurement() + # Use NetworkX approximation for a greedy TSP solution tour = approx.greedy_tsp(mapped_problem) - # We remove the duplicate node as we don't want a cycle - # https://stackoverflow.com/a/7961390/10456906 + # Remove the duplicate node as we don't want a cycle tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later - result = {} - for idx, node in enumerate(tour): - result[(node, idx)] = 1 - # Tour needs to look like + result = {(node, idx): 1 for idx, node in enumerate(tour)} + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/MIPsolverACL.py b/src/modules/solvers/MIPsolverACL.py index b3c43096..b6b93493 100644 --- a/src/modules/solvers/MIPsolverACL.py +++ b/src/modules/solvers/MIPsolverACL.py @@ -30,7 +30,8 @@ from typing import TypedDict import pulp -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -41,7 +42,7 @@ class MIPaclp(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -49,19 +50,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module + """ + return [{"name": "pulp", "version": "2.9.0"},] + + def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule """ - return [ - { - "name": "pulp", - "version": "2.9.0" - }, - ] - - def get_default_submodule(self, option: str) -> any: if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -70,35 +71,28 @@ def get_default_submodule(self, option: str) -> any: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ - Solve the ACL problem as a mixed integer problem (MIP) + Solve the ACL problem as a mixed integer problem (MIP). - :param mapped_problem: linear problem in form of a dictionary - :type mapped_problem: dict + :param mapped_problem: Linear problem in form of a dictionary :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(dict, float, dict) """ # Convert dict of problem instance to LP problem _, problem_instance = pulp.LpProblem.from_dict(mapped_problem) @@ -112,4 +106,5 @@ def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwarg for v in problem_instance.variables(): variables[v.name] = v.varValue solution_data["variables"] = variables + return solution_data, end_time_measurement(start), {} diff --git a/src/modules/solvers/NeutralAtomMIS.py b/src/modules/solvers/NeutralAtomMIS.py index a13205a7..b0d99d63 100644 --- a/src/modules/solvers/NeutralAtomMIS.py +++ b/src/modules/solvers/NeutralAtomMIS.py @@ -13,11 +13,13 @@ # limitations under the License. from typing import TypedDict +import logging import numpy as np import pulser -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -28,7 +30,7 @@ class NeutralAtomMIS(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["MockNeutralAtomDevice"] @@ -36,19 +38,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "pulser", - "version": "0.19.0" - } - ] + return [{"name": "pulser", "version": "0.19.0"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "MockNeutralAtomDevice": from modules.devices.pulser.MockNeutralAtomDevice import MockNeutralAtomDevice # pylint: disable=C0415 return MockNeutralAtomDevice() @@ -57,7 +59,9 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this solver + Returns the configurable settings for this solver. + + :return: Dictionary of configurable settings. """ return { "samples": { @@ -71,35 +75,29 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. samples (int): How many times to sample the final state from the quantum computer per measurement """ samples: int - def run(self, mapped_problem: dict, device_wrapper: any, config: any, **kwargs: dict) -> (list, float, dict): + def run(self, mapped_problem: dict, device_wrapper: any, config: any, **kwargs: dict) -> tuple[list, float, dict]: """ The given application is a problem instance from the pysat library. This uses the rc2 maxsat solver given in that library to return a solution. - :param mapped_problem: - :type mapped_problem: dict with graph and register + :param mapped_problem: Dictionary with graph and register :param device_wrapper: Device to run the problem on - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Solver Configuration + :param kwargs: Additional settings (not used) :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ register = mapped_problem.get('register') graph = mapped_problem.get('graph') nodes = list(graph.nodes()) edges = list(graph.edges()) - logging.info( - f"Got problem with {len(graph.nodes)} nodes, {len(graph.edges)} edges." - ) + + logging.info(f"Got problem with {len(graph.nodes)} nodes, {len(graph.edges)} edges.") device = device_wrapper.get_device() device.validate_register(register) @@ -123,10 +121,14 @@ def run(self, mapped_problem: dict, device_wrapper: any, config: any, **kwargs: return state_nodes, end_time_measurement(start), {} - def _create_sequence(self, register:pulser.Register, device:pulser.devices._device_datacls.Device) -> ( - pulser.Sequence): + def _create_sequence(self, register: pulser.Register, device: pulser.devices._device_datacls.Device) \ + -> pulser.Sequence: """ Creates a pulser sequence from a register and a device. + + :param register: The quantum register + :param device: The device being used + :return: The created sequence """ pulses = self._create_pulses(device) sequence = pulser.Sequence(register, device) @@ -135,7 +137,7 @@ def _create_sequence(self, register:pulser.Register, device:pulser.devices._devi sequence.add(pulse, "Rydberg global") return sequence - def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: + def _create_pulses(self, device: pulser.devices._device_datacls.Device) -> list[pulser.Pulse]: """ Creates pulses tuned to MIS problem. @@ -144,14 +146,17 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p We found this configuration in the documentation of the pulser documentation and it works for MIS. We are hesitant to make them parametrizable, because setting the wrong values will break your whole MIS. Though parameterization of pulses is a feature that we might implement in the future. + + :param device: The device being used + :return: List of pulses """ - Omega_max = 2.3 * 2 * np.pi + omega_max = 2.3 * 2 * np.pi delta_factor = 2 * np.pi channel = device.channels['rydberg_global'] max_amp = channel.max_amp - if max_amp is not None and max_amp < Omega_max: - Omega_max = max_amp + if max_amp is not None and max_amp < omega_max: + omega_max = max_amp delta_0 = -3 * delta_factor delta_f = 1 * delta_factor @@ -161,13 +166,13 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p t_sweep = (delta_f - delta_0) / (2 * np.pi * 10) * 5000 rise = pulser.Pulse.ConstantDetuning( - pulser.waveforms.RampWaveform(t_rise, 0.0, Omega_max), delta_0, 0.0 + pulser.waveforms.RampWaveform(t_rise, 0.0, omega_max), delta_0, 0.0 ) sweep = pulser.Pulse.ConstantAmplitude( - Omega_max, pulser.waveforms.RampWaveform(t_sweep, delta_0, delta_f), 0.0 + omega_max, pulser.waveforms.RampWaveform(t_sweep, delta_0, delta_f), 0.0 ) fall = pulser.Pulse.ConstantDetuning( - pulser.waveforms.RampWaveform(t_fall, Omega_max, 0.0), delta_f, 0.0 + pulser.waveforms.RampWaveform(t_fall, omega_max, 0.0), delta_f, 0.0 ) pulses = [rise, sweep, fall] @@ -176,7 +181,15 @@ def _create_pulses(self, device:pulser.devices._device_datacls.Device) -> list[p return pulses - def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> dict: + def _filter_invalid_states(self, state_counts: dict, nodes: list, edges: list) -> dict: + """ + Filters out invalid states that do not meet the problem constraints. + + :param state_counts: Counts of each sampled data + :param nodes: List of nodes in the graph + :param edges: List of edges in the graph + :return: Dictionary of valid state counts + """ valid_state_counts = {} for state, count in state_counts.items(): selected_nodes = self._translate_state_to_nodes(state, nodes) @@ -191,16 +204,31 @@ def _filter_invalid_states(self, state_counts:dict, nodes:list, edges:list) -> d return valid_state_counts - def _translate_state_to_nodes(self, state:str, nodes:list) -> list: + def _translate_state_to_nodes(self, state: str, nodes: list) -> list: + """ + Translates a state string into the corresponding list of nodes. + + :param state: State string + :param nodes: List of nodes + :return: List of nodes corresponding to the states + """ return [key for index, key in enumerate(nodes) if state[index] == '1'] - def _select_best_state(self, states:dict, nodes=list) -> str: + def _select_best_state(self, states: dict, nodes: list) -> str: + """ + Selects the best state from the available valid states. + + :param states: Dictionary of valid states and their counts + :param nodes: List of nodes + :return: The best state as a string + """ # TODO: Implement the samplers try: best_state = max(states, key=lambda k: states[k]) - except: # pylint: disable=W0702 + except Exception: # pylint: disable=W0702 # TODO: Specify error - # TODO: Clean up this monstrocity + # TODO: Clean this up n_nodes = len(nodes) best_state = "0" * n_nodes + return best_state diff --git a/src/modules/solvers/PennylaneQAOA.py b/src/modules/solvers/PennylaneQAOA.py index 71140ef9..27f5d311 100644 --- a/src/modules/solvers/PennylaneQAOA.py +++ b/src/modules/solvers/PennylaneQAOA.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import ast import inspect import json @@ -26,7 +27,8 @@ import pennylane as qml from pennylane import numpy as npqml -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -37,49 +39,44 @@ class PennylaneQAOA(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() - self.submodule_options = ["arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", - "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", - "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", - "braket.local.qubit", - "default.qubit", - "default.qubit.autograd", - "qulacs.simulator", - "lightning.gpu", - "lightning.qubit"] + self.submodule_options = [ + "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3", + "arn:aws:braket:eu-west-2::device/qpu/oqc/Lucy", + "braket.local.qubit", + "default.qubit", + "default.qubit.autograd", + "qulacs.simulator", + "lightning.gpu", + "lightning.qubit" + ] @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "pennylane", - "version": "0.37.0" - }, - { - "name": "pennylane-lightning", - "version": "0.38.0" - }, - { - "name": "amazon-braket-pennylane-plugin", - "version": "1.30.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "pennylane", "version": "0.37.0"}, + {"name": "pennylane-lightning", "version": "0.38.0"}, + {"name": "amazon-braket-pennylane-plugin", "version": "1.30.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony": from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 @@ -119,33 +116,33 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this solver - - :return: - .. code-block:: python - - return { - "shots": { # number measurements to make on circuit - "values": list(range(10, 500, 30)), - "description": "How many shots do you need?" - }, - "iterations": { # number measurements to make on circuit - "values": [1, 10, 20, 50, 75], - "description": "How many iterations do you need?" - }, - "layers": { - "values": [2, 3, 4], - "description": "How many layers for QAOA do you want?" - }, - "coeff_scale": { - "values": [0.01, 0.1, 1, 10], - "description": "How do you want to scale your coefficients?" - }, - "stepsize": { - "values": [0.0001, 0.001, 0.01, 0.1, 1], - "description": "Which stepsize do you want?" - } - } + Returns the configurable settings for this solver. + + :return: Dictionary of configuration settings + .. code-block:: python + + return { + "shots": { # number measurements to make on circuit + "values": list(range(10, 500, 30)), + "description": "How many shots do you need?" + }, + "iterations": { # number measurements to make on circuit + "values": [1, 10, 20, 50, 75], + "description": "How many iterations do you need?" + }, + "layers": { + "values": [2, 3, 4], + "description": "How many layers for QAOA do you want?" + }, + "coeff_scale": { + "values": [0.01, 0.1, 1, 10], + "description": "How do you want to scale your coefficients?" + }, + "stepsize": { + "values": [0.0001, 0.001, 0.01, 0.1, 1], + "description": "Which stepsize do you want?" + } + } """ return { @@ -173,7 +170,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -197,71 +194,58 @@ def normalize_data(data: any, scale: float = 1.0) -> any: """ Not used currently, as I just scale the coefficients in the qaoa_operators_from_ising. - :param data: - :type data: any - :param scale: - :type scale: float + :param data: Data to normalize + :param scale: Scaling factor :return: Normalized data - :rtype: any """ return scale * data / np.max(np.abs(data)) @staticmethod - def qaoa_operators_from_ising(J: any, t: any, scale: float = 1.0) -> (any, any): + def qaoa_operators_from_ising(j: any, t: any, scale: float = 1.0) -> tuple[any, any]: """ - Generates pennylane cost and mixer hamiltonians from the Ising matrix J and vector t. + Generates pennylane cost and mixer Hamiltonians from the Ising matrix J and vector t. - :param J: J matrix - :type J: any + :param j: J matrix :param t: t vector - :type t: any - :param scale: - :type scale: float - :return: - :rtype: tuple(any, any) + :param scale: Scaling factor + :return: Cost Hamiltonian and mixer Hamiltonian """ - # we define the scaling factor as scale * the maximum parameter found in the coefficients - scaling_factor = scale * max(np.max(np.abs(J.flatten())), np.max(np.abs(t))) - # we scale the coefficients - J /= scaling_factor + # Define the scaling factor + scaling_factor = scale * max(np.max(np.abs(j.flatten())), np.max(np.abs(t))) + + # Scale the coefficients + j /= scaling_factor t /= scaling_factor sigzsigz_arr = [ - qml.PauliZ(i) @ qml.PauliZ(j) for i in range(len(J)) - for j in range(len(J)) - ] + qml.PauliZ(i) @ qml.PauliZ(k) for i in range(len(j)) for k in range(len(j)) + ] sigz_arr = [qml.PauliZ(i) for i in range(len(t))] - J_real = np.real(J.flatten()) + j_real = np.real(j.flatten()) t_real = np.real(t) - h_cost = qml.simplify(qml.Hamiltonian([*t_real, *J_real.flatten()], [*sigz_arr, *sigzsigz_arr])) + h_cost = qml.simplify(qml.Hamiltonian([*t_real, *j_real.flatten()], [*sigz_arr, *sigzsigz_arr])) - # definition of the mixer hamiltonian - h_mixer = -1 * qml.qaoa.mixers.x_mixer(range(len(J))) + # Definition of the mixer hamiltonian + h_mixer = -1 * qml.qaoa.mixers.x_mixer(range(len(j))) return h_cost, h_mixer # pylint: disable=R0915 - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> (any, any, float): + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float, dict]: """ Runs Pennylane QAOA on the Ising problem. - :param mapped_problem: Ising - :type mapped_problem: any - :param device_wrapper: - :type device_wrapper: any - :param config: - :type config: Config - :param kwargs: contains store_dir for the plot of the optimization - :type kwargs: any + :param mapped_problem: Dict containing problem parameters mapped to the Ising model + :param device_wrapper: Device to run the problem on + :param config: QAOA solver settings + :param kwargs: Contains store_dir for the plot of the optimization :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - - J = mapped_problem['J'] + j = mapped_problem['J'] t = mapped_problem['t'] - wires = J.shape[0] - cost_h, mixer_h = self.qaoa_operators_from_ising(J, t, scale=config['coeff_scale']) + wires = j.shape[0] + cost_h, mixer_h = self.qaoa_operators_from_ising(j, t, scale=config['coeff_scale']) # set up the problem try: @@ -291,13 +275,13 @@ def circuit(params, **kwargs): if device_wrapper.device == 'qulacs.simulator': dev = qml.device(device_wrapper.device, wires=wires, shots=config['shots'], gpu=True) elif device_wrapper.device in ['lightning.qubit', 'lightning.gpu']: - # no number shots as diff method will be adjoint backprop for these devices + # No number shots as diff method will be adjoint backprop for these devices if diff_method in ["adjoint", "backprop"]: dev = qml.device(device_wrapper.device, wires=wires, shots=None, batch_obs=True) else: dev = qml.device(device_wrapper.device, wires=wires, batch_obs=True, shots=config['shots']) elif device_wrapper.device == 'default.qubit': - # no number shots as diff method will be adjoint backprop for these devices + # No number shots as diff method will be adjoint backprop for these devices if diff_method in ["adjoint", "backprop"]: dev = qml.device(device_wrapper.device, shots=None, wires=wires) else: @@ -351,19 +335,14 @@ def cost_function(params): # Optimization Loop optimizer = qml.GradientDescentOptimizer(stepsize=config['stepsize']) - # optimizer = qml.MomentumOptimizer(stepsize=config['stepsize'], momentum=0.9) - # optimizer = qml.QNSPSAOptimizer(stepsize=config['stepsize']) logging.info(f"Device: {device_wrapper.device}, Optimizer {optimizer}, Differentiation: {diff_method}, " f"Optimization start...") additional_solver_information = {} - min_param = None - min_cost = None - cost_pt = [] - params_list = [] - x = [] + min_param, min_cost, cost_pt, params_list, x = None, None, [], [], [] run_id = round(time()) start = start_time_measurement() + for iteration in range(config['iterations']): t0 = start_time_measurement() # Evaluates the cost, then does a gradient step to new params @@ -375,12 +354,14 @@ def cost_function(params): logging.error(e) logging.error("Run a smaller problem size or select another device.") raise e + # Convert cost_before to a float, so it's easier to handle cost_before = float(cost_before) if iteration == 0: logging.info(f"Initial cost: {cost_before}") else: logging.info(f"Cost at step {iteration}: {cost_before}") + # Log the current loss as a metric logging.info(f"Time to complete iteration {iteration + 1}: {end_time_measurement(t0)}") cost_pt.append(cost_before) @@ -403,7 +384,6 @@ def cost_function(params): plt.clf() params = min_param - logging.info(f"Final params: {params}") logging.info(f"Final costs: {min_cost}") @@ -435,7 +415,6 @@ def evaluate_params_probs(params): best_bitstring = max(probs, key=probs.get) return best_bitstring, probs - best_bitstring, probs = None, None if config['shots'] is None or diff_method in ["backprop", "adjoint"]: best_bitstring, probs = evaluate_params_probs(params) else: @@ -458,7 +437,7 @@ def evaluate_params_probs(params): bitstring_list.append(bitstring) # Save the cost, best bitstring, variational parameters per iteration as well as the final prob. distribution - # TODO: Maybe this can be done more efficient, e.g. only saving the circuit and its weights? + # TODO: Maybe this can be done more efficient, e.g., only saving the circuit and its weights? json_data = { 'cost': cost_pt, 'bitstrings': bitstring_list, @@ -474,27 +453,24 @@ def evaluate_params_probs(params): def monkey_init_array(self): """ - Here we create the timings array where we later append the quantum timings - :param self: - :return: + Here we create the timings array where we later append the quantum timings. """ self.timings = [] def _pseudo_decor(fun, device): """ - Massive shoutout to this guy: https://stackoverflow.com/a/25827070/10456906 - We use this decorator for measuring execute and batch_execute + We use this decorator for measuring execute and batch_execute. """ - # magic sauce to lift the name and doc of the function + # Lift the name and doc of the function @wraps(fun) def ret_fun(*args, **kwargs): - # pre function execution stuff here + # Pre function execution here from time import time # pylint: disable=W0621 disable=C0415 disable=W0404 start_timing = time() * 1000 returned_value = fun(*args, **kwargs) - # post execution stuff here + # Post execution here device.timings.append(round(time() * 1000 - start_timing, 3)) return returned_value diff --git a/src/modules/solvers/QAOA.py b/src/modules/solvers/QAOA.py index 78bcc0c1..d6f44712 100644 --- a/src/modules/solvers/QAOA.py +++ b/src/modules/solvers/QAOA.py @@ -14,12 +14,15 @@ from time import sleep from typing import TypedDict +import logging import numpy as np from braket.circuits import Circuit +from braket.aws import AwsDevice from scipy.optimize import minimize -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -30,38 +33,36 @@ class QAOA(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() - self.submodule_options = ["LocalSimulator", "arn:aws:braket:::device/quantum-simulator/amazon/sv1", - "arn:aws:braket:::device/quantum-simulator/amazon/tn1", - "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", - "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3"] + self.submodule_options = [ + "LocalSimulator", "arn:aws:braket:::device/quantum-simulator/amazon/sv1", + "arn:aws:braket:::device/quantum-simulator/amazon/tn1", + "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony", + "arn:aws:braket:us-west-1::device/qpu/rigetti/Aspen-M-3" + ] @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "amazon-braket-sdk", - "version": "1.87.0" - }, - { - "name": "scipy", - "version": "1.12.0" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "amazon-braket-sdk", "version": "1.87.0"}, + {"name": "scipy", "version": "1.12.0"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "arn:aws:braket:us-east-1::device/qpu/ionq/Harmony": from modules.devices.braket.Ionq import Ionq # pylint: disable=C0415 @@ -83,29 +84,29 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns the configurable settings for this solver - - :return: - .. code-block:: python - - return { - "shots": { # number measurements to make on circuit - "values": list(range(10, 500, 30)), - "description": "How many shots do you need?" - }, - "opt_method": { - "values": ["Powell", "Nelder-Mead"], - "description": "Which optimization method do you want?" - }, - "depth": { - "values": [3], - "description": "Which circuit depth for QAOA do you want?" - } - } + Returns the configurable settings for this solver. + + :return: Dictionary of parameter settings + .. code-block:: python + + return { + "shots": { # number measurements to make on circuit + "values": list(range(10, 500, 30)), + "description": "How many shots do you need?" + }, + "opt_method": { + "values": ["Powell", "Nelder-Mead"], + "description": "Which optimization method do you want?" + }, + "depth": { + "values": [3], + "description": "Which circuit depth for QAOA do you want?" + } + } """ return { - "shots": { # number measurements to make on circuit + "shots": { # number of measurements to make on circuit "values": list(range(10, 500, 30)), "description": "How many shots do you need?" }, @@ -121,7 +122,7 @@ def get_parameter_options(self) -> dict: class Config(TypedDict): """ - Attributes of a valid config + Attributes of a valid config. .. code-block:: python @@ -134,22 +135,16 @@ class Config(TypedDict): opt_method: str depth: int - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> (any, float): + def run(self, mapped_problem: dict, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float, dict]: """ Run QAOA algorithm on Ising. - :param mapped_problem: dictionary with the keys 'J' and 't' - :type mapped_problem: any - :param device_wrapper: instance of device - :type device_wrapper: any - :param config: - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param mapped_problem: Dict containing problem parameters mapped to the Ising model + :param device_wrapper: Instance of device + :param config: Solver configuration settings + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - j = mapped_problem['J'] if np.any(np.iscomplex(j)): logging.warning("The problem matrix of the QAOA solver contains imaginary numbers." @@ -157,18 +152,18 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs else: j = np.real(j) - # set up the problem + # Set up the problem n_qubits = j.shape[0] # User-defined hypers - depth = config['depth'] # circuit depth for QAOA + depth = config['depth'] opt_method = config['opt_method'] # SLSQP, COBYLA, Nelder-Mead, BFGS, Powell, ... - # initialize reference solution (simple guess) + # Initialize reference solution (simple guess) bitstring_init = -1 * np.ones([n_qubits]) energy_init = np.dot(bitstring_init, np.dot(j, bitstring_init)) - # set tracker to keep track of results + # Set tracker to keep track of results tracker = { 'count': 1, # Elapsed optimization steps 'optimal_energy': energy_init, # Global optimal energy @@ -181,37 +176,36 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs 'params': [] # Track parameters } - # set options for classical optimization + # Set options for classical optimization options = {'disp': True, 'maxiter': 100} # options = {'disp': True, 'ftol': 1e-08, 'maxiter': 100, 'maxfev': 50} # example options ################################################################################## - # run QAOA optimization on graph + # Run QAOA optimization on graph ################################################################################## logging.info(f"Circuit depth hyperparameter:{depth}") logging.info(f"Problem size:{n_qubits}") - # kick off training + # Kick off training start = start_time_measurement() - # result_energy, result_angle, tracker _, _, tracker = train( - device=device_wrapper.get_device(), options=options, p=depth, ising=j, n_qubits=n_qubits, + device=device_wrapper.get_device(), + options=options, + p=depth, ising=j, + n_qubits=n_qubits, n_shots=config['shots'], - opt_method=opt_method, tracker=tracker, s3_folder=device_wrapper.s3_destination_folder, verbose=True) + opt_method=opt_method, + tracker=tracker, + s3_folder=device_wrapper.s3_destination_folder, + verbose=True + ) time_to_solve = end_time_measurement(start) - # print execution time - # logging.info('Code execution time [sec]: ' + (end - start)) - - # print optimized results + # Log optimized results logging.info(f"Optimal energy: {tracker['optimal_energy']}") logging.info(f"Optimal classical bitstring: {tracker['optimal_bitstring']}") - # visualize the optimization process - # cycles = np.arange(1, tracker['count']) - # optim_classical = tracker['global_energies'] - # TODO maybe save this plot # plt.plot(cycles, optim_classical) # plt.xlabel('optimization cycle') @@ -224,30 +218,38 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs # QAOA utils (source: # https://github.com/aws/amazon-braket-examples/blob/main/examples/hybrid_quantum_algorithms/QAOA/utils_qaoa.py) -# function to implement ZZ gate using CNOT gates -def ZZgate(q1, q2, gamma): - """ - function that returns a circuit implementing exp(-i \\gamma Z_i Z_j) using CNOT gates if ZZ not supported +# Function to implement ZZ gate using CNOT gates +def zz_gate(q1: any, q2: any, gamma: float) -> Circuit: """ + Function that returns a circuit implementing exp(-i \\gamma Z_i Z_j) using CNOT gates if ZZ not supported. - # get a circuit + :param q1: Qubit 1 (control) + :param q2: Qubit 2 (target) + :param gamma: Gamma parameter (angle) + :return: ZZ gate + """ + # Get a circuit circ_zz = Circuit() - # construct decomposition of ZZ + # Construct decomposition of ZZ circ_zz.cnot(q1, q2).rz(q2, gamma).cnot(q1, q2) return circ_zz -# function to implement evolution with driver Hamiltonian -def driver(beta, n_qubits): +# Function to implement evolution with driver Hamiltonian +def driver(beta: float, n_qubits: int) -> Circuit: """ - Returns circuit for driver Hamiltonian U(Hb, beta) + Returns circuit for driver Hamiltonian U(Hb, beta). + + :param beta: Beta parameter (angle) + :param n_qubits: Number of qubits + :return: Circuit with rotated qubits """ - # instantiate circuit object + # Instantiate circuit object circ = Circuit() - # apply parametrized rotation around x to every qubit + # Apply parametrized rotation around x to every qubit for qubit in range(n_qubits): gate = Circuit().rx(qubit, 2 * beta) circ.add(gate) @@ -255,126 +257,142 @@ def driver(beta, n_qubits): return circ -# helper function for evolution with cost Hamiltonian -def cost_circuit(gamma, n_qubits, ising, device): +# Helper function for evolution with cost Hamiltonian +def cost_circuit(gamma: float, ising: np.ndarray, device: AwsDevice) -> Circuit: """ - returns circuit for evolution with cost Hamiltonian + Returns circuit for evolution with cost Hamiltonian. + + :param gamma: Gamma parameter (angle) + :param ising: Ising matrix + :param device: Device to run the circuit on + :return: Circuit representing the cost Hamiltonian """ - # instantiate circuit object + # Instantiate circuit object circ = Circuit() - # get all non-zero entries (edges) from Ising matrix + # Get all non-zero entries (edges) from Ising matrix idx = ising.nonzero() edges = list(zip(idx[0], idx[1])) - # apply ZZ gate for every edge (with corresponding interaction strength) + # Apply ZZ gate for every edge (with corresponding interaction strength) for qubit_pair in edges: - # get interaction strength from Ising matrix + # Get interaction strength from Ising matrix int_strength = ising[qubit_pair[0], qubit_pair[1]] - # for Rigetti we decompose ZZ using CNOT gates + # For Rigetti we decompose ZZ using CNOT gates if device.name in ["Rigetti", "Aspen-9"]: # TODO make this more flexible - gate = ZZgate(qubit_pair[0], qubit_pair[1], gamma * int_strength) - circ.add(gate) - # classical simulators and IonQ support ZZ gate + gate = zz_gate(qubit_pair[0], qubit_pair[1], gamma * int_strength) + # Classical simulators and IonQ support ZZ gate else: gate = Circuit().zz(qubit_pair[0], qubit_pair[1], angle=2 * gamma * int_strength) - circ.add(gate) + circ.add(gate) return circ -# function to build the QAOA circuit with depth p -def circuit(params, device, n_qubits, ising): +# Function to build the QAOA circuit with depth p +def circuit(params: np.array, device: AwsDevice, n_qubits: int, ising: np.ndarray) -> Circuit: """ - function to return full QAOA circuit; depends on device as ZZ implementation depends on gate set of backend + Function to return the full QAOA circuit; depends on device as ZZ implementation depends on gate set of backend. + + :param params: Array containing the beta and gamma parameters + :param device: Device to run the circuit on + :param n_qubits: Number of qubits + :param ising: Ising matrix + :return: QAOA Circuit """ - # initialize qaoa circuit with first Hadamard layer: for minimization start in |-> + # Initialize QAOA circuit with first Hadamard layer circ = Circuit() - X_on_all = Circuit().x(range(0, n_qubits)) - circ.add(X_on_all) - H_on_all = Circuit().h(range(0, n_qubits)) - circ.add(H_on_all) + x_on_all = Circuit().x(range(0, n_qubits)) + circ.add(x_on_all) + h_on_all = Circuit().h(range(0, n_qubits)) + circ.add(h_on_all) - # setup two parameter families + # Setup two parameter families circuit_length = int(len(params) / 2) gammas = params[:circuit_length] betas = params[circuit_length:] - # add QAOA circuit layer blocks + # Add QAOA circuit layer blocks for mm in range(circuit_length): - circ.add(cost_circuit(gammas[mm], n_qubits, ising, device)) + circ.add(cost_circuit(gammas[mm], ising, device)) circ.add(driver(betas[mm], n_qubits)) return circ -# function that computes cost function for given params +# Function that computes cost function for given params # pylint: disable=R0917 # pylint: disable=R0913 -def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_folder, verbose): +def objective_function(params: np.array, device: AwsDevice, ising: np.ndarray, n_qubits: int, n_shots: int, + tracker: dict, s3_folder: tuple[str, str], verbose: bool) -> float: """ - objective function takes a list of variational parameters as input, - and returns the cost associated with those parameters + Objective function takes a list of variational parameters as input, + and returns the cost associated with those parameters. + + :param params: Array containing beta and gamma parameters + :param device: Device to run the circuit on + :param ising: Ising matrix + :param n_qubits: Number of qubits + :param n_shots: Number of measurements to make on the circuit + :param tracker: Keeps track of the runs on the circuit + :param s3_folder: AWS S3 bucket + :param verbose: Controls degree of detail in logs + :return: Energy expectation value """ if verbose: logging.info("==================================" * 2) logging.info(f"Calling the quantum circuit. Cycle: {tracker['count']}") - # get a quantum circuit instance from the parameters + # Get a quantum circuit instance from the parameters qaoa_circuit = circuit(params, device, n_qubits, ising) - # classically simulate the circuit - # execute the correct device.run call depending on whether the backend is local or cloud based + # Classically simulate the circuit + # Execute the correct device.run call depending on whether the backend is local or cloud based if device.name in ["DefaultSimulator", "StateVectorSimulator"]: task = device.run(qaoa_circuit, shots=n_shots) else: - task = device.run( - qaoa_circuit, s3_folder, shots=n_shots, poll_timeout_seconds=3 * 24 * 60 * 60 - ) + task = device.run(qaoa_circuit, s3_folder, shots=n_shots, poll_timeout_seconds=3 * 24 * 60 * 60) - # get ID and status of submitted task + # Get ID and status of submitted task task_id = task.id status = task.state() logging.info(f"ID of task: {task_id}") logging.info(f"Status of task: {status}") - # wait for job to complete + + # Wait for job to complete while status != 'COMPLETED': status = task.state() logging.info(f"Status: {status}") sleep(10) - # get result for this task + # Get result for this task result = task.result() logging.info(result) - # get metadata - # metadata = result.task_metadata - - # convert results (0 and 1) to ising (-1 and 1) + # Convert results (0 and 1) to ising (-1 and 1) meas_ising = result.measurements meas_ising[meas_ising == 0] = -1 - # get all energies (for every shot): (n_shots, 1) vector + # Get all energies (for every shot): (n_shots, 1) vector all_energies = np.diag(np.dot(meas_ising, np.dot(ising, np.transpose(meas_ising)))) - # find minimum and corresponding classical string + # Find minimum and corresponding classical string energy_min = np.min(all_energies) tracker["opt_energies"].append(energy_min) optimal_string = meas_ising[np.argmin(all_energies)] tracker["opt_bitstrings"].append(optimal_string) logging.info(tracker["optimal_energy"]) - # store optimal (classical) result/bitstring + # Store optimal (classical) result/bitstring if energy_min < tracker["optimal_energy"]: - tracker.update({"optimal_energy": energy_min}) - tracker.update({"optimal_bitstring": optimal_string}) + tracker.update({"optimal_energy": energy_min, "optimal_bitstring": optimal_string}) - # store global minimum + # Store global minimum tracker["global_energies"].append(tracker["optimal_energy"]) - # energy expectation value + # Energy expectation value energy_expect = np.sum(all_energies) / n_shots if verbose: @@ -382,7 +400,7 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol logging.info(f"Optimal classical string: {optimal_string}") logging.info(f"Energy expectation value (cost): {energy_expect}") - # update tracker + # Update tracker tracker.update({"count": tracker["count"] + 1, "res": result}) tracker["costs"].append(energy_expect) tracker["params"].append(params) @@ -392,12 +410,24 @@ def objective_function(params, device, ising, n_qubits, n_shots, tracker, s3_fol # The function to execute the training: run classical minimization. # pylint: disable=R0917 -def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_folder, verbose=True): +def train(device: AwsDevice, options: dict, p: int, ising: np.ndarray, n_qubits: int, n_shots: int, opt_method: str, + tracker: dict, s3_folder: tuple[str, str], verbose: bool = True) -> tuple[float, np.ndarray, dict]: """ - function to run QAOA algorithm for given, fixed circuit depth p + Function to run QAOA algorithm for given, fixed circuit depth p. + + :param device: Device to run the circuit on + :param options: Dict containing parameters of classical part of the QAOA + :param p: Circuit depth + :param ising: Ising matrix + :param n_qubits: Number of qubits + :param n_shots: Number of measurements to make on the circuit + :param opt_method: Controls degree of detail in logs + :param tracker: Keeps track of the runs on the circuit + :param s3_folder: AWS S3 bucket + :param verbose: Controls degree of detail in logs + :return: Results of the training as a tuple of the energy, the angle and the tracker """ logging.info("Starting the training.") - logging.info("==================================" * 2) logging.info(f"OPTIMIZATION for circuit depth p={p}") @@ -405,22 +435,22 @@ def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_ logging.info('Param "verbose" set to False. Will not print intermediate steps.') logging.info("==================================" * 2) - # initialize + # Initialize cost_energy = [] - # randomly initialize variational parameters within appropriate bounds + # Randomly initialize variational parameters within appropriate bounds gamma_initial = np.random.uniform(0, 2 * np.pi, p).tolist() beta_initial = np.random.uniform(0, np.pi, p).tolist() params0 = np.array(gamma_initial + beta_initial) - # set bounds for search space + # Set bounds for search space bnds_gamma = [(0, 2 * np.pi) for _ in range(int(len(params0) / 2))] bnds_beta = [(0, np.pi) for _ in range(int(len(params0) / 2))] bnds = bnds_gamma + bnds_beta tracker["params"].append(params0) - # run classical optimization (example: method='Nelder-Mead') + # Run classical optimization (example: method='Nelder-Mead') try: result = minimize( objective_function, @@ -435,7 +465,7 @@ def train(device, options, p, ising, n_qubits, n_shots, opt_method, tracker, s3_ logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") from e - # store result of classical optimization + # Store result of classical optimization result_energy = result.fun cost_energy.append(result_energy) logging.info(f"Final average energy (cost): {result_energy}") diff --git a/src/modules/solvers/QiskitQAOA.py b/src/modules/solvers/QiskitQAOA.py index d854665e..5b41a5fc 100644 --- a/src/modules/solvers/QiskitQAOA.py +++ b/src/modules/solvers/QiskitQAOA.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + import logging -from typing import Tuple, TypedDict +from typing import TypedDict import numpy as np @@ -24,9 +25,11 @@ from qiskit_algorithms.optimizers import POWELL, SPSA, COBYLA from qiskit_algorithms.minimum_eigensolvers import VQE, QAOA, NumPyMinimumEigensolver -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement + class QiskitQAOA(Solver): """ Qiskit QAOA. @@ -34,51 +37,36 @@ class QiskitQAOA(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() - # self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu", "ibm_eagle"] self.submodule_options = ["qasm_simulator", "qasm_simulator_gpu"] self.ry = None @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "qiskit", - "version": "1.1.0" - }, - { - "name": "qiskit-optimization", - "version": "0.6.1" - }, - { - "name": "numpy", - "version": "1.26.4" - }, - { - "name": "qiskit-algorithms", - "version": "0.3.0" - } - + {"name": "qiskit", "version": "1.1.0"}, + {"name": "qiskit-optimization", "version": "0.6.1"}, + {"name": "numpy", "version": "1.26.4"}, + {"name": "qiskit-algorithms", "version": "0.3.0"} ] def get_default_submodule(self, option: str) -> Core: - if option == "qasm_simulator": - from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - return HelperClass("qasm_simulator") - elif option == "qasm_simulator_gpu": + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ + if option in ["qasm_simulator", "qasm_simulator_gpu"]: from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - return HelperClass("qasm_simulator_gpu") - # elif option == "ibm_eagle": - # from modules.devices.HelperClass import HelperClass # pylint: disable=C0415 - # return HelperClass("ibm_eagle") + return HelperClass(option) else: raise NotImplementedError(f"Device Option {option} not implemented") @@ -86,36 +74,36 @@ def get_parameter_options(self) -> dict: """ Returns the configurable settings for this solver. - :return: - .. code-block:: python - - return { - "shots": { # number measurements to make on circuit - "values": list(range(10, 500, 30)), - "description": "How many shots do you need?" - }, - "iterations": { # number measurements to make on circuit - "values": [1, 5, 10, 20, 50, 75], - "description": "How many iterations do you need? Warning: When using\ - the IBM Eagle Device you should only choose a lower number of\ - iterations, since a high number would lead to a waiting time that\ - could take up to mulitple days!" - }, - "depth": { - "values": [2, 3, 4, 5, 10, 20], - "description": "How many layers for QAOA (Parameter: p) do you want?" - }, - "method": { - "values": ["classic", "vqe", "qaoa"], - "description": "Which Qiskit solver should be used?" - }, - "optimizer": { - "values": ["POWELL", "SPSA", "COBYLA"], - "description": "Which Qiskit solver should be used? Warning: When\ - using the IBM Eagle Device you should not use the SPSA optimizer,\ - since it is not suited for only one evaluation!" - } - } + :return: Dictionary of configurable settings + .. code-block:: python + + return { + "shots": { # number measurements to make on circuit + "values": list(range(10, 500, 30)), + "description": "How many shots do you need?" + }, + "iterations": { # number measurements to make on circuit + "values": [1, 5, 10, 20, 50, 75], + "description": "How many iterations do you need? Warning: When using\ + the IBM Eagle Device you should only choose a lower number of\ + iterations, since a high number would lead to a waiting time that\ + could take up to multiple days!" + }, + "depth": { + "values": [2, 3, 4, 5, 10, 20], + "description": "How many layers for QAOA (Parameter: p) do you want?" + }, + "method": { + "values": ["classic", "vqe", "qaoa"], + "description": "Which Qiskit solver should be used?" + }, + "optimizer": { + "values": ["POWELL", "SPSA", "COBYLA"], + "description": "Which Qiskit solver should be used? Warning: When\ + using the IBM Eagle Device you should not use the SPSA optimizer,\ + since it is not suited for only one evaluation!" + } + } """ return { @@ -125,13 +113,13 @@ def get_parameter_options(self) -> dict: }, "iterations": { # number measurements to make on circuit "values": [1, 5, 10, 20, 50, 75], - "description": "How many iterations do you need? Warning: When using the IBM Eagle Device you\ - should only choose a lower number of iterations, since a high number would lead to a waiting \ - ime that could take up to mulitple days!" + "description": "How many iterations do you need? Warning: When using the IBM Eagle device you\ + should only choose a low number of iterations, since a high number would lead to a waiting \ + time that could take up to multiple days!" }, "depth": { "values": [2, 3, 4, 5, 10, 20], - "description": "How many layers for QAOA (Parameter: p) do you want?" + "description": "How many layers for QAOA (parameter: p) do you want?" }, "method": { "values": ["classic", "vqe", "qaoa"], @@ -139,7 +127,7 @@ def get_parameter_options(self) -> dict: }, "optimizer": { "values": ["POWELL", "SPSA", "COBYLA"], - "description": "Which Qiskit solver should be used? Warning: When using the IBM Eagle Device\ + "description": "Which Qiskit solver should be used? Warning: When using the IBM Eagle device\ you should not use the SPSA optimizer for a low number of iterations!" } } @@ -155,6 +143,7 @@ class Config(TypedDict): iterations: int layers: int method: str + optimizer: str """ shots: int @@ -162,41 +151,34 @@ class Config(TypedDict): iterations: int layers: int method: str + optimizer: str @staticmethod def normalize_data(data: any, scale: float = 1.0) -> any: """ Not used currently, as I just scale the coefficients in the qaoa_operators_from_ising. - :param data: - :type data: any - :param scale: - :type scale: float - :return: scaled data - :rtype: any + :param data: Data to normalize + :param scale: Scaling factor + :return: Normalized data """ return scale * data / np.max(np.abs(data)) - def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> (any, float): + def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs: dict) -> tuple[any, float, dict]: """ Run Qiskit QAOA algorithm on Ising. - :param mapped_problem: dictionary with the keys 'J' and 't' - :type mapped_problem: any - :param device_wrapper: instance of device - :type device_wrapper: any - :param config: - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param mapped_problem: Dictionary with the keys 'J' and 't' + :param device_wrapper: Instance of device + :param config: Config object for the solver + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - J = mapped_problem['J'] t = mapped_problem['t'] start = start_time_measurement() ising_op = self._get_pauli_op((t, J)) + if config["method"] == "classic": algorithm = NumPyMinimumEigensolver() else: @@ -205,7 +187,7 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs optimizer = COBYLA(maxiter=config["iterations"]) elif config["optimizer"] == "POWELL": optimizer = POWELL(maxiter=config["iterations"], maxfev=config["iterations"] if - device_wrapper.device == 'ibm_eagle' else None) + device_wrapper.device == 'ibm_eagle' else None) elif config["optimizer"] == "SPSA": optimizer = SPSA(maxiter=config["iterations"]) if config["method"] == "vqe": @@ -219,40 +201,27 @@ def run(self, mapped_problem: any, device_wrapper: any, config: Config, **kwargs logging.warning("No method selected in QiskitQAOA. Continue with NumPyMinimumEigensolver.") algorithm = NumPyMinimumEigensolver() - # run actual optimization algorithm + # Run actual optimization algorithm try: result = algorithm.compute_minimum_eigenvalue(ising_op) - print('result',result) except ValueError as e: logging.error(f"The following ValueError occurred in module QiskitQAOA: {e}") logging.error("The benchmarking run terminates with exception.") raise Exception("Please refer to the logged error message.") from e + best_bitstring = self._get_best_solution(result) return best_bitstring, end_time_measurement(start), {} - @staticmethod - def _get_quantum_instance(device_wrapper: any) -> any: - backend = Aer.get_backend("qasm_simulator") - if device_wrapper.device == 'qasm_simulator_gpu': - logging.info("Using GPU simulator") - backend.set_options(device='GPU') - backend.set_options(method='statevector_gpu') - # elif device_wrapper.device == 'ibm_eagle': - # logging.info("Using IBM Eagle") - # ibm_quantum_token = os.environ.get('ibm_quantum_token') - # service = QiskitRuntimeService(channel="ibm_quantum", token=ibm_quantum_token) - # backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - else: - logging.info("Using CPU simulator") - backend.set_options(device='CPU') - backend.set_options(method='statevector') - backend.set_options(max_parallel_threads=48) - return backend - def _get_best_solution(self, result) -> any: + """ + Gets the best solution from the result. + + :param result: Result from the quantum algorithm + :return: Best bitstring solution + """ if self.ry is not None: if hasattr(result, "optimal_point"): - para_dict = dict(zip(self.ry.parameters, result.optimal_point)) + para_dict = dict(zip(self.ry.parameters, result.optimal_point)) unbound_para = set(self.ry.parameters) - set(para_dict.keys()) for param in unbound_para: para_dict[param] = 0.0 @@ -262,20 +231,26 @@ def _get_best_solution(self, result) -> any: else: raise AttributeError("The result object does not have 'optimal_point' or 'eigenstate' attributes.") else: - # If self.ry is None if hasattr(result, "eigenstate"): eigvec = result.eigenstate else: raise AttributeError("The result object does not have 'eigenstate'.") + best_bitstring = OptimizationApplication.sample_most_likely(eigvec) return best_bitstring @staticmethod - def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> object: + def _get_pauli_op(ising: tuple[np.ndarray, np.ndarray]) -> SparsePauliOp: + """ + Creates a Pauli operator from the given Ising model representation. + + :param ising: Tuple with linear and quandratic terms + .return: SparsePauliOp representing the Ising model + """ pauli_list = [] number_qubits = len(ising[0]) - # linear terms + # Linear terms it = np.nditer(ising[0], flags=['multi_index']) for x in it: logging.debug(f"{x},{it.multi_index}") @@ -299,20 +274,5 @@ def _get_pauli_op(ising: Tuple[np.ndarray, np.ndarray]) -> object: pauli_str = "".join(pauli_str_list) pauli_list.append((pauli_str, complex(x))) - # for key, value in ising[0].items(): - # pauli_str = "I"*number_qubits - # pauli_str_list = list(pauli_str) - # pauli_str_list[key] = "Z" - # pauli_str = "".join(pauli_str_list) - # pauli_list.append((pauli_str, value)) - # - # for key, value in ising[1].items(): - # pauli_str = "I"*number_qubits - # pauli_str_list = list(pauli_str) - # pauli_str_list[key[0]] = "Z" - # pauli_str_list[key[1]] = "Z" - # pauli_str = "".join(pauli_str_list) - # pauli_list.append((pauli_str, value)) - - isingOp =SparsePauliOp.from_list(pauli_list) - return isingOp + ising_op = SparsePauliOp.from_list(pauli_list) + return ising_op diff --git a/src/modules/solvers/RandomClassicalPVC.py b/src/modules/solvers/RandomClassicalPVC.py index 9b9050b2..12aa6688 100644 --- a/src/modules/solvers/RandomClassicalPVC.py +++ b/src/modules/solvers/RandomClassicalPVC.py @@ -14,9 +14,10 @@ from typing import TypedDict import random -import networkx +import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,7 +28,7 @@ class RandomPVC(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -35,19 +36,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module + """ + return [{"name": "networkx", "version": "3.2.1"}] + + def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] - - def get_default_submodule(self, option: str) -> any: if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -56,41 +57,34 @@ def get_default_submodule(self, option: str) -> any: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dictionary as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the PVC graph in a greedy fashion. - :param mapped_problem: graph representing a PVC problem - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work + # Deep copy since we are modifying the graph. This ensures that the original graph remains unchanged # with a different graph mapped_problem = mapped_problem.copy() start = start_time_measurement() + # We always start at the base node current_node = ((0, 0), 1, 1) idx = 1 @@ -101,9 +95,11 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi # to the same seam while len(mapped_problem.nodes) > 2: # Get the random neighbor edge from the current node - next_node = random.choice([x for x in mapped_problem.edges(current_node[0], data=True) if - x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] - and x[2]['t_start'] == current_node[2]]) + next_node = random.choice([ + x for x in mapped_problem.edges(current_node[0], data=True) + if x[1][0] != current_node[0][0] and x[2]['c_start'] == current_node[1] and x[2]['t_start'] + == current_node[2] + ]) next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) # Make the step - add distance to cost, add the best node to tour, @@ -113,6 +109,7 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) + current_node = next_node idx += 1 diff --git a/src/modules/solvers/RandomClassicalSAT.py b/src/modules/solvers/RandomClassicalSAT.py index cc247afe..4dc94dea 100644 --- a/src/modules/solvers/RandomClassicalSAT.py +++ b/src/modules/solvers/RandomClassicalSAT.py @@ -13,10 +13,14 @@ # limitations under the License. from typing import TypedDict +import logging + import numpy as np + from pysat.formula import WCNF -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,7 +31,7 @@ class RandomSAT(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -35,20 +39,13 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ return [ - { - "name": "python-sat", - "version": "1.8.dev13" - }, - { - "name": "numpy", - "version": "1.26.4" - } + {"name": "python-sat", "version": "1.8.dev13"}, + {"name": "numpy", "version": "1.26.4"} ] def get_default_submodule(self, option: str) -> Core: @@ -60,40 +57,32 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) -> (list, float): + def run(self, mapped_problem: WCNF, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[list, float, dict]: """ - The given application is a problem instance from the pysat library. This generates a random solution to the - problem. + The given application is a problem instance from the pysat library. + This generates a random solution to the problem. - :param mapped_problem: - :type mapped_problem: WCNF + :param mapped_problem: The WCNF representation of the SAT problem :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - logging.info( - f"Got problem with {mapped_problem.nv} variables, {len(mapped_problem.hard)} constraints and" + f"Got SAT problem with {mapped_problem.nv} variables, {len(mapped_problem.hard)} constraints and" f" {len(mapped_problem.soft)} tests." ) diff --git a/src/modules/solvers/RandomClassicalTSP.py b/src/modules/solvers/RandomClassicalTSP.py index 0e289e67..b5dcc7e7 100644 --- a/src/modules/solvers/RandomClassicalTSP.py +++ b/src/modules/solvers/RandomClassicalTSP.py @@ -16,18 +16,19 @@ import random import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class RandomTSP(Solver): """ - Classical Random Solver the TSP + Classical Random Solver the TSP. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -35,17 +36,11 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: if option == "Local": @@ -56,56 +51,46 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the TSP graph in a greedy fashion. - :param mapped_problem: graph representing a TSP - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a TSP :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - start = start_time_measurement() source = nx.utils.arbitrary_element(mapped_problem) nodeset = set(mapped_problem) nodeset.remove(source) tour = [source] + while nodeset: next_node = random.choice(list(nodeset)) tour.append(next_node) nodeset.remove(next_node) tour.append(tour[0]) - # We remove the duplicate node as we don't want a cycle - # https://stackoverflow.com/a/7961390/10456906 + # Remove the duplicate node as we don't want a cycle tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later - result = {} - for idx, node in enumerate(tour): - result[(node, idx)] = 1 - # Tour needs to look like + result = {(node, idx): 1 for idx, node in enumerate(tour)} + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/ReverseGreedyClassicalPVC.py b/src/modules/solvers/ReverseGreedyClassicalPVC.py index f040353c..150d2d78 100644 --- a/src/modules/solvers/ReverseGreedyClassicalPVC.py +++ b/src/modules/solvers/ReverseGreedyClassicalPVC.py @@ -14,9 +14,10 @@ from typing import TypedDict -import networkx +import networkx as nx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement @@ -27,12 +28,18 @@ class ReverseGreedyClassicalPVC(Solver): def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -42,55 +49,42 @@ def get_default_submodule(self, option: str) -> Core: @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Config, **kwargs: dict) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: dict) \ + -> tuple[dict, float, dict]: """ Solve the PVC graph in a greedy fashion. We take the worst choice at each step. - :param mapped_problem: graph representing a PVC problem - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a PVC problem :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Need to deep copy since we are modifying the graph in this function. + # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() start = start_time_measurement() + # We always start at the base node current_node = ((0, 0), 1, 1) idx = 1 @@ -102,10 +96,15 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi while len(mapped_problem.nodes) > 2: # Get the minimum neighbor edge from the current node # TODO This only works if the artificial high edge weights are exactly 100000 - next_node = max((x for x in mapped_problem.edges(current_node[0], data=True) if - x[2]['c_start'] == current_node[1] and x[2]['t_start'] == current_node[2] and - x[2]['weight'] != 100000), - key=lambda x: x[2]['weight']) + next_node = max( + ( + x for x in mapped_problem.edges(current_node[0], data=True) + if x[2]['c_start'] == current_node[1] + and x[2]['t_start'] == current_node[2] + and x[2]['weight'] != 100000 + ), + key=lambda x: x[2]['weight'] + ) next_node = (next_node[1], next_node[2]["c_end"], next_node[2]["t_end"]) # Make the step - add distance to cost, add the best node to tour, @@ -115,6 +114,7 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi to_remove = [x for x in mapped_problem.nodes if x[0] == current_node[0][0]] for node in to_remove: mapped_problem.remove_node(node) + current_node = next_node idx += 1 diff --git a/src/modules/solvers/ReverseGreedyClassicalTSP.py b/src/modules/solvers/ReverseGreedyClassicalTSP.py index 8f0eb60e..d338fbd3 100644 --- a/src/modules/solvers/ReverseGreedyClassicalTSP.py +++ b/src/modules/solvers/ReverseGreedyClassicalTSP.py @@ -14,21 +14,23 @@ from typing import TypedDict -import networkx +import networkx as nx from networkx.algorithms import approximation as approx -from modules.solvers.Solver import * +from modules.solvers.Solver import Solver +from modules.Core import Core from utils import start_time_measurement, end_time_measurement class ReverseGreedyClassicalTSP(Solver): """ - Classical Reverse Greedy Solver for the TSP. We take the worst choice at each step. + Classical Reverse Greedy Solver for the TSP. + We take the worst choice at each step. """ def __init__(self): """ - Constructor method + Constructor method. """ super().__init__() self.submodule_options = ["Local"] @@ -36,19 +38,19 @@ def __init__(self): @staticmethod def get_requirements() -> list[dict]: """ - Return requirements of this module + Return requirements of this module. - :return: list of dict with requirements of this module - :rtype: list[dict] + :return: List of dict with requirements of this module """ - return [ - { - "name": "networkx", - "version": "3.2.1" - } - ] + return [{"name": "networkx", "version": "3.2.1"}] def get_default_submodule(self, option: str) -> Core: + """ + Returns the default submodule based on the provided option. + + :param option: The name of the submodule + :return: Instance of the default submodule + """ if option == "Local": from modules.devices.Local import Local # pylint: disable=C0415 return Local() @@ -57,43 +59,37 @@ def get_default_submodule(self, option: str) -> Core: def get_parameter_options(self) -> dict: """ - Returns empty dict as this solver has no configurable settings + Returns empty dict as this solver has no configurable settings. - :return: empty dict - :rtype: dict + :return: Empty dict """ - return { - - } + return {} class Config(TypedDict): """ - Empty config as this solver has no configurable settings + Empty config as this solver has no configurable settings. """ pass - def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Config, **kwargs: any) -> (dict, float): + def run(self, mapped_problem: nx.Graph, device_wrapper: any, config: Config, **kwargs: any) \ + -> tuple[dict, float, dict]: """ Solve the TSP graph in a greedy fashion. - :param mapped_problem: graph representing a TSP - :type mapped_problem: networkx.Graph + :param mapped_problem: Graph representing a TSP :param device_wrapper: Local device - :type device_wrapper: any - :param config: empty dict - :type config: Config - :param kwargs: no additionally settings needed - :type kwargs: any + :param config: Empty dict + :param kwargs: No additionally settings needed :return: Solution, the time it took to compute it and optional additional information - :rtype: tuple(list, float, dict) """ - - # Need to deep copy since we are modifying the graph in this function. Else the next repetition would work - # with a different graph + # Need to deep copy since we are modifying the graph in this function. + # Else the next repetition would work with a different graph mapped_problem = mapped_problem.copy() + # Let's flip the edge weights to take the worst node every time instead of the best for _, _, d in mapped_problem.edges(data=True): d['weight'] = -1.0 * d['weight'] + start = start_time_measurement() tour = approx.greedy_tsp(mapped_problem) @@ -103,8 +99,6 @@ def run(self, mapped_problem: networkx.Graph, device_wrapper: any, config: Confi tour = list(dict.fromkeys(tour)) # Parse tour so that it can be processed later - result = {} - for idx, node in enumerate(tour): - result[(node, idx)] = 1 - # Tour needs to look like + result = {(node, idx): 1 for idx, node in enumerate(tour)} + return result, end_time_measurement(start), {} diff --git a/src/modules/solvers/Solver.py b/src/modules/solvers/Solver.py index dc08beba..30238f6d 100644 --- a/src/modules/solvers/Solver.py +++ b/src/modules/solvers/Solver.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from modules.Core import * +from abc import ABC, abstractmethod +from modules.Core import Core class Solver(Core, ABC): @@ -22,38 +22,29 @@ class Solver(Core, ABC): defined objective function. """ - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ - The actual solving process is done here, as we have the device, which got provided by the device submodule, + The actual solving process is done here, using the device which is provided by the device submodule and the problem data provided by the parent module. :param input_data: Data passed to the run function of the solver - :type input_data: any - :param config: solver config - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param config: Solver config + :param kwargs: Optional keyword arguments :return: Output and time needed - :rtype: (any, float) """ output, elapsed_time, additional_metrics = self.run(self.preprocessed_input, input_data, config, **kwargs) self.metrics.add_metric_batch(additional_metrics) return output, elapsed_time @abstractmethod - def run(self, mapped_problem, device_wrapper, config, **kwargs) -> (any, float, dict): + def run(self, mapped_problem: any, device_wrapper: any, config: any, **kwargs) -> tuple[any, float, dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. - :param mapped_problem: a representation of the problem that the solver can solve - :type mapped_problem: any - :param device_wrapper: a device the solver can leverage for the algorithm - :type device_wrapper: any - :param config: settings for the solver such as hyperparameters - :type config: any - :param kwargs: optional additional settings - :type kwargs: any + :param mapped_problem: A representation of the problem that the solver can solve + :param device_wrapper: A device the solver can leverage for the algorithm + :param config: Settings for the solver such as hyperparameters + :param kwargs: Optional additional settings :return: Solution, the time it took to compute it and some optional additional information - :rtype: tuple(any, float, dict) """ pass diff --git a/src/quark2_adapter/adapters.py b/src/quark2_adapter/adapters.py index 5135a101..6e864738 100644 --- a/src/quark2_adapter/adapters.py +++ b/src/quark2_adapter/adapters.py @@ -15,8 +15,8 @@ from abc import ABC import json -from time import time import logging +from time import time from modules.Core import Core from modules.applications.Application import Application as Application_NEW @@ -48,11 +48,11 @@ class ApplicationAdapter(Application_NEW, Application_OLD, ABC): to get your Application running with QUARK2. """ - def __init__(self, application_name, *args, **kwargs): + def __init__(self, application_name: str, *args, **kwargs): """ - Constructor method + Constructor method. """ - logging.warning(WARNING_MSG, self.__class__.__name__) + logging.warning(WARNING_MSG, self.__class__.__name__) Application_NEW.__init__(self, application_name) Application_OLD.__init__(self, application_name) self.args = args @@ -62,8 +62,10 @@ def __init__(self, application_name, *args, **kwargs): self.problems = {} @property - def submodule_options(self): - """Maps the old attribute mapping_options to the new attribute submodule_options.""" + def submodule_options(self) -> list[str]: + """ + Maps the old attribute mapping_options to the new attribute submodule_options. + """ return self.mapping_options @submodule_options.setter @@ -71,7 +73,7 @@ def submodule_options(self, options: list[str]): """ Maps the old attribute mapping_options to the new attribute submodule_options. - :param options: list[str] + :param options: List of submodule options """ self.mapping_options = options @@ -80,32 +82,26 @@ def get_default_submodule(self, option: str) -> Core: Maps the old method get_mapping to the new get_default_submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ return self.get_mapping(option) - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Application_NEW.preprocess using the Application_OLD interface. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ start = time() logging.warning(WARNING_MSG, self.__class__.__name__) rep_count = kwargs["rep_count"] - #create a hash value for identifying the problem configuration - #compare https://stackoverflow.com/questions/5884066/hashing-a-dictionary + # create a hash value for identifying the problem configuration + # compare https://stackoverflow.com/questions/5884066/hashing-a-dictionary problem_conf_hash = json.dumps(config, sort_keys=True) if self.problem_conf_hash != problem_conf_hash: @@ -117,39 +113,34 @@ def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): self.problem, creation_time = self.problems[problem_key] else: start = time() - logging.info("generate new problem instance") + logging.info("Generating new problem instance") self.problem = self.generate_problem(config, rep_count) - creation_time = (time() - start)*1000 + creation_time = (time() - start) * 1000 self.problems[problem_key] = (self.problem, creation_time) + return self.problem, creation_time - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Application_NEW.postprocess using the Application_OLD interface. :param input_data: Input data comes from the submodule if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the postprocessing and the time it took to postprocess - :rtype: (any, float) """ - processed_solution, time_processing = self.process_solution(input_data) solution_validity, time_validate = self.validate(processed_solution) - if solution_validity: - solution_quality, time_evaluate = self.evaluate(processed_solution) - else: - solution_quality, time_evaluate = None, 0.0 + solution_quality, time_evaluate = (self.evaluate(processed_solution) + if solution_validity else (None, 0.0)) self.metrics.add_metric("time_to_validation", time_validate) self.metrics.add_metric("time_to_validation_unit", "ms") self.metrics.add_metric("solution_validity", solution_validity) self.metrics.add_metric("solution_quality", solution_quality) self.metrics.add_metric("solution_quality_unit", self.get_solution_quality_unit()) - return (solution_validity, solution_quality), time_validate+time_evaluate+time_processing + + return (solution_validity, solution_quality), time_validate + time_evaluate + time_processing class MappingAdapter(Mapping_NEW, Mapping_OLD, ABC): @@ -168,24 +159,26 @@ class MappingAdapter(Mapping_NEW, Mapping_OLD, ABC): to get your Mapping running with QUARK2. """ - def __init__(self,*args, **kwargs): + def __init__(self, *args, **kwargs): """ - Constructor method + Constructor method. """ Mapping_NEW.__init__(self) Mapping_OLD.__init__(self) @property - def submodule_options(self): - """Maps the old attribute solver_options to the new attribute submodule_options.""" + def submodule_options(self) -> list[str]: + """ + Maps the old attribute solver_options to the new attribute submodule_options. + """ return self.solver_options @submodule_options.setter - def submodule_options(self, options): + def submodule_options(self, options: list[str]): """ Maps the old attribute solver_options to the new attribute submodule_options. - :param options: list[str] + :param options: List of solver options """ self.solver_options = options @@ -194,47 +187,39 @@ def get_default_submodule(self, option: str) -> Core: Maps the old method get_solver to the new get_default_submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ return self.get_solver(option) - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Mapping_NEW.preprocess using the Mapping_OLD interface. """ logging.warning(WARNING_MSG, self.__class__.__name__) return self.map(input_data, config=config) - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Mapping_NEW.postprocess using the Mapping_OLD interface. """ logging.info("Calling %s.reverse_map", __class__.__name__) processed_solution, postprocessing_time = self.reverse_map(input_data) - - # self.metrics.add_metric("processed_solution", ["%s: %s" % ( - # sol.__class__.__name__, sol) for sol in processed_solution]) return processed_solution, postprocessing_time -def recursive_replace_dict_keys(obj: any)-> any: +def recursive_replace_dict_keys(obj: any) -> any: """ - Replace values used as dict-keys by its str(), to make the object json compatible. + Replace values used as dictionary keys by their string representation + to make the object JSON-compatible. - :param obj: the object - :type obj: any + :param obj: The object to convert + .return: The object with all dictionary keys converted to strings """ obj_new = None if isinstance(obj, dict): - obj_new = {} - for key in obj: - obj_new[str(key)] = recursive_replace_dict_keys(obj[key]) + obj_new = {str(key): recursive_replace_dict_keys(value) for key, value in obj.items()} elif isinstance(obj, list): - obj_new = [] - for element in obj: - obj_new.append(recursive_replace_dict_keys(element)) + obj_new = [recursive_replace_dict_keys(element) for element in obj] elif isinstance(obj, tuple): obj_new = tuple(recursive_replace_dict_keys(element) for element in obj) else: @@ -259,24 +244,26 @@ class SolverAdapter(Solver_NEW, Solver_OLD, ABC): to get your Solver running with QUARK2. """ - def __init__(self,*args, **kwargs): + def __init__(self, *args, **kwargs): """ - Constructor method + Constructor method. """ Solver_NEW.__init__(self) Solver_OLD.__init__(self) @property - def submodule_options(self): - """Maps the old attribute device_options to the new attribute submodule_options.""" + def submodule_options(self) -> list[str]: + """ + Maps the old attribute device_options to the new attribute submodule_options. + """ return self.device_options @submodule_options.setter - def submodule_options(self, options): + def submodule_options(self, options: list[str]): """ Maps the old attribute device_options to the new attribute submodule_options. - :param options: list[str] + :param options: List of device options """ self.device_options = options @@ -285,49 +272,49 @@ def get_default_submodule(self, option: str) -> Core: Maps the old method get_device to the new get_default_submodule. :param option: String with the chosen submodule - :type option: str :return: Module of type Core - :rtype: Core """ return self.get_device(option) - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Solver_NEW.preprocess using the Solver_OLD interface. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the module - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ logging.warning(WARNING_MSG, self.__class__.__name__) return input_data, 0.0 - def postprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def postprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Solver_NEW.postprocess using the Solver_OLD interface. :param input_data: Data passed to the run function of the solver - :type input_data: any - :param config: solver config - :type config: dict - :param kwargs: optional keyword arguments - :type kwargs: dict + :param config: Solver config + :param kwargs: Optional keyword arguments :return: Output and time needed - :rtype: (any, float) """ run_kwargs = { - "store_dir": kwargs["store_dir"], "repetition": kwargs["rep_count"]} - raw_solution, runtime, additional_solver_information = self.run(input_data["mapped_problem"], - device_wrapper=input_data["device"], - config=config, **run_kwargs) - self.metrics.add_metric("additional_solver_information", dict( - additional_solver_information)) - self.metrics.add_metric("solution_raw", self.raw_solution_to_json(raw_solution)) + "store_dir": kwargs["store_dir"], + "repetition": kwargs["rep_count"] + } + raw_solution, runtime, additional_solver_information = self.run( + input_data["mapped_problem"], + device_wrapper=input_data["device"], + config=config, + **run_kwargs + ) + + self.metrics.add_metric( + "additional_solver_information", dict(additional_solver_information) + ) + self.metrics.add_metric( + "solution_raw", self.raw_solution_to_json(raw_solution) + ) + return raw_solution, runtime def raw_solution_to_json(self, raw_solution: any) -> any: @@ -337,9 +324,8 @@ def raw_solution_to_json(self, raw_solution: any) -> any: to json. Note that using 'recursive_replace_dict_keys' provided by this module might help. - :param raw_solution: the raw solution - :type raw_solution: any - :rtype: any + :param raw_solution: The raw solution + :return: JSON-compatible representation of the raw solution """ return raw_solution @@ -360,9 +346,9 @@ class DeviceAdapter(Device_NEW, Device_OLD): to get your Device running with QUARK2. """ - def __init__(self, name): + def __init__(self, name: str): """ - Constructor method + Constructor method. """ Device_NEW.__init__(self, name) Device_OLD.__init__(self, name) @@ -374,24 +360,18 @@ def get_default_submodule(self, option: str) -> Core: could not have submodules. :param option: String with the chosen submodule - :type option: str :return: None - :rtype: Core """ return None - def preprocess(self, input_data: any, config: dict, **kwargs) -> (any, float): + def preprocess(self, input_data: any, config: dict, **kwargs) -> tuple[any, float]: """ Implements Device_NEW.preprocess using the Device_OLD interface. :param input_data: Data for the module, comes from the parent module if that exists - :type input_data: any :param config: Config for the device - :type config: dict :param kwargs: Optional keyword arguments - :type kwargs: dict :return: The output of the preprocessing and the time it took to preprocess - :rtype: (any, float) """ logging.warning(WARNING_MSG, self.__class__.__name__) self.set_config(config) @@ -416,7 +396,7 @@ class LocalAdapter(DeviceAdapter): def __init__(self): """ - Constructor method + Constructor method. """ DeviceAdapter.__init__(self, name="local") self.device = None diff --git a/src/quark2_adapter/legacy_classes/Application.py b/src/quark2_adapter/legacy_classes/Application.py index 71b2417f..f3aa8a19 100644 --- a/src/quark2_adapter/legacy_classes/Application.py +++ b/src/quark2_adapter/legacy_classes/Application.py @@ -20,13 +20,15 @@ class Application(ABC): """ - The application component defines the workload, comprising a dataset of increasing complexity, a validation, and an - evaluation function. + The application component defines the workload, comprising a dataset of increasing complexity, + a validation, and an evaluation function. """ - def __init__(self, application_name): + def __init__(self, application_name: str): """ - Constructor method + Constructor method. + + :param application_name: Name of the application """ self.application_name = application_name self.application = None @@ -41,10 +43,9 @@ def __init__(self, application_name): def get_application(self) -> any: """ - Getter that returns the application + Getter that returns the application. - :return: self.application - :rtype: any + :return: The application instance """ return self.application @@ -54,7 +55,6 @@ def get_solution_quality_unit(self) -> str: Method to return the unit of the evaluation which is used to make the plots nicer. :return: String with the unit - :rtype: str """ @abstractmethod @@ -78,23 +78,22 @@ def get_parameter_options(self) -> dict: } :return: Available application settings for this application - :rtype: dict """ pass def regenerate_on_iteration(self, config: dict) -> bool: - """Overwrite this to return True if the problem should be newly generated + """ + Overwrite this to return True if the problem should be newly generated on every iteration. Typically, this will be the case if the problem is taken from a statistical ensemble e.g. an erdos-renyi graph. - :param config: the application configuration - :type config: dict - :return: whether the problem should be recreated on every iteration. Returns False if not overwritten. - :rtype: bool + + :param config: The application configuration + :return: Whether the problem should be recreated on every iteration. Returns False if not overwritten. """ return False @final - def init_problem(self, config, conf_idx: int, iter_count: int, path): + def init_problem(self, config: dict, conf_idx: int, iter_count: int, path: str) -> any: """ This method is called on every iteration and calls generate_problem if necessary. conf_idx identifies the application configuration. @@ -106,17 +105,11 @@ def init_problem(self, config, conf_idx: int, iter_count: int, path): over the different application configurations (conf_idx). :param config: the application configuration - :type config: dict :param conf_idx: the index of the application configuration - :conf_idx: int :param iter_count: the repetition count (starting with 1) - :type iter_count: int :param path: the path used to save each newly generated problem instance - :type path: str :return: the current problem instance - :rtype: any """ - if conf_idx != self.conf_idx: self.problems = {} self.conf_idx = conf_idx @@ -135,65 +128,49 @@ def generate_problem(self, config: dict, iter_count: int) -> any: """ Depending on the config this method creates a concrete problem and returns it. - :param config: - :type config: dict - :param iter_count: the iteration count - :type iter_count: int - :return: - :rtype: any + :param config: The application configuration + :param iter_count: The iteration count + :return: The generated problem instance """ pass - def process_solution(self, solution) -> (any, float): + def process_solution(self, solution) -> tuple[any, float]: """ Most of the time the solution has to be processed before it can be validated and evaluated This might not be necessary in all cases, so the default is to return the original solution. - :param solution: - :type solution: any + :param solution: The solution to be processed :return: Processed solution and the execution time to process it - :rtype: tuple(any, float) - """ return solution, 0 @abstractmethod - def validate(self, solution) -> (bool, float): + def validate(self, solution) -> tuple[bool, float]: """ - Check if the solution is a valid solution. - - :return: bool and the time it took to create it - :param solution: - :type solution: any - :rtype: tuple(bool, float) + Check if the solution is valid. + :param solution: The solution to validate + :return: Boolean indicating if the solution is valid and the time it took to create it """ pass @abstractmethod - def evaluate(self, solution: any) -> (float, float): + def evaluate(self, solution: any) -> tuple[float, float]: """ Checks how good the solution is to allow comparison to other solutions. - :param solution: - :type solution: any + :param solution: The solution to evaluate :return: Evaluation and the time it took to create it - :rtype: tuple(any, float) - """ pass @abstractmethod def save(self, path: str, iter_count: int) -> None: """ - Function to save the concrete problem. + Save the concrete problem. - :param path: path of the experiment directory for this run - :type path: str + :param path: Path of the experiment directory for this run :param iter_count: the iteration count - :type iter_count: int - :return: - :rtype: None """ pass @@ -202,10 +179,8 @@ def get_submodule(self, mapping_option: str) -> any: If self.sub_options is not None, a mapping is instantiated according to the information given in self.sub_options. Otherwise, get_mapping is called as fall back. - :param mapping_option: String with the option - :type mapping_option: str + :param mapping_option: The option for the mapping :return: instance of a mapping class - :rtype: any """ if self.sub_options is None: return self.get_mapping(mapping_option) @@ -219,20 +194,17 @@ def get_mapping(self, mapping_option: str) -> any: self.sub_options is None. See get_submodule. :param mapping_option: String with the option - :rtype: str - :return: instance of a mapping class - :rtype: any + :return: Instance of a mapping class """ pass def get_available_mapping_options(self) -> list: """ - Get list of available mapping options. + Gets the list of available mapping options. :return: list of mapping options - :rtype: list """ if self.sub_options is None: return self.mapping_options else: - return [o["name"] for o in self.sub_options] + return [option["name"] for option in self.sub_options] diff --git a/src/quark2_adapter/legacy_classes/Device.py b/src/quark2_adapter/legacy_classes/Device.py index dec7d6a0..7bc0d066 100644 --- a/src/quark2_adapter/legacy_classes/Device.py +++ b/src/quark2_adapter/legacy_classes/Device.py @@ -17,12 +17,15 @@ class Device(ABC): """ - The device class abstracts away details of the physical device, such as submitting a task to the quantum system. + The device class abstracts away details of the physical device, + such as submitting a task to the quantum system. """ def __init__(self, device_name: str): """ - Constructor method + Constructor method. + + :param device_name: Name of the device """ self.device = None self.device_name = device_name @@ -44,28 +47,29 @@ def get_parameter_options(self) -> dict: } :return: Available device settings for this device - :rtype: dict """ - return { - } + return {} + + def set_config(self, config: dict) -> None: + """ + Sets the device configuration. - def set_config(self, config): + :param config: Configuration dictionary + """ self.config = config def get_device(self) -> any: """ - Returns Device. + Returns the device instance. - :return: Instance of the device class - :rtype: any + :return: Instance of the device """ return self.device def get_device_name(self) -> str: """ - Returns Device name. + Returns the name of the Device. :return: Name of the device - :rtype: str """ return self.device_name diff --git a/src/quark2_adapter/legacy_classes/Mapping.py b/src/quark2_adapter/legacy_classes/Mapping.py index 971f5af2..05e0d716 100644 --- a/src/quark2_adapter/legacy_classes/Mapping.py +++ b/src/quark2_adapter/legacy_classes/Mapping.py @@ -13,47 +13,42 @@ # limitations under the License. from abc import ABC, abstractmethod -from time import time from utils import _get_instance_with_sub_options class Mapping(ABC): """ - The task of the mapping module is to translate the application’s data and problem specification into a mathematical - formulation suitable for a solver. + The task of the mapping module is to translate the application’s data and problem specification + into a mathematical formulation suitable for a solver. """ def __init__(self): """ - Constructor method + Constructor method. """ self.solver_options = [] self.sub_options = None super().__init__() @abstractmethod - def map(self, problem, config) -> (any, float): + def map(self, problem: any, config: dict) -> tuple[any, float]: """ Maps the given problem into a specific format a solver can work with. E.g. graph to QUBO. - :param config: instance of class Config specifying the mapping settings - :param problem: problem instance which should be mapped to the target representation - :return: Must always return the mapped problem and the time it took to create the mapping - :rtype: tuple(any, float) + :param problem: Problem instance which should be mapped to the target representation + :param config: Instance of class Config specifying the mapping settings + :return: The mapped problem and the time it took to create the mapping """ pass - def reverse_map(self, solution) -> (any, float): + def reverse_map(self, solution: any) -> tuple[any, float]: """ Maps the solution back to the original problem. This might not be necessary in all cases, so the default is to return the original solution. This might be needed to convert the solution to a representation needed for validation and evaluation. - :param solution: - :type solution: any + :param solution: Solution to be reversed back to its original representation :return: Mapped solution and the time it took to create it - :rtype: tuple(any, float) - """ return solution, 0 @@ -74,7 +69,6 @@ def get_parameter_options(self) -> dict: } :return: Returns the available parameter options of this mapping - :rtype: dict """ pass @@ -83,10 +77,8 @@ def get_submodule(self, solver_option: str) -> any: If self.sub_options is not None, a solver is instantiated according to the information given in sub_options. Otherwise, get_solver is called as fall back. - :param solver_option: String with the option - :type solver_option: str - :return: instance of a solver class - :rtype: any + :param solver_option: The option for the solver + :return: Instance of a solver class """ if self.sub_options is None: return self.get_solver(solver_option) @@ -99,10 +91,8 @@ def get_solver(self, solver_option: str) -> any: Returns the default solver for a given string. This applies only if self.sub_options is None. See get_submodule. - :param solver_option: desired solver - :type solver_option: str - :return: instance of solver class - :rtype: any + :param solver_option: desired solver option + :return: Instance of solver class """ pass @@ -110,10 +100,9 @@ def get_available_solver_options(self) -> list: """ Returns all available solvers. - :return: list of solvers - :rtype: list + :return: List of solvers """ if self.sub_options is None: return self.solver_options else: - return [o["name"] for o in self.sub_options] + return [option["name"] for option in self.sub_options] diff --git a/src/quark2_adapter/legacy_classes/Solver.py b/src/quark2_adapter/legacy_classes/Solver.py index b103e9fb..07ad5ee2 100644 --- a/src/quark2_adapter/legacy_classes/Solver.py +++ b/src/quark2_adapter/legacy_classes/Solver.py @@ -13,40 +13,33 @@ # limitations under the License. from abc import ABC, abstractmethod -from time import time from utils import _get_instance_with_sub_options - class Solver(ABC): """ - The solver is responsible for finding feasible and high-quality solutions of the formulated problem, i.e., of the - defined objective function. + The solver is responsible for finding feasible and high-quality solutions + of the formulated problem, i.e., of the defined objective function. """ def __init__(self): """ - Constructor method + Constructor method. """ self.device_options = [] self.sub_options = None super().__init__() @abstractmethod - def run(self, mapped_problem, device, config, **kwargs) -> (any, float, dict): + def run(self, mapped_problem: any, device: any, config: dict, **kwargs) -> tuple[any, float, dict]: """ This function runs the solving algorithm on a mapped problem instance and returns a solution. - :param mapped_problem: a representation of the problem that the solver can solve - :type mapped_problem: any - :param device: a device the solver can leverage for the algorithm - :type device: any - :param config: settings for the solver such as hyperparameters - :type config: any - :param kwargs: optional additional settings - :type kwargs: any + :param mapped_problem: A representation of the problem that the solver can solve + :param device: A device the solver can leverage for the algorithm + :param config: Settings for the solver such as hyperparameters + :param kwargs: Optional additional settings :return: Solution, the time it took to compute it and some optional additional information - :rtype: tuple(any, float, dict) """ pass @@ -67,7 +60,6 @@ def get_parameter_options(self) -> dict: } :return: Available solver settings for this solver - :rtype: dict """ pass @@ -76,10 +68,8 @@ def get_submodule(self, device_option: str) -> any: If self.sub_options is not None, a device is instantiated according to the information given in self.sub_options. Otherwise, get_device is called as fall back. - :param device_option: String with the option - :type device_option: str - :return: instance of the device class - :rtype: any + :param device_option: The option for the device + :return: Instance of the device class """ if self.sub_options is None: return self.get_device(device_option) @@ -92,19 +82,16 @@ def get_device(self, device_option: str) -> any: Returns the default device based on string. This applies only if self.sub_options is None. See get_submodule. - :param device_option: - :type device_option: str - :return: instance of the device class - :rtype: any + :param device_option: Desired device option + :return: Instance of the device class """ pass def get_available_device_options(self) -> list: """ - Returns list of devices. + Returns the list of available devices. - :return: list of devices - :rtype: list + :return: List of devices """ if self.sub_options is None: return self.device_options diff --git a/src/utils.py b/src/utils.py index 0c9c5992..6058c799 100644 --- a/src/utils.py +++ b/src/utils.py @@ -25,31 +25,28 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: """ - Creates an instance of the QUARK module identified by class_name + Creates an instance of the QUARK module identified by class_name. - :param options: Section of the QUARK module configuration including the submodules' information. - :type options: list of dict - :param name: name of the QUARK component to be initialized - :type name: str + :param options: Section of the QUARK module configuration including the submodules' information + :param name: Name of the QUARK component to be initialized :return: New instance of the QUARK module - :rtype: any """ for opt in options: if name != opt["name"]: continue class_name = opt.get("class", name) clazz = _import_class(opt["module"], class_name, opt.get("dir")) - sub_options = None - if "submodules" in opt: - sub_options = opt["submodules"] + sub_options = opt.get("submodules", None) - # In case the class requires some arguments in its constructor they can be defined in the "args" dict + # In case the class requires some arguments in its constructor + # they can be defined in the "args" dict if "args" in opt and opt["args"]: instance = clazz(**opt["args"]) else: instance = clazz() - # _get_instance_with_sub_options is mostly called when using the --modules option, so it makes sense to also + # _get_instance_with_sub_options is mostly called when using the --modules option, + # so it makes sense to also # save the git revision of the given module, since it can be in a different git # Directory of this file @@ -62,7 +59,8 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: instance.metrics.add_metric_batch({ "module_git_revision_number": git_revision_number, - "module_git_uncommitted_changes": git_uncommitted_changes}) + "module_git_uncommitted_changes": git_uncommitted_changes + }) # sub_options inherits 'dir' if sub_options and "dir" in opt: @@ -72,11 +70,12 @@ def _get_instance_with_sub_options(options: list[dict], name: str) -> any: instance.sub_options = sub_options return instance + logging.error(f"{name} not found in {options}") raise ValueError(f"{name} not found in {options}") -def _import_class(module_path: str, class_name: str, base_dir: str = None) -> type: +def _import_class(module_path: str, class_name: str, base_dir: str = None) -> any: """ Helper function which allows to replace hard-coded imports of the form 'import MyClass from path.to.mypkg' by calling _import_class('path.to.mypkg', 'MyClass'). @@ -84,11 +83,8 @@ def _import_class(module_path: str, class_name: str, base_dir: str = None) -> ty unless it's already contained in it. :param module_path: Python module path of the module containing the class to be imported - :type module_path: str :param class_name: Name of the class to be imported - :type class_name: str :return: Imported class object - :rtype: type """ # Make sure that base_dir is in the search path. @@ -103,23 +99,19 @@ def _import_class(module_path: str, class_name: str, base_dir: str = None) -> ty def checkbox(key: str, message: str, choices: list) -> dict: """ - Wrapper method to avoid empty responses in checkbox + Wrapper method to avoid empty responses in checkboxes. :param key: Key for response dict - :type key: str :param message: Message for the user - :type message: str :param choices: Choices for the user - :type choices: list :return: Dict with the response from the user - :rtype: dict """ - if len(choices) > 1: answer = inquirer.prompt([inquirer.Checkbox(key, message=message, choices=choices)]) else: if len(choices) == 1: - logging.info(f"Skipping asking for submodule, since only 1 option ({choices[0]}) is available.") + logging.info(f"Skipping asking for submodule" + f"since only 1 option ({choices[0]}) is available.") return {key: choices} if not answer[key]: @@ -129,28 +121,27 @@ def checkbox(key: str, message: str, choices: list) -> dict: return answer -def get_git_revision(git_dir: str) -> (str, str): +def get_git_revision(git_dir: str) -> tuple[str, str]: """ - Collects git revision number and checks if there are uncommitted changes to allow user to analyze which - codebase was used + Collects git revision number and checks if there are uncommitted changes + to allow user to analyze which codebase was used. :param git_dir: Directory of the git repository - :type git_dir: str :return: Tuple with git_revision_number, git_uncommitted_changes - :rtype: (str, str) """ try: - # '-C', git_dir ensures that the following commands also work when QUARK is started from other working - # directories - git_revision_number = subprocess.check_output(['git', '-C', git_dir, 'rev-parse', 'HEAD']).decode( - 'ascii').strip() + # '-C', git_dir ensures that the following commands also work + # when QUARK is started from other working directories + git_revision_number = subprocess.check_output( + ['git', '-C', git_dir, 'rev-parse', 'HEAD']).decode('ascii').strip() git_uncommitted_changes = bool(subprocess.check_output( ['git', '-C', git_dir, 'status', '--porcelain', '--untracked-files=no']).decode( 'ascii').strip()) logging.info( f"Codebase is based on revision {git_revision_number} and has " - f"{'some' if git_uncommitted_changes else 'no'} uncommitted changes") + f"{'some' if git_uncommitted_changes else 'no'} uncommitted changes" + ) except Exception as e: logging.warning(f"Logging of git revision number not possible because of: {e}") git_revision_number = "unknown" @@ -159,20 +150,18 @@ def get_git_revision(git_dir: str) -> (str, str): return git_revision_number, git_uncommitted_changes +#autopep8: off def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: """ Expands the paths given as value of the 'dir' attribute appearing in the QUARK modules - configuration by joining base_dir with that path + configuration by joining base_dir with that path. - :param j: the json to be adapted - expected to be a QUARK modules configuration or a part of it - :type j: dict|list - :param base_dir: the base directory to be used for path expansion - :type base_dir: str - :return: the adapted json - :rtype: dict|list + :param j: The JSON to be adapted - expected to be a QUARK modules configuration or a part of it + :param base_dir: The base directory to be used for path expansion + :return: The adapted JSON """ assert type(j) in [dict, list], f"unexpected type:{type(j)}" - if type(j) == list: + if type(j) is list: for entry in j: _expand_paths(entry, base_dir) else: @@ -188,22 +177,19 @@ def _expand_paths(j: Union[dict, list], base_dir: str) -> Union[dict, list]: def start_time_measurement() -> float: """ - Starts a time measurement + Starts a time measurement. :return: Starting point - :rtype: float """ return time.perf_counter() def end_time_measurement(start: float) -> float: """ - Returns the result of the time measurement in milliseconds + Returns the result of the time measurement in milliseconds. :param start: Starting point for the measurement - :type start: float :return: Time elapsed in ms - :rtype: float """ end = time.perf_counter() return round((end - start) * 1000, 3) @@ -211,7 +197,7 @@ def end_time_measurement(start: float) -> float: def stop_watch(position: int = None) -> Callable: """ - Usage as decorator to measure time, eg: + Usage as decorator to measure time, e.g.: ``` @stop_watch() def run(input_data,...): @@ -226,10 +212,8 @@ def run(input_data,...): measured time is to be inserted in the return tuple. :param position: The position at which the measured time gets inserted in the return tuple. - If not specified the measured time will be appended to the original return value. - :type position: int + If not specified the measured time will be appended to the original return value. :return: The wrapper function - :rtype: Callable """ def wrap(func): def wrapper(*args, **kwargs): diff --git a/src/utils_mpi.py b/src/utils_mpi.py index 7fd6c2a2..a188b4b7 100644 --- a/src/utils_mpi.py +++ b/src/utils_mpi.py @@ -17,6 +17,12 @@ def is_running_mpiexec(): + """ + Determines if the script is running under mpiexec. + + :return: True if running under mpiexec, False otherwise + :rtype: bool + """ # This is not 100% robust but should cover MPICH & Open MPI for key in os.environ: if key.startswith("PMI_") or key.startswith("OMPI_COMM_WORLD_"): @@ -25,56 +31,78 @@ def is_running_mpiexec(): def is_running_mpi(): + """ + Determines if the MPI environment is available and import mpi4py if so. + + :return: MPI object if available, None otherwise + :rtype: MPI or None + """ if is_running_mpiexec(): try: from mpi4py import MPI # pylint: disable=C0415 except ImportError as e: raise RuntimeError( 'it seems you are running mpiexec/mpirun but mpi4py cannot be ' - 'imported, maybe you forgot to install it?') from e + 'imported, maybe you forgot to install it?' + ) from e else: MPI = None return MPI class MPIStreamHandler(logging.StreamHandler): + """ + A logging handler that only emits records from the root process in an MPI environment. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) MPI = is_running_mpi() - if MPI: - self.rank = MPI.COMM_WORLD.Get_rank() - else: - self.rank = 0 + self.rank = MPI.COMM_WORLD.Get_rank() if MPI else 0 + + def emit(self, record) -> None: + """ + Emits a log record if running on the root process. - def emit(self, record): - # don't log unless I am the root process + :param record: Log record + :type record: Logging.LOgRecord + """ if self.rank == 0: super().emit(record) class MPIFileHandler(logging.FileHandler): + """ + A logging handler that only emits records to a file from the root process in an MPI environment. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) MPI = is_running_mpi() - if MPI: - self.rank = MPI.COMM_WORLD.Get_rank() - else: - self.rank = 0 + self.rank = MPI.COMM_WORLD.Get_rank() if MPI else 0 - def emit(self, record): - # don't log unless I am the root process + def emit(self, record) -> None: + """ + Emits a log record if running on the root process. + + :param record: Log record + :type record: Logging.LOgRecord + """ if self.rank == 0: super().emit(record) -def get_comm(): - MPI = is_running_mpi() - if MPI: - comm = MPI.COMM_WORLD +def get_comm() -> any: + """ + Retrieves the MPI communicator if running in an MPI environment, otherwise provides a mock comm class. + + return: MPI communicator or a mock class with limited methods + """ + mpi = is_running_mpi() + if mpi: + Comm = mpi.COMM_WORLD else: - class comm(): + class Comm(): @staticmethod def Get_rank(): return 0 @@ -86,4 +114,5 @@ def Bcast(loss, root): @staticmethod def Barrier(): pass - return comm + + return Comm