diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1354a4165..9277e412f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -143,6 +143,14 @@ jobs: with: name: ml${{ matrix.python-version }} path: ./m${{ matrix.python-version }}/* + - name: Machine Learning Unit Tests without torch under Python ${{ matrix.python-version }} + run: | + pip uninstall -y torch + if [ "${{ github.event_name }}" == "schedule" ]; then + export QISKIT_TESTS="run_slow" + fi + stestr --test-path test run + shell: bash Tutorials: runs-on: ubuntu-latest strategy: @@ -163,7 +171,7 @@ jobs: - uses: ./.github/actions/install-machine-learning - name: Install Dependencies run: | - pip install -U jupyter sphinx nbsphinx sphinx_rtd_theme 'matplotlib<3.3.0' qiskit-terra[visualization] + pip install -U jupyter sphinx nbsphinx sphinx_rtd_theme 'matplotlib<3.3.0' qiskit-terra[visualization] torchvision sudo apt-get install -y pandoc graphviz shell: bash - name: Run Machine Learning Tutorials diff --git a/setup.py b/setup.py index a367e111b..18b919e01 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ include_package_data=True, python_requires=">=3.6", extras_require={ - 'torch': ["torch", "torchvision"], + 'torch': ["torch"], }, zip_safe=False ) diff --git a/test/__init__.py b/test/__init__.py index b6a559055..08f7ccb9b 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -12,6 +12,10 @@ """ ML test packages """ -from .machine_learning_test_case import QiskitMachineLearningTestCase +from .machine_learning_test_case import (QiskitMachineLearningTestCase, + requires_extra_library) -__all__ = ['QiskitMachineLearningTestCase'] +__all__ = [ + 'QiskitMachineLearningTestCase', + 'requires_extra_library' +] diff --git a/test/algorithms/distribution_learners/qgan/test_qgan.py b/test/algorithms/distribution_learners/qgan/test_qgan.py index 6bc7cbcf8..95678c208 100644 --- a/test/algorithms/distribution_learners/qgan/test_qgan.py +++ b/test/algorithms/distribution_learners/qgan/test_qgan.py @@ -15,12 +15,11 @@ import unittest import warnings import tempfile -from test import QiskitMachineLearningTestCase +from test import QiskitMachineLearningTestCase, requires_extra_library from qiskit import BasicAer from qiskit.circuit.library import UniformDistribution, RealAmplitudes from qiskit.utils import algorithm_globals, QuantumInstance -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.algorithms.optimizers import CG, COBYLA from qiskit.opflow.gradients import Gradient from qiskit_machine_learning.algorithms import (NumPyDiscriminator, @@ -116,72 +115,68 @@ def test_qgan_training(self): trained_qasm = self.qgan.run(self.qi_qasm) self.assertAlmostEqual(trained_qasm['rel_entr'], trained_statevector['rel_entr'], delta=0.1) + @requires_extra_library def test_qgan_training_run_algo_torch(self): """Test QGAN training using a PyTorch discriminator.""" - try: - # Set number of qubits per data dimension as list of k qubit values[#q_0,...,#q_k-1] - num_qubits = [2] - # Batch size - batch_size = 100 - # Set number of training epochs - num_epochs = 5 - _qgan = QGAN(self._real_data, - self._bounds, - num_qubits, - batch_size, - num_epochs, - discriminator=PyTorchDiscriminator(n_features=len(num_qubits)), - snapshot_dir=None) - _qgan.seed = self.seed - _qgan.set_generator() - trained_statevector = _qgan.run(QuantumInstance( - BasicAer.get_backend('statevector_simulator'), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed)) - trained_qasm = _qgan.run(QuantumInstance(BasicAer.get_backend('qasm_simulator'), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed)) - self.assertAlmostEqual(trained_qasm['rel_entr'], - trained_statevector['rel_entr'], delta=0.1) - except MissingOptionalLibraryError: - self.skipTest('pytorch not installed, skipping test') + # Set number of qubits per data dimension as list of k qubit values[#q_0,...,#q_k-1] + num_qubits = [2] + # Batch size + batch_size = 100 + # Set number of training epochs + num_epochs = 5 + _qgan = QGAN(self._real_data, + self._bounds, + num_qubits, + batch_size, + num_epochs, + discriminator=PyTorchDiscriminator(n_features=len(num_qubits)), + snapshot_dir=None) + _qgan.seed = self.seed + _qgan.set_generator() + trained_statevector = _qgan.run(QuantumInstance( + BasicAer.get_backend('statevector_simulator'), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed)) + trained_qasm = _qgan.run(QuantumInstance(BasicAer.get_backend('qasm_simulator'), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed)) + self.assertAlmostEqual(trained_qasm['rel_entr'], + trained_statevector['rel_entr'], delta=0.1) + @requires_extra_library def test_qgan_training_run_algo_torch_multivariate(self): """Test QGAN training using a PyTorch discriminator, for multivariate distributions.""" - try: - # Set number of qubits per data dimension as list of k qubit values[#q_0,...,#q_k-1] - num_qubits = [1, 2] - # Batch size - batch_size = 100 - # Set number of training epochs - num_epochs = 5 + # Set number of qubits per data dimension as list of k qubit values[#q_0,...,#q_k-1] + num_qubits = [1, 2] + # Batch size + batch_size = 100 + # Set number of training epochs + num_epochs = 5 - # Reshape data in a multi-variate fashion - # (two independent identically distributed variables, - # each represented by half of the generated samples) - real_data = self._real_data.reshape((-1, 2)) - bounds = [self._bounds, self._bounds] + # Reshape data in a multi-variate fashion + # (two independent identically distributed variables, + # each represented by half of the generated samples) + real_data = self._real_data.reshape((-1, 2)) + bounds = [self._bounds, self._bounds] - _qgan = QGAN(real_data, - bounds, - num_qubits, - batch_size, - num_epochs, - discriminator=PyTorchDiscriminator(n_features=len(num_qubits)), - snapshot_dir=None) - _qgan.seed = self.seed - _qgan.set_generator() - trained_statevector = _qgan.run(QuantumInstance( - BasicAer.get_backend('statevector_simulator'), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed)) - trained_qasm = _qgan.run(QuantumInstance(BasicAer.get_backend('qasm_simulator'), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed)) - self.assertAlmostEqual(trained_qasm['rel_entr'], - trained_statevector['rel_entr'], delta=0.1) - except MissingOptionalLibraryError: - self.skipTest('pytorch not installed, skipping test') + _qgan = QGAN(real_data, + bounds, + num_qubits, + batch_size, + num_epochs, + discriminator=PyTorchDiscriminator(n_features=len(num_qubits)), + snapshot_dir=None) + _qgan.seed = self.seed + _qgan.set_generator() + trained_statevector = _qgan.run(QuantumInstance( + BasicAer.get_backend('statevector_simulator'), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed)) + trained_qasm = _qgan.run(QuantumInstance(BasicAer.get_backend('qasm_simulator'), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed)) + self.assertAlmostEqual(trained_qasm['rel_entr'], + trained_statevector['rel_entr'], delta=0.1) def test_qgan_training_run_algo_numpy(self): """Test QGAN training using a NumPy discriminator.""" diff --git a/test/connectors/test_torch_connector.py b/test/connectors/test_torch_connector.py index 03560f63b..5658f25ad 100644 --- a/test/connectors/test_torch_connector.py +++ b/test/connectors/test_torch_connector.py @@ -16,7 +16,7 @@ from typing import List -from test import QiskitMachineLearningTestCase +from test import QiskitMachineLearningTestCase, requires_extra_library import numpy as np @@ -32,7 +32,7 @@ class Tensor: # type: ignore pass from qiskit import QuantumCircuit -from qiskit.providers.aer import QasmSimulator, StatevectorSimulator +from qiskit.providers.aer import AerSimulator, StatevectorSimulator from qiskit.exceptions import MissingOptionalLibraryError from qiskit.circuit import Parameter from qiskit.utils import QuantumInstance @@ -52,7 +52,7 @@ def setUp(self): # specify quantum instances self.sv_quantum_instance = QuantumInstance(StatevectorSimulator()) - self.qasm_quantum_instance = QuantumInstance(QasmSimulator(), shots=100) + self.qasm_quantum_instance = QuantumInstance(AerSimulator(), shots=100) def validate_output_shape(self, model: TorchConnector, test_data: List[Tensor]) -> None: """Creates a Linear PyTorch module with the same in/out dimensions as the given model, @@ -129,6 +129,7 @@ def validate_backward_pass(self, model: TorchConnector) -> None: @data( 'sv', 'qasm' ) + @requires_extra_library def test_opflow_qnn_1_1(self, q_i): """ Test Torch Connector + Opflow QNN with input/output dimension 1/1.""" @@ -149,27 +150,25 @@ def test_opflow_qnn_1_1(self, q_i): # construct QNN with statevector simulator qnn = TwoLayerQNN(1, feature_map, ansatz, quantum_instance=quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor(1), - Tensor([1]), - Tensor([1, 2]), - Tensor([[1], [2]]), - Tensor([[[1], [2]], [[3], [4]]]) - ] - - # test model - self.validate_output_shape(model, test_data) - if q_i == 'sv': - self.validate_backward_pass(model) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + model = TorchConnector(qnn) + + test_data = [ + Tensor(1), + Tensor([1]), + Tensor([1, 2]), + Tensor([[1], [2]]), + Tensor([[[1], [2]], [[3], [4]]]) + ] + + # test model + self.validate_output_shape(model, test_data) + if q_i == 'sv': + self.validate_backward_pass(model) @data( 'sv', 'qasm' ) + @requires_extra_library def test_opflow_qnn_2_1(self, q_i): """ Test Torch Connector + Opflow QNN with input/output dimension 2/1.""" @@ -180,27 +179,25 @@ def test_opflow_qnn_2_1(self, q_i): # construct QNN qnn = TwoLayerQNN(2, quantum_instance=quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor(1), - Tensor([1, 2]), - Tensor([[1, 2]]), - Tensor([[1], [2]]), - Tensor([[[1], [2]], [[3], [4]]]) - ] - - # test model - self.validate_output_shape(model, test_data) - if q_i == 'sv': - self.validate_backward_pass(model) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + model = TorchConnector(qnn) + + test_data = [ + Tensor(1), + Tensor([1, 2]), + Tensor([[1, 2]]), + Tensor([[1], [2]]), + Tensor([[[1], [2]], [[3], [4]]]) + ] + + # test model + self.validate_output_shape(model, test_data) + if q_i == 'sv': + self.validate_backward_pass(model) @data( 'sv', 'qasm' ) + @requires_extra_library def test_opflow_qnn_2_2(self, q_i): """ Test Torch Connector + Opflow QNN with input/output dimension 2/2.""" @@ -241,22 +238,19 @@ def test_opflow_qnn_2_2(self, q_i): qnn = OpflowQNN(op, [params_1[0], params_2[0]], [params_1[1], params_2[1]], quantum_instance=quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor(1), - Tensor([1, 2]), - Tensor([[1], [2]]), - Tensor([[1, 2], [3, 4]]) - ] - - # test model - self.validate_output_shape(model, test_data) - if q_i == 'sv': - self.validate_backward_pass(model) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + model = TorchConnector(qnn) + + test_data = [ + Tensor(1), + Tensor([1, 2]), + Tensor([[1], [2]]), + Tensor([[1, 2], [3, 4]]) + ] + + # test model + self.validate_output_shape(model, test_data) + if q_i == 'sv': + self.validate_backward_pass(model) @data( # interpret, output_shape, sparse, quantum_instance @@ -269,6 +263,7 @@ def test_opflow_qnn_2_2(self, q_i): (lambda x: np.sum(x) % 2, 2, False, 'qasm'), (lambda x: np.sum(x) % 2, 2, True, 'qasm'), ) + @requires_extra_library def test_circuit_qnn_1_1(self, config): """Torch Connector + Circuit QNN with no interpret, dense output, and input/output shape 1/1 .""" @@ -295,22 +290,19 @@ def test_circuit_qnn_1_1(self, config): interpret=interpret, output_shape=output_shape, quantum_instance=quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor(1), - Tensor([1, 2]), - Tensor([[1], [2]]), - Tensor([[[1], [2]], [[3], [4]]]) - ] - - # test model - self.validate_output_shape(model, test_data) - if q_i == 'sv': - self.validate_backward_pass(model) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + model = TorchConnector(qnn) + + test_data = [ + Tensor(1), + Tensor([1, 2]), + Tensor([[1], [2]]), + Tensor([[[1], [2]], [[3], [4]]]) + ] + + # test model + self.validate_output_shape(model, test_data) + if q_i == 'sv': + self.validate_backward_pass(model) @data( # interpret, output_shape, sparse, quantum_instance @@ -323,6 +315,7 @@ def test_circuit_qnn_1_1(self, config): (lambda x: np.sum(x) % 2, 2, False, 'qasm'), (lambda x: np.sum(x) % 2, 2, True, 'qasm'), ) + @requires_extra_library def test_circuit_qnn_1_8(self, config): """Torch Connector + Circuit QNN with no interpret, dense output, and input/output shape 1/8 .""" @@ -349,22 +342,19 @@ def test_circuit_qnn_1_8(self, config): interpret=interpret, output_shape=output_shape, quantum_instance=quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor(1), - Tensor([1, 2]), - Tensor([[1], [2]]), - Tensor([[[1], [2]], [[3], [4]]]) - ] - - # test model - self.validate_output_shape(model, test_data) - if q_i == 'sv': - self.validate_backward_pass(model) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + model = TorchConnector(qnn) + + test_data = [ + Tensor(1), + Tensor([1, 2]), + Tensor([[1], [2]]), + Tensor([[[1], [2]], [[3], [4]]]) + ] + + # test model + self.validate_output_shape(model, test_data) + if q_i == 'sv': + self.validate_backward_pass(model) @data( # interpret, output_shape, sparse, quantum_instance @@ -377,6 +367,7 @@ def test_circuit_qnn_1_8(self, config): (lambda x: np.sum(x) % 2, 2, False, 'qasm'), (lambda x: np.sum(x) % 2, 2, True, 'qasm'), ) + @requires_extra_library def test_circuit_qnn_2_4(self, config): """Torch Connector + Circuit QNN with no interpret, dense output, and input/output shape 1/8 .""" @@ -404,29 +395,27 @@ def test_circuit_qnn_2_4(self, config): interpret=interpret, output_shape=output_shape, quantum_instance=quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor(1), - Tensor([1, 2]), - Tensor([[1], [2]]), - Tensor([[1, 2], [3, 4]]), - Tensor([[[1], [2]], [[3], [4]]]) - ] - - # test model - self.validate_output_shape(model, test_data) - if q_i == 'sv': - self.validate_backward_pass(model) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) + model = TorchConnector(qnn) + + test_data = [ + Tensor(1), + Tensor([1, 2]), + Tensor([[1], [2]]), + Tensor([[1, 2], [3, 4]]), + Tensor([[[1], [2]], [[3], [4]]]) + ] + + # test model + self.validate_output_shape(model, test_data) + if q_i == 'sv': + self.validate_backward_pass(model) @data( # interpret (None), (lambda x: np.sum(x) % 2) ) + @requires_extra_library def test_circuit_qnn_sampling(self, interpret): """Test Torch Connector + Circuit QNN for sampling.""" @@ -447,22 +436,20 @@ def test_circuit_qnn_sampling(self, interpret): interpret=interpret, output_shape=None, quantum_instance=self.qasm_quantum_instance) - try: - model = TorchConnector(qnn) - - test_data = [ - Tensor([2, 2]), - Tensor([[1, 1], [2, 2]]) - ] - for i, x in enumerate(test_data): - if i == 0: - self.assertEqual(model(x).shape, qnn.output_shape) - else: - shape = model(x).shape - self.assertEqual(shape, (len(x), *qnn.output_shape)) - except MissingOptionalLibraryError as ex: - self.skipTest(str(ex)) - + model = TorchConnector(qnn) + + test_data = [ + Tensor([2, 2]), + Tensor([[1, 1], [2, 2]]) + ] + for i, x in enumerate(test_data): + if i == 0: + self.assertEqual(model(x).shape, qnn.output_shape) + else: + shape = model(x).shape + self.assertEqual(shape, (len(x), *qnn.output_shape)) + + @requires_extra_library def test_batch_gradients(self): """Test backward pass for batch input.""" @@ -526,7 +513,8 @@ def test_batch_gradients(self): batch_res_model = sum(model(Tensor(x))) batch_res_model.backward() self.assertAlmostEqual( - np.linalg.norm(model.weights.grad.numpy() - batch_grad.transpose()[0]), 0.0, places=4) + np.linalg.norm(model.weights.grad.numpy() - batch_grad.transpose()[0]), + 0.0, places=4) if __name__ == '__main__': diff --git a/test/machine_learning_test_case.py b/test/machine_learning_test_case.py index cd4c6734d..5fb3bc31d 100644 --- a/test/machine_learning_test_case.py +++ b/test/machine_learning_test_case.py @@ -20,6 +20,7 @@ import os import unittest import time +from qiskit.exceptions import MissingOptionalLibraryError # disable deprecation warnings that can cause log output overflow # pylint: disable=unused-argument @@ -33,6 +34,26 @@ def _noop(*args, **kargs): # warnings.warn = _noop +def requires_extra_library(test_item): + """Decorator that skips test if an extra library is not available + + Args: + test_item (callable): function to be decorated. + + Returns: + callable: the decorated function. + """ + + def wrapper(self, *args): + try: + test_item(self, *args) + except MissingOptionalLibraryError as ex: + self.skipTest(str(ex)) + return wrapper + + return wrapper + + class QiskitMachineLearningTestCase(unittest.TestCase, ABC): """Machine Learning Test Case""" diff --git a/test/neural_networks/test_circuit_qnn.py b/test/neural_networks/test_circuit_qnn.py index 9e56ed613..d707a670b 100644 --- a/test/neural_networks/test_circuit_qnn.py +++ b/test/neural_networks/test_circuit_qnn.py @@ -21,7 +21,7 @@ import numpy as np from sparse import SparseArray -from qiskit.providers.aer import QasmSimulator, StatevectorSimulator +from qiskit.providers.aer import AerSimulator, StatevectorSimulator from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap from qiskit.utils import QuantumInstance @@ -39,7 +39,7 @@ def setUp(self): # specify "run configuration" self.quantum_instance_sv = QuantumInstance(StatevectorSimulator()) - self.quantum_instance_qasm = QuantumInstance(QasmSimulator(shots=100)) + self.quantum_instance_qasm = QuantumInstance(AerSimulator(shots=100)) # define feature map and ansatz num_qubits = 2 diff --git a/test/neural_networks/test_opflow_qnn.py b/test/neural_networks/test_opflow_qnn.py index b1e6d4dac..a9f4ce709 100644 --- a/test/neural_networks/test_opflow_qnn.py +++ b/test/neural_networks/test_opflow_qnn.py @@ -20,7 +20,7 @@ from ddt import ddt, data import numpy as np -from qiskit.providers.aer import QasmSimulator, StatevectorSimulator +from qiskit.providers.aer import AerSimulator, StatevectorSimulator from qiskit.circuit import Parameter, QuantumCircuit from qiskit.opflow import PauliExpectation, Gradient, StateFn, PauliSumOp, ListOp from qiskit.utils import QuantumInstance @@ -38,7 +38,7 @@ def setUp(self): # specify quantum instances self.sv_quantum_instance = QuantumInstance(StatevectorSimulator()) - self.qasm_quantum_instance = QuantumInstance(QasmSimulator(shots=100)) + self.qasm_quantum_instance = QuantumInstance(AerSimulator(shots=100)) def validate_output_shape(self, qnn: OpflowQNN, test_data: List[np.ndarray]) -> None: """