From 4cb18471a01334d63f19e60c52884851e1cc9f48 Mon Sep 17 00:00:00 2001 From: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:19:14 -0400 Subject: [PATCH] New Qiskit device prototype (#513) * New Qiskit device prototype (#350) * initial prototype device * add use_primitives kwarg * reorganize circuit conversion part 1 * move circuit translation out of device * estimator execution * some small improvements for codefactor * allow circuits with mixed MP types * move translation functions * add support for broadcast_expand and session * add kwargs for options * tidy up options and session * warn if using non-primitive measurements * remove context manager for device session * refactor handling of Options and kwargs * cleaning up * fix 'c register already exists' hardware error * change Options update interface * don't allow options to override shots * don't use classical reg in estimator circuits * update docstring * add conversion tests * add observable conversion test * fix bug in PauliOps converter * add more conversion test functions * remove Adjoint from supported ops and tidy up * tests and little fixes * add tests and black formatting * fix wire order bug for Estimator returns * add tests * remove error if device doesn't initialize * try to get CI to run * more CI stuff * try a thing * black formatting * fix typo * add missing skip-if-no-account * update converter tests * add mockers to allow tests to run in CI * temporarily comment out integration tests * get service from backend * add mock service to mock backend * mock calls to Session in unit testing * uncomment the other tests again * black formatting * newer black formatting * add mocked tests for main execute method * add MockSession to mocked execution tests * pylint * add backend to MockSession calls * mock tests for _execute methods * black formatting * mock transpile for execute_runtime_service * add name to MockedBackend * Apply suggestions from code review Co-authored-by: Matthew Silverman Co-authored-by: Astral Cai * Add barrier to ops list * apply suggestion from code review Co-authored-by: Astral Cai * revert adding barrier for now * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Matthew Silverman * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Matthew Silverman * Small performance change * Revert * pin qiskit for now * Pin qiskit-ibm-runtime * Move function in init to update_kwargs * Delete print statement lol * fix for shot information * name property change * fixes * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Matthew Silverman * Edited docstrings * Function signature * Shots are now with context manager * black reformat * black reformat * test changes * tests * fixed conflicts * Change mp_to_pauli to accept mps that affect more than 1 qubit * Revert to fix CI tests * black reformat * Update setup.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update requirements-ci.txt Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * tests * Fixed a test * maybe this works? * placeholder settings * fixed CI tests, bandaid fix * gitignore for 1.0 qiskit versioning * delete venv1 * tests, shots information, shot vector case, ibm_run_time compatibility * woops * docstring for operation_to_qiskit * black * appease codecov * mock test * Deleted comment * remove some notebooks * Update .gitignore Co-authored-by: Astral Cai * tests and docstrings --------- Co-authored-by: Matthew Silverman Co-authored-by: Astral Cai Co-authored-by: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Co-authored-by: Austin Huang Co-authored-by: Astral Cai * Add compile_backend kwarg (#398) Adds the compile_backend kwarg to the Qiskit device. It is useful when you want to do circuit transpilation when using the old Qiskit API (e.g. use_primitives = False). * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh --------- Co-authored-by: Austin Huang Co-authored-by: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Co-authored-by: Utkarsh * Support both V1 and V2 (#399) * add name to MockedBackend * support both V1 and V2 syntax for retrieving backend name and num_qubits * test relevant methods with both V1 and V2 MockBackends * tests updated for old device api as per other PR * tests * Update tests/test_base_device.py Co-authored-by: Utkarsh * Apply suggestions from code review Co-authored-by: Utkarsh * merge conf --------- Co-authored-by: Austin Huang <65315367+austingmhuang@users.noreply.github.com> Co-authored-by: Austin Huang Co-authored-by: Utkarsh * Scalar tests passing * Change mp_to_pauli to be more general * Hamiltonian testing * tests * Revert "tests" This reverts commit 4f21e7ddf354cc36efee55c5d50e4f978c3d2729. * Revert "Hamiltonian testing" This reverts commit 17c3e674235709a7c5e1baf037ebf37b42f3efbf. * Revert "Change mp_to_pauli to be more general" This reverts commit a1ff60638f19d302a802f46a4d586a616e0be95c. * revert * Install PL dev intead of PL stable on CI (#516) (#518) * install PL dev intead of PL stable on CI * trigger ci * also a couple of other places Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Delete ds store * import change * black * some changes * linters * black * pylinting * black * small change * change conftest * black linter * fix * Generalize mp_to_pauli so that it can work with any observable that has a Pauli Rep (#517) * tests * converter * fixes to tests * refactor * delete print * black * pylint changes * deleted tests/pylintrc * one additional test * black * tests * redo commits * clean up * black * formatting * black * Update pennylane_qiskit/converter.py Co-authored-by: Utkarsh * rewrite tests to all manual cases * [skip-ci] black * single qubit operations * value error for no pauli rep * list comprehension * removed one list comprehension * ehh honestly this is fine too * delete some useless lines * [skip-ci] refactor * [skip ci] accidentally deleted something...? * Update tests/test_converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/converter.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * remove parametrize * [skip ci] black * [skip ci] docstring * [skip ci] added assert statement * [skip-ci] linear comb changes * additional more complicated integration tests * undo some formatting * formats * integration tests * tests * deleted unsupported observables * remove sparsehamiltonian for now * rollback pylint * revert observable stopping condition * black * just a commit * black * fm * Update tests/test_base_device.py Co-authored-by: Utkarsh * linter errors * linter * linter * linters * try finally block better? * revert * let's just do this... * trailing whitespace * linter * black * formats * black * black * clean up commits * reformatting fixed * clean up commit --------- Co-authored-by: Utkarsh Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Use old _execute_runtime_service for observables that are not compatible with SPO representation (#525) * Functionality * syntax error * additional test * additional test cases * edit comment * comment * additional comments * added a warning * added a warning * delete print statement * warning test * delete a test * black * Minor adjustment to sort observables. Modified tests to accomodate. Doc strings edit * Modified tests and warning * edit * fixes * moved test to mocked * mockedbackend update * black * changes * test * uncomment * docstring * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * black * fix --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Move off of "ibmq_qasm_simulator" and towards using AerSimulator for qiskit tests (#521) * import aersimulator * commit * black * added functionality for local simulators when use_primitves = False * [skip ci] changes to AerSim * removal of ibm service * tests * woops * temp fixes * unfinished changes * ignore tests due to version errors * ignore tests due to version errors * black * cleanup * delete comment * revert a change * replaced a test case * shouldn't need to skip if no acc anymore * added comments * delete comment * minor refactor * Added additional comments * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * [skip ci] delete comment * add comment --------- Co-authored-by: Utkarsh * Update reqs to 1.0 (#536) * removed legacy ci * Delete .github/workflows/tests.yml * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * removed devices * pylint * pylint * docs fix * setup.py changes * delete * revert * reqs change * setup * change to reqs to match ci * removed a test * pylint * put ibmq.rst back * delete ibmq * remove ibmq * deletion * codecov * lint * changes to tests * Revert "Remove devices that will not be supported in the new release" (#544) * changelogs * Remove basicaer (#546) * removed legacy ci * Delete .github/workflows/tests.yml * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * removed devices * pylint * pylint * docs fix * setup.py changes * delete * revert * reqs change * setup * change to reqs to match ci * removed a test * pylint * put ibmq.rst back * delete ibmq * remove ibmq * deletion * codecov * lint * changes to tests * Revert "Remove devices that will not be supported in the new release" (#544) * docs * remove basicaer * reqs to build docs * docs * pylint * tests fix * path change * Changelog and doc * changelogs * Update CHANGELOG.md * deleted a test file * removed error * basic sim pylint * Update CHANGELOG.md Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * remove ifelse block * pylint --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Remove use primitives and everything that depends on it (#538) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Remove ibmq devices (#550) * Removing ibmq devices from the docs and relevant files * missed something in docs * Changelog updates * Update CHANGELOG.md Co-authored-by: Utkarsh --------- Co-authored-by: Utkarsh * Migrate to v2 primitives (#539) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * deleting unused tests * line change * pylint * yay * docstring * refactoring of estimator and sampler * process_estimator_job tests * comment for clarity --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * pylint * Process kwargs (#547) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * docstring * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * revert to tuple(res) * [skip ci] fix to dimensions of sampler * docstrings * some docstrings changes * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * black * linter --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Docstrings for converter functions and new qiskit device (#552) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * [skip ci] docstrings for converter functions * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * [skip ci] examples of QiskitDevice2 added to docstring * docstring changes * docstring * more docstrings * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * docstrings * docstrings * formatting * changes * some examples * Update pennylane_qiskit/converter.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * examples and links * change inheritance for remote device * build docs * better docs * fix docs a little * remove redundant docstrings * revert * import fix * build sphinx * revert change to QiskitDev2 * Update pennylane_qiskit/remote.py Co-authored-by: Utkarsh * reformat to within 100 chars --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Diagonalize gates (#558) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * docstring * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * revert to tuple(res) * [skip ci] fix to dimensions of sampler * docstrings * some docstrings changes * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * black * [skip ci] import split_non_commuting * diagonalize tests for Hadamard * changed stopping condition to reflect reality of what's supported and added tests and changed tests to fit new stopping condition * [skip ci] added comment about qml.var not providing matching answers * some tests * [skip ci] linter * interesting changes * linter * split non commuting test cases * sprod * sampler tested as well * linter * black * docstrings and comments * docstrings and comments black * comment regarding magic number * todo * add np * more concise way of testing * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update tests/test_base_device.py Co-authored-by: Utkarsh * fix black * diagonalize for edge case * linter * pylint * clean up * fix docstring --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Qiskit session (#551) * functionality implemented * minor adjustments to tests * [skip-ci] Qiskit Sessions now test many warnings since you can set session options on device initialization and when using the session manager. We use the options in the device for things that are generally not updateable for the device e.g. backends; we use session options for everything else * [skip-ci] tests that we are passing on kwargs to Qiskit's session constructor, and verifying that an error is raised due to such behavior * [skip ci] pylint * small comments * Generalization of the session options * delicious docstrings * [skip ci] tests and clarification * [skip ci] better session options * comments for clarity * changes to the tests & the warning message * docstrings * type error changes * docstrings * add qiskit_session to docs * a little more consistency in comments * for docs * fix ci * black * revert * revert * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update tests/test_base_device.py Co-authored-by: Utkarsh * Qiskit Session update * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstring update --------- Co-authored-by: Utkarsh Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Tests with fakehardware (#553) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * docstring * [skip ci] some tests with fakehardware * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * revert to tuple(res) * [skip ci] fix to dimensions of sampler * docstrings * some docstrings changes * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * black * [skip ci] import split_non_commuting * diagonalize tests for Hadamard * changed stopping condition to reflect reality of what's supported and added tests and changed tests to fit new stopping condition * [skip ci] added comment about qml.var not providing matching answers * some tests * [skip ci] linter * interesting changes * linter * split non commuting test cases * sprod * sampler tested as well * linter * black * docstrings and comments * docstrings and comments black * comment regarding magic number * todo * add np * more concise way of testing * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update tests/test_base_device.py Co-authored-by: Utkarsh * fix black * diagonalize for edge case * linter * pylint * clean up * fix docstring * flaky * flaky and fake * rename bakcend to aer_backend --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * fix merge conf * fix setup.py * reqs.txt * changelog changes * Use new qiskit device as the base for remote (#566) * import changes * Added TODOs for tests * changes * this should pass * pylint * circular import * observables update * remerge * import from qiskitdevice2 * Delete unnecessary tests and mocks * black/pylint * Add tests back in for codecov. * refactor tests * delete legacy device only functionality * change around imports * add assertion * fix * fix * fix setup * clean up * fix reqs.txt * fix * changelog changed * maybe this works? * a docstring? * a docstring? * reverts * does this break * revert * fix * fix * attempt a doc fix * Update tests/test_integration.py Co-authored-by: Utkarsh * some docstrings --------- Co-authored-by: obliviateandsurrender * Plugin page update (#563) * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] minor formatting * [skip ci] docstrings for converter functions * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * [skip ci] examples of QiskitDevice2 added to docstring * docstring changes * docstring * more docstrings * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * clean up * refactor * pylint * formatting of docstring * docstrings * docstrings * formatting * changes * some examples * Update pennylane_qiskit/converter.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * examples and links * change inheritance for remote device * build docs * better docs * fix docs a little * remove redundant docstrings * revert * import fix * prelim changes to aer * changes to build sphinx * revert change * delete section on ibmq devices * add examples for remote.rst * plugin updates to the remote device and basicsim * doc fixes for sphinx build * docs * small fi * fix weird spacing * [skip ci] small fix * [skip ci] error in codeblock * delete extra the * [skip ci] changelog * merge confs * formatting * black * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * address comments * undo * readme * plugin page fixes * fixed documentation * fix * Update doc/index.rst Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * doc changes * typo * weird change didn't go through * change to doc * small change * Update doc/devices/remote.rst Co-authored-by: Utkarsh * Update doc/devices/remote.rst Co-authored-by: Utkarsh * change name of iqp token * small fix --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Tracker functionality (#533) * is this all? * doc strings * tests for tracker * trackers * trackers * fixed tests * removed legacy ci * Delete .github/workflows/tests.yml * Deleted use_primitives from tests * remove use_primitive kwarg and things that depend on it * fix tests and split_exec * fixed test * pylint * change to v2 prims, del Options * del Options * temp changes to options * naming and 0.46 test * rename * update qiskit device * dep warnings * pylint * changes to tests * deleted options for now * small changes * access sampler results * Sampler tests and functionality * estimator multi measurement works * estimator now gives variances * comments * removed backend.run() and _execute_runtime * remove additional stuff * linter * docstring changes * skip additional test * [skip ci] format is correct, checks probs as well * [skip ci] docstring * docstrings * We delete the Options Handling class because there are no more Options() to handle. Additionally, process_kwargs is left as a stub as a temporary measure while we figure out what to do with kwargs * changed warnings due to difference in UI for setting shots between Qiskit and Pennylane. Tracking shots has also been updated due to estimatorV2 syntax change * un did confusing change that didnt do anything * rerun ci * Syntax changes due to version change * Delete for codecov * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * docstrings * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * finishing touches * comments * [skip ci] formatting * [skip ci] refactor * due to the fact that shots are not tracked in the estimator's metadata anymore, variances are calculated a different way using the precision instead * black * lint * docstring changes * backend options? * deleting unused tests * line change * [skip ci] changed test to be more readable * New tests for options functionality and edge case * pylint * We make sure that transpilation options are not passed to the primitive and that no errors are raised as a result * Due to changing the signature of get_transpile_args(), we need to fix one of the tests * warning message for default_shots was unclear. changed to be more clear * add more comments * yay * docstring * Testing to ensure that options and kwargs combine properly * edit tests for pylint * woops * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * Update pennylane_qiskit/qiskit_device2.py Co-authored-by: Utkarsh * edit test regex matching due to changes earlier * print out some stuff for tests * refactoring of estimator and sampler * generate samples tested * process_estimator_job tests * comment for clarity * pylint * pylint * [skip ci] temp * [skip ci] There is a bug due to the post processing of results that is causing some of the assertion statements to fail. We can ignore these assertions for now and address how to rework reorder_fn to avoid this bug * [skip ci] * [skip ci] minor formatting * Fix unintended additonal dimensionality and added test for res != 1 testcase * fix to transpiles * comments to explain some stuff * merge conflicts * pylint * formatting * tracker comments * black * comments about the tracker * bet * fix to imports * black * temp * baller implementation * increase shot number to reduce error * edit function * black * better tests * refactor * black * delete print statement * refactor * Delete unneeded import * fix assertion * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * Update tests/test_base_device.py Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * new tests * removed simulations keyword * some xfails pending discussion --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Utkarsh * Update CHANGELOG.md Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> * CHANGELOG --------- Co-authored-by: lillian542 <38584660+lillian542@users.noreply.github.com> Co-authored-by: Matthew Silverman Co-authored-by: Astral Cai Co-authored-by: Astral Cai Co-authored-by: Utkarsh --- .github/workflows/ibmq_tests.yml | 6 +- .github/workflows/ibmq_tests_1.yml | 44 - .github/workflows/tests.yml | 17 +- .github/workflows/tests_qiskit_1.yml | 91 -- CHANGELOG.md | 29 +- README.rst | 20 +- doc/devices/aer.rst | 43 +- doc/devices/basicaer.rst | 32 - doc/devices/basicsim.rst | 3 +- doc/devices/ibmq.rst | 84 -- doc/devices/remote.rst | 118 +- doc/devices/runtime.rst | 21 - doc/index.rst | 63 +- doc/requirements.txt | 8 +- pennylane_qiskit/__init__.py | 5 +- pennylane_qiskit/aer.py | 4 +- pennylane_qiskit/basic_aer.py | 111 -- pennylane_qiskit/basic_sim.py | 49 + pennylane_qiskit/converter.py | 192 ++- pennylane_qiskit/ibmq.py | 149 -- pennylane_qiskit/qiskit_device.py | 965 +++++++------ pennylane_qiskit/qiskit_device_legacy.py | 491 +++++++ pennylane_qiskit/remote.py | 125 +- pennylane_qiskit/runtime_devices.py | 228 ---- requirements-ci-legacy.txt | 5 - requirements.txt | 48 +- setup.py | 5 +- tests/conftest.py | 28 +- tests/test_base_device.py | 1583 ++++++++++++++++++++++ tests/test_converter.py | 378 +++++- tests/test_ibmq.py | 323 ----- tests/test_integration.py | 84 +- tests/test_new_qiskit_temp.py | 57 - tests/test_qiskit_device.py | 18 +- tests/test_runtime.py | 227 ---- tests/test_sample.py | 294 ---- 36 files changed, 3630 insertions(+), 2318 deletions(-) delete mode 100644 .github/workflows/ibmq_tests_1.yml delete mode 100644 .github/workflows/tests_qiskit_1.yml delete mode 100644 doc/devices/basicaer.rst delete mode 100644 doc/devices/ibmq.rst delete mode 100644 doc/devices/runtime.rst delete mode 100644 pennylane_qiskit/basic_aer.py create mode 100644 pennylane_qiskit/basic_sim.py delete mode 100644 pennylane_qiskit/ibmq.py create mode 100644 pennylane_qiskit/qiskit_device_legacy.py delete mode 100644 pennylane_qiskit/runtime_devices.py delete mode 100644 requirements-ci-legacy.txt create mode 100644 tests/test_base_device.py delete mode 100644 tests/test_ibmq.py delete mode 100644 tests/test_new_qiskit_temp.py delete mode 100644 tests/test_runtime.py delete mode 100644 tests/test_sample.py diff --git a/.github/workflows/ibmq_tests.yml b/.github/workflows/ibmq_tests.yml index a094d1b0f..edbf3e7f8 100644 --- a/.github/workflows/ibmq_tests.yml +++ b/.github/workflows/ibmq_tests.yml @@ -1,7 +1,7 @@ -name: IBMQ integration tests +name: IBMQ integration tests with Qiskit 1.0 on: schedule: - - cron: '0 0 * * 0,4' # At 00:00 on Sunday and Thursday. + - cron: '1 0 * * 0,4' # At 01:00 on Sunday and Thursday. workflow_dispatch: jobs: @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-ci-legacy.txt + pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock flaky --upgrade pip freeze diff --git a/.github/workflows/ibmq_tests_1.yml b/.github/workflows/ibmq_tests_1.yml deleted file mode 100644 index edbf3e7f8..000000000 --- a/.github/workflows/ibmq_tests_1.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: IBMQ integration tests with Qiskit 1.0 -on: - schedule: - - cron: '1 0 * * 0,4' # At 01:00 on Sunday and Thursday. - workflow_dispatch: - -jobs: - tests: - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [3.9] - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-ci.txt - pip install wheel pytest pytest-cov pytest-mock flaky --upgrade - pip freeze - - - name: Install Plugin - run: | - pip install git+https://github.com/PennyLaneAI/pennylane-qiskit.git@${{ github.ref }} - pip freeze - - - name: Run tests - # Only run IBMQ and Runtime tests (skipped otherwise) - run: python -m pytest tests -k 'test_ibmq.py or test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native - env: - IBMQX_TOKEN: ${{ secrets.IBMQX_TOKEN_TEST }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3703508a6..1ad3c3b53 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Tests for 1.0 on: push: branches: @@ -30,7 +30,7 @@ jobs: run: | python -m pip install --upgrade pip pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci-legacy.txt + pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock flaky --upgrade pip freeze @@ -39,12 +39,9 @@ jobs: pip install git+https://github.com/PennyLaneAI/pennylane-qiskit.git@${{ github.ref }} pip freeze - - name: Run tests - # Skip IBMQ and Runtime tests as they depend on IBMQ's availability and - # easily result in timeouts + - name: Run standard Qiskit plugin tests + # Run the standard tests with the most recent version of Qiskit run: python -m pytest tests -k 'not test_ibmq.py and not test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native - env: - IBMQX_TOKEN: ${{ secrets.IBMQX_TOKEN_TEST }} - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -72,19 +69,17 @@ jobs: run: | python -m pip install --upgrade pip pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci-legacy.txt + pip install -r requirements-ci.txt pip install wheel pytest pytest-cov pytest-mock pytest-benchmark flaky --upgrade - pip freeze - name: Install Plugin run: | python setup.py bdist_wheel pip install dist/PennyLane*.whl - pip freeze - name: Run tests run: | - pl-device-test --device=qiskit.basicaer --tb=short --skip-ops --shots=20000 --device-kwargs backend=qasm_simulator + pl-device-test --device=qiskit.basicsim --tb=short --skip-ops --shots=20000 --device-kwargs backend=basic_simulator pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=20000 --device-kwargs backend=qasm_simulator pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=statevector_simulator pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=unitary_simulator diff --git a/.github/workflows/tests_qiskit_1.yml b/.github/workflows/tests_qiskit_1.yml deleted file mode 100644 index 1ad3c3b53..000000000 --- a/.github/workflows/tests_qiskit_1.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Tests for 1.0 -on: - push: - branches: - - master - pull_request: - -jobs: - tests: - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [3.9, '3.10', '3.11'] - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci.txt - pip install wheel pytest pytest-cov pytest-mock flaky --upgrade - pip freeze - - - name: Install Plugin - run: | - pip install git+https://github.com/PennyLaneAI/pennylane-qiskit.git@${{ github.ref }} - pip freeze - - - name: Run standard Qiskit plugin tests - # Run the standard tests with the most recent version of Qiskit - run: python -m pytest tests -k 'not test_ibmq.py and not test_runtime.py' --cov=pennylane_qiskit --cov-report=term-missing --cov-report=xml -p no:warnings --tb=native - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.codecov_token }} - file: ./coverage.xml - - integration-tests: - runs-on: ubuntu-latest - - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.4.1 - with: - access_token: ${{ github.token }} - - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install git+https://github.com/PennyLaneAI/pennylane.git - pip install -r requirements-ci.txt - pip install wheel pytest pytest-cov pytest-mock pytest-benchmark flaky --upgrade - - - name: Install Plugin - run: | - python setup.py bdist_wheel - pip install dist/PennyLane*.whl - - - name: Run tests - run: | - pl-device-test --device=qiskit.basicsim --tb=short --skip-ops --shots=20000 --device-kwargs backend=basic_simulator - pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=20000 --device-kwargs backend=qasm_simulator - pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=statevector_simulator - pl-device-test --device=qiskit.aer --tb=short --skip-ops --shots=None --device-kwargs backend=unitary_simulator - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.codecov_token }} - file: ./coverage.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 44c439fc7..9b5e20c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,40 @@ ### New features since last release +* Qiskit Sessions can now be used for the ``qiskit.remote`` device with the ``qiskit_session`` context + manager. + [(#551)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/551) + ### Improvements 🛠 +* Qiskit Runtime Primitives are supported by the ``qiskit.remote`` device. Circuits ran using the ``qiskit.remote`` + device will automatically call the SamplerV2 and EstimatorV2 primitives appropriately. Additionally, runtime options can be passed as keyword arguments directly to the ``qiskit.remote`` device. + [(#513)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/513) + ### Breaking changes 💔 +* Support has been removed for Qiskit versions below 0.46. The minimum required version for Qiskit is now 1.0. + If you want to continue to use older versions of Qiskit with the plugin, please use version 0.36 of + the Pennylane-Qiskit plugin. + [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) + +* The test suite no longer runs for Qiskit versions below 0.46. + [(#536)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/536) + +* The ``qiskit.basicaer`` device has been removed because it is not supported for versions of Qiskit above 0.46. + [(#546)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/546) + +* The IBM quantum devices, ``qiskit.ibmq``, ``qiskit.ibmq.circuit_runner`` and ``qiskit.ibmq.sampler``, have been removed due to deprecations of the IBMProvider and the cloud simulator "ibmq_qasm_simulator". + [(#550)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/550) + ### Deprecations 👋 ### Documentation 📝 +* The Pennylane-Qiskit plugin page has been updated to reflect the changes in both the plugin's +capabilities and Qiskit. + [#563](https://github.com/PennyLaneAI/pennylane-qiskit/pull/563) + ### Bug fixes 🐛 ### Contributors ✍️ @@ -30,8 +56,9 @@ This release contains contributions from (in alphabetical order): ### Contributors ✍️ This release contains contributions from (in alphabetical order): - Utkarsh Azad +Lillian M. A. Frederiksen +Austin Huang Mashhood Khan --- diff --git a/README.rst b/README.rst index fe3651891..eab139fab 100644 --- a/README.rst +++ b/README.rst @@ -68,21 +68,11 @@ To test that the PennyLane-Qiskit plugin is working correctly you can run in the source folder. -.. note:: - - Tests on the `IBMQ device `_ can - only be run if a ``ibmqx_token`` for the - `IBM Q experience `_ is - configured in the `PennyLane configuration file - `_, if the token is - exported in your environment under the name ``IBMQX_TOKEN``, or if you have previously saved your - account credentials using the - `new IBMProvider `_ - - If this is the case, running ``make test`` also executes tests on the ``ibmq`` device. - By default, tests on the ``ibmq`` device run with ``ibmq_qasm_simulator`` backend. At - the time of writing this means that the test are "free". - Please verify that this is also the case for your account. +.. warning:: + + When installing the Pennylane-Qiskit plugin, we recommend starting with a clean environment. + This is especially pertinent when upgrading from a pre-1.0 version of Qiskit, as described + in `Qiskit's migration guide `_. .. installation-end-inclusion-marker-do-not-remove diff --git a/doc/devices/aer.rst b/doc/devices/aer.rst index 2f39c6cf0..1dd332691 100644 --- a/doc/devices/aer.rst +++ b/doc/devices/aer.rst @@ -27,24 +27,19 @@ parameters would look like: qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(wires=1)) -You can then execute the circuit like any other function to get the quantum mechanical expectation value. +You can then execute the circuit like any other function to get the expectation value of a Pauli +operator. .. code-block:: python circuit(0.2, 0.1, 0.3) -Backends -~~~~~~~~ +Backend Methods and Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~ The default backend is the ``AerSimulator``. However, multiple other backends are also available. To get a current overview what backends are available you can query -.. code-block:: python - - dev.capabilities()['backend'] - -or, alternatively, - .. code-block:: python from qiskit_aer import Aer @@ -58,18 +53,28 @@ You can change a ``'qiskit.aer'`` device's backend with the ``backend`` argument .. code-block:: python - dev = qml.device('qiskit.aer', wires=2, backend='aer_simulator_statevector') + from qiskit_aer import UnitarySimulator + dev = qml.device('qiskit.aer', wires=2, backend=UnitarySimulator()) -Backend Methods and Options -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: + + Occassionally, you may see others pass in a string as a backend. For example: + + .. code-block:: python + + dev = qml.device('qiskit.aer', wires=2, backend='unitary_simulator') + + At the time of writing, this is still functional for the Aer devices. However, this will soon be + deprecated and may not function as intended. To ensure accurate results, we recommend passing in + a backend instance. -This ``AerSimulator`` backend has several available methods, which +The ``AerSimulator`` backend has several available methods, which can be passed via the ``method`` keyword argument. For example ``'automatic'``, ``'statevector'``, and ``'unitary'``. .. code-block:: python - dev = qml.device("qiskit.aer", wires=2, method="automatic") + dev = qml.device("qiskit.aer", wires=2, backend=AerSimulator(), method="automatic") Each of these methods can take different *run options*, for example to specify the numerical precision of the simulation. @@ -81,7 +86,7 @@ The options are set via additional keyword arguments: dev = qml.device( 'qiskit.aer', wires=2, - backend='unitary_simulator', + backend=AerSimulator(), validation_threshold=1e-6 ) @@ -96,9 +101,9 @@ documentation `_ qiskit tutorial): +One great feature of the ``'qiskit.aer'`` device is the ability to simulate noise. There are +different noise models, which you can instantiate and apply to the device as follows (adapted +from a `Qiskit tutorial `_.): .. code-block:: python @@ -136,4 +141,4 @@ which you can instantiate and apply to the device as follows print(circuit(0.2, 0.1, 0.3)) Please refer to the Qiskit documentation for more information on -`noise models `_. +`noise models `_. diff --git a/doc/devices/basicaer.rst b/doc/devices/basicaer.rst deleted file mode 100644 index e8746b2c8..000000000 --- a/doc/devices/basicaer.rst +++ /dev/null @@ -1,32 +0,0 @@ -The BasicAer device -=================== - -.. note:: - - Qiskit discontinued their ``BasicAer`` device in the 1.0 release, so this device - is only available for lower versions of Qiskit. For a simple Python simulator - compatible with Qiskit 1.0, use the :ref:`BasicSim device ` instead. - -While the ``'qiskit.aer'`` device is the standard go-to simulator that is provided along -the Qiskit main package installation, there exists a natively included python simulator -that is slower but will work usually without the need to install other dependencies -(C++, BLAS, and so on). This simulator can be used through the device ``'qiskit.basicaer'``: - -.. code-block:: python - - import pennylane as qml - dev = qml.device('qiskit.basicaer', wires=2) - -As with the ``'qiskit.aer'`` device, there are different backends available, which you can find -by calling - -.. code-block:: python - - dev.capabilities()['backend'] - -.. note:: - - Currently, PennyLane does not support the ``'pulse_simulator'`` backend. - -The backends are used in the same manner as specified for the ``'qiskit.aer'`` device. -The ``'qiskit.basicaer'`` device, however, does not support the simulation of noise models. diff --git a/doc/devices/basicsim.rst b/doc/devices/basicsim.rst index cd4060b0b..21c7dad5b 100644 --- a/doc/devices/basicsim.rst +++ b/doc/devices/basicsim.rst @@ -19,6 +19,5 @@ This device uses the Qiskit ``BasicSimulator`` backend from the The `Qiskit Aer `_ device provides a fast simulator that is also capable of simulating - noise. It is available as :ref:`"qiskit.aer" `, but the backend must be - installed separately with ``pip install qiskit-aer``. + noise. It is available as :ref:`"qiskit.aer" `. \ No newline at end of file diff --git a/doc/devices/ibmq.rst b/doc/devices/ibmq.rst deleted file mode 100644 index 880828e54..000000000 --- a/doc/devices/ibmq.rst +++ /dev/null @@ -1,84 +0,0 @@ -IBM Q Experience -================ - -PennyLane-Qiskit supports running PennyLane on IBM Q hardware via the ``qistkit.ibmq`` device. -You can choose between different backends - either simulators tailor-made to emulate the real hardware, -or the real hardware itself. - -Accounts and Tokens -~~~~~~~~~~~~~~~~~~~ - -By default, the ``qiskit.ibmq`` device will attempt to use an already active or stored -IBM Q account. If the device finds no account it will raise an error: - -.. code:: - - 'No active IBM Q account, and no IBM Q token provided. - -You can use the ``qiskit_ibm_provider.IBMProvider.save_account("")`` function to permanently -store an account, and the account will be automatically used from then onward. -Alternatively, you can specify the token with PennyLane via the -`PennyLane configuration file `__ by -adding the section - -.. code:: - - [qiskit.global] - - [qiskit.ibmq] - ibmqx_token = "XXX" - -You may also directly pass your IBM Q API token to the device: - -.. code-block:: python - - dev = qml.device('qiskit.ibmq', wires=2, backend='ibmq_qasm_simulator', ibmqx_token="XXX") - -You may also save your token as an environment variable by running the following in a terminal: - -.. code:: - - export IBMQX_TOKEN= - -.. warning:: Never publish code containing your token online. - -Backends -~~~~~~~~ - -By default, the ``qiskit.ibmq`` device uses the simulator backend -``ibmq_qasm_simulator``, but this may be changed to any of the real backends as returned by - -.. code-block:: python - - dev.capabilities()['backend'] - -Most of the backends of the ``qiskit.ibmq`` device, such as ``ibmq_london`` or ``ibmq_16_melbourne``, -are *hardware backends*. Running PennyLane with these backends means to send the circuit as a job to the actual quantum -computer and retrieve the results via the cloud. - -Specifying providers -~~~~~~~~~~~~~~~~~~~~ - -Custom providers can be passed as arguments when a ``qiskit.ibmq`` device is created: - -.. code-block:: python - - from qiskit_ibm_provider import IBMProvider - provider = IBMProvider("XYZ") - - import pennylane as qml - dev = qml.device('qiskit.ibmq', wires=2, backend='ibmq_qasm_simulator', provider=provider) - -If no provider is passed explicitly, then the official provider options are used, -``hub='ibm-q'``, ``group='open'`` and ``project='main'``. - -Custom provider options can also be passed as keyword arguments when creating a device: - -.. code-block:: python - - import pennylane as qml - dev = qml.device('qiskit.ibmq', wires=2, backend='ibmq_qasm_simulator', - ibmqx_token='XXX', hub='MYHUB', group='MYGROUP', project='MYPROJECT') - -More details on Qiskit providers can be found -in the `IBMQ provider documentation `_. diff --git a/doc/devices/remote.rst b/doc/devices/remote.rst index f2b81b799..a2a04b476 100644 --- a/doc/devices/remote.rst +++ b/doc/devices/remote.rst @@ -4,17 +4,117 @@ The Remote device The ``'qiskit.remote'`` device is a generic adapter to use any Qiskit backend as interface for a PennyLane device. -This device is useful when retrieving backends from providers with complex search options in -their ``get_backend()`` method, or for setting options on a backend prior to wrapping it as -PennyLane device. +To access IBM backends, we recommend using `Qiskit Runtime `_. .. code-block:: python - import pennylane as qml + from qiskit_ibm_runtime import QiskitRuntimeService - def configured_backend(): - backend = SomeProvider.get_backend(...) - backend.options.update_options(...) - return backend + QiskitRuntimeService.save_account(channel="ibm_quantum", token="") - dev = qml.device('qiskit.remote', wires=2, backend=configured_backend()) + # To access saved credentials for the IBM quantum channel and select an instance + service = QiskitRuntimeService(channel="ibm_quantum", instance="my_hub/my_group/my_project") + backend = service.least_busy(operational=True, simulator=False, min_num_qubits=) + + dev = qml.device('qiskit.remote', wires=, backend=backend) + + +.. note:: + + Certain third-party backends may be using the deprecated ``Provider`` interface, in which case + you can get the backend instance from providers with complex search options using their + ``get_backend()`` method. For example: + + .. code-block:: python + + import pennylane as qml + + def configured_backend(): + backend = SomeProvider.get_backend(...) + backend.options.update_options(...) # Set backend options this way + return backend + + dev = qml.device('qiskit.remote', wires=2, backend=configured_backend()) + +After installing the plugin, this device can be used just like any other PennyLane device for defining and evaluating QNodes. +For example, a simple quantum function that returns the expectation value of a measurement and depends on +three classical input parameters can be decorated with ``qml.qnode`` as usual to construct a ``QNode``: + +.. code-block:: python + + @qml.qnode(dev) + def circuit(x, y, z): + qml.RZ(z, wires=[0]) + qml.RY(y, wires=[0]) + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(wires=1)) + +You can then execute the above quantum circuit to get the expectation value of a Pauli operator. + +.. code-block:: python + + circuit(0.2, 0.1, 0.3) + +The ``'qiskit.remote'`` device also supports the use of `local simulators `_ such as ``FakeManila``. + +.. code-block:: python + + from qiskit_ibm_runtime.fake_provider import FakeManilaV2 + backend = FakeManilaV2() + + # You could use an Aer simulator instead by using the following code: + # from qiskit_aer import AerSimulator + # backend = AerSimulator() + + dev = qml.device('qiskit.remote', wires=5, backend=backend) + +Device options +~~~~~~~~~~~~~~ + +The ``'qiskit.remote'`` device uses the `EstimatorV2 `_ +and the `SamplerV2 `_ runtime primitives to execute +the measurements. To set options for `transpilation `_ +or `runtime `_, simply pass the keyword arguments into the device. + +.. code-block:: python + + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=1, + seed_transpiler=42 + ) + # to change options, re-initialize the device + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=2, + seed_transpiler=24 + ) + +This device is not compatible with analytic mode, so an error will be raised if ``shots=0`` or ``shots=None``. +The default value of the shots argument is ``1024``. You can set the number of shots on device initialization using the +``shots`` keyword, or you can choose the number of shots on circuit execution. + +.. code-block:: python + + dev = qml.device("qiskit.remote", wires=5, backend=backend, shots=4096) + + @qml.qnode(dev) + def circuit(x, y, z): + qml.RZ(z, wires=[0]) + qml.RY(y, wires=[0]) + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(wires=1)) + + # Runs with 4096 shots + circuit(0.2, 0.1, 0.3) + + # Runs with 10000 shots + circuit(0.2, 0.1, 0.3, shots=10000) diff --git a/doc/devices/runtime.rst b/doc/devices/runtime.rst deleted file mode 100644 index db94c90cc..000000000 --- a/doc/devices/runtime.rst +++ /dev/null @@ -1,21 +0,0 @@ -Qiskit Runtime Programs -======================= - -PennyLane-Qiskit supports running PennyLane on IBM Q hardware via the Qiskit runtime programs ``circuit-runner`` -and ``sampler``. You can choose between those two runtime programs and also have the possibility to choose the -backend on which the circuits will be run. Those two devices inherit directly from the ``IBMQ`` device and work the -the same way, you can refer to the corresponding documentation for details about token and providers -`IBMQ documentation for PennyLane `_. - -You can use the ``circuit_runner`` and ``sampler`` devices by using their short names, for example: - -.. code-block:: python - - dev = qml.device('qiskit.ibmq.circuit_runner', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs) - - -.. code-block:: python - - dev = qml.device('qiskit.ibmq.sampler', wires=2, backend='ibmq_qasm_simulator', shots=8000, **kwargs) - -More details on Qiskit runtime programs in the `IBMQ runtime documentation `_. diff --git a/doc/index.rst b/doc/index.rst index 372bf84d8..ddf3944d8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,7 +8,7 @@ PennyLane-Qiskit Plugin :end-before: header-end-inclusion-marker-do-not-remove -Once the PennyLane-Qiskit plugin is installed, the the Qiskit devices +Once the PennyLane-Qiskit plugin is installed, the Qiskit devices can be accessed straightaway in PennyLane, without the need to import new packages. Devices @@ -21,37 +21,17 @@ The following devices are available: :description: Qiskit's staple simulator with great features such as noise models. :link: devices/aer.html -.. title-card:: - :name: 'qiskit.basicaer' - :description: A simplified version of the Aer device, which requires fewer dependencies. - :link: devices/basicaer.html .. title-card:: :name: 'qiskit.basicsim' :description: A simple local Python simulator running the Qiskit ``BasicSimulator``. :link: devices/basicsim.html -.. title-card:: - :name: 'qiskit.ibmq.circuit_runner' - :description: Allows integration with Qiskit's circuit runner runtime program. - :link: devices/runtime.html - -.. title-card:: - :name: 'qiskit.ibmq.sampler' - :description: Allows integration with Qiskit's sampler runtime program. - :link: devices/runtime.html - .. title-card:: :name: 'qiskit.remote' :description: Allows integration with any Qiskit backend. :link: devices/remote.html -.. title-card:: - :name: 'qiskit.ibmq' - :description: Allows integration with Qiskit's hardware backends, and hardware-specific simulators. - :link: devices/ibmq.html - - .. raw:: html
@@ -68,17 +48,39 @@ For example, the ``'qiskit.aer'`` device with two wires is called like this: Backends ~~~~~~~~ -Qiskit devices have different **backends**, which define which actual simulator or hardware is used by the -device. Different simulator backends are optimized for different types of circuits. A backend can be defined as -follows: +Qiskit devices have different **backends**, which define the actual simulator or hardware +used by the device. A backend instance should be initalized and passed to the device. + +Different simulator backends are optimized for different purposes. To change what backend is used, +a simulator backend can be defined as follows: .. code-block:: python - dev = qml.device('qiskit.aer', wires=2, backend='unitary_simulator') + from qiskit_aer import UnitarySimulator + + dev = qml.device('qiskit.aer', wires=, backend=UnitarySimulator()) + +.. note:: + + For ``'qiskit.aer'``, PennyLane chooses the ``aer_simulator`` as the default backend if no + backend is specified. For more details on the ``aer_simulator``, including available backend + options, see `Qiskit Aer Simulator documentation `_. + +To access a real device, we can use the ``'qiskit.remote'`` device. A real hardware backend can +be defined as follows: + +.. code-block:: python + + from qiskit_ibm_runtime import QiskitRuntimeService + + QiskitRuntimeService.save_account(channel="ibm_quantum", token="") + + # To access saved credentials for the IBM quantum channel and select an instance + service = QiskitRuntimeService(channel="ibm_quantum", instance="my_hub/my_group/my_project") + backend = service.least_busy(operational=True, simulator=False, min_num_qubits=) -PennyLane chooses the ``qasm_simulator`` as the default backend if no backend is specified. -For more details on the ``qasm_simulator``, including available backend options, see -`Qiskit Qasm Simulator documentation `_. + # passing a string in backend would result in an error + dev = qml.device('qiskit.remote', wires=, backend=backend) Tutorials ~~~~~~~~~ @@ -116,7 +118,7 @@ You can also try it out using any of the qubit based `demos from the PennyLane d `_, for example the tutorial on `qubit rotation `_. Simply replace ``'default.qubit'`` with any of the available Qiskit devices, -such as ``'qiskit.aer'``, or ``'qiskit.ibmq'`` if you have an API key for +such as ``'qiskit.aer'``, or ``'qiskit.remote'`` if you have an API key for hardware access. .. raw:: html @@ -138,10 +140,7 @@ hardware access. :hidden: devices/aer - devices/basicaer devices/basicsim - devices/ibmq - devices/runtime devices/remote .. toctree:: diff --git a/doc/requirements.txt b/doc/requirements.txt index 812db6eb1..f625b9347 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -7,10 +7,10 @@ pennylane==0.37 pybind11==2.11.1 pygments==2.17.2 pygments-github-lexers==0.0.5 -qiskit==0.45.3 -qiskit-aer==0.13.3 -qiskit-ibm-runtime==0.20.0 -qiskit-ibm-provider==0.10.0 +qiskit==1.0.2 +qiskit-aer==0.14.1 +qiskit-ibm-runtime==0.23.0 +qiskit-ibm-provider==0.11.0 sphinxcontrib-bibtex==2.6.2 sphinx-automodapi==0.17.0 pennylane-sphinx-theme diff --git a/pennylane_qiskit/__init__.py b/pennylane_qiskit/__init__.py index 991b73d7c..2a886b80e 100644 --- a/pennylane_qiskit/__init__.py +++ b/pennylane_qiskit/__init__.py @@ -15,9 +15,6 @@ from ._version import __version__ from .aer import AerDevice -from .basic_aer import BasicAerDevice, BasicSimulatorDevice -from .ibmq import IBMQDevice +from .basic_sim import BasicSimulatorDevice from .remote import RemoteDevice from .converter import load, load_pauli_op, load_qasm, load_qasm_from_file -from .runtime_devices import IBMQCircuitRunnerDevice -from .runtime_devices import IBMQSamplerDevice diff --git a/pennylane_qiskit/aer.py b/pennylane_qiskit/aer.py index 24e1a3536..34d6c3bce 100644 --- a/pennylane_qiskit/aer.py +++ b/pennylane_qiskit/aer.py @@ -18,10 +18,10 @@ """ import qiskit_aer -from .qiskit_device import QiskitDevice +from .qiskit_device_legacy import QiskitDeviceLegacy -class AerDevice(QiskitDevice): +class AerDevice(QiskitDeviceLegacy): """A PennyLane device for the C++ Qiskit Aer simulator. Please refer to the `Qiskit documentation `_ for diff --git a/pennylane_qiskit/basic_aer.py b/pennylane_qiskit/basic_aer.py deleted file mode 100644 index ecf26fce8..000000000 --- a/pennylane_qiskit/basic_aer.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2019 Xanadu Quantum Technologies Inc. - -# 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. -""" -This module contains the :class:`~.BasicAerDevice` class, a PennyLane device that allows -evaluation and differentiation of Qiskit Terra's BasicAer simulator -using PennyLane. -""" -import qiskit - -from semantic_version import Version - -from .qiskit_device import QiskitDevice - -if Version(qiskit.__version__) >= Version("1.0.0"): - from qiskit.providers.basic_provider import BasicProvider - - -class BasicAerDevice(QiskitDevice): - """A PennyLane device for the native Python Qiskit simulator BasicAer. - - Please see the `Qiskit documentations `_ - further information on the backend options and transpile options. - - A range of :code:`backend_options` that will be passed to the simulator and - a range of transpile options can be given as kwargs. - - For more information on backends, please visit the - `Basic Aer provider documentation `_. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['aux_wire', 'q1', 'q2']``). - backend (str): the desired backend - shots (int or None): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. For statevector backends, - setting to ``None`` results in computing statistics like expectation values and variances analytically. - - Keyword Args: - name (str): The name of the circuit. Default ``'circuit'``. - compile_backend (BaseBackend): The backend used for compilation. If you wish - to simulate a device compliant circuit, you can specify a backend here. - """ - - short_name = "qiskit.basicaer" - - def __init__(self, wires, shots=1024, backend="qasm_simulator", **kwargs): - - max_ver = Version("0.46", partial=True) - - if Version(qiskit.__version__) > max_ver: - raise RuntimeError( - f"Qiskit has discontinued the BasicAer device, so it can only be used in" - f"versions of Qiskit below 1.0. You have version {qiskit.__version__} " - f"installed. For a Python simulator, use the 'qiskit.basicsim' device " - f"instead. Alternatively, you can downgrade Qiskit to use the " - f"'qiskit.basicaer' device." - ) - - super().__init__(wires, provider=qiskit.BasicAer, backend=backend, shots=shots, **kwargs) - - -class BasicSimulatorDevice(QiskitDevice): - """A PennyLane device for the native Python Qiskit simulator. - - For more information on the ``BasicSimulator`` backend options and transpile options, please visit the - `BasicProvider documentation `_. - These options can be passed to this plugin device as keyword arguments. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['aux_wire', 'q1', 'q2']``). - backend (str): the desired backend - shots (int or None): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. For statevector backends, - setting to ``None`` results in computing statistics like expectation values and variances analytically. - """ - - short_name = "qiskit.basicsim" - - analytic_warning_message = ( - "The plugin does not currently support analytic calculation of expectations, variances " - "and probabilities with the BasicProvider backend {}. Such statistics obtained from this " - "device are estimates based on samples." - ) - - def __init__(self, wires, shots=1024, backend="basic_simulator", **kwargs): - - min_version = Version("1.0.0") - - if Version(qiskit.__version__) < min_version: - raise RuntimeError( - f"The 'qiskit.simulator' device is not compatible with version of Qiskit prior " - f"to 1.0. You have version {qiskit.__version__} installed. For a Python simulator, " - f"use the 'qiskit.basicaer' device instead. Alternatively, upgrade Qiskit " - f"(see https://docs.quantum.ibm.com/start/install) to use the 'qiskit.basicsim' device." - ) - - super().__init__(wires, provider=BasicProvider(), backend=backend, shots=shots, **kwargs) diff --git a/pennylane_qiskit/basic_sim.py b/pennylane_qiskit/basic_sim.py new file mode 100644 index 000000000..99f0a10eb --- /dev/null +++ b/pennylane_qiskit/basic_sim.py @@ -0,0 +1,49 @@ +# Copyright 2019 Xanadu Quantum Technologies Inc. + +# 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. +""" +This module contains the :class:`~.BasicAerDevice` class, a PennyLane device that allows +evaluation and differentiation of Qiskit Terra's BasicAer simulator +using PennyLane. +""" +from qiskit.providers.basic_provider import BasicProvider +from .qiskit_device_legacy import QiskitDeviceLegacy + + +class BasicSimulatorDevice(QiskitDeviceLegacy): + """A PennyLane device for the native Python Qiskit simulator. + + For more information on the ``BasicSimulator`` backend options and transpile options, please visit the + `BasicProvider documentation `_. + These options can be passed to this plugin device as keyword arguments. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['aux_wire', 'q1', 'q2']``). + backend (str): the desired backend + shots (int or None): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. For statevector backends, + setting to ``None`` results in computing statistics like expectation values and variances analytically. + """ + + short_name = "qiskit.basicsim" + + analytic_warning_message = ( + "The plugin does not currently support analytic calculation of expectations, variances " + "and probabilities with the BasicProvider backend {}. Such statistics obtained from this " + "device are estimates based on samples." + ) + + def __init__(self, wires, shots=1024, backend="basic_simulator", **kwargs): + super().__init__(wires, provider=BasicProvider(), backend=backend, shots=shots, **kwargs) diff --git a/pennylane_qiskit/converter.py b/pennylane_qiskit/converter.py index 7f7172e03..0df4e492d 100644 --- a/pennylane_qiskit/converter.py +++ b/pennylane_qiskit/converter.py @@ -1,4 +1,4 @@ -# Copyright 2019 Xanadu Quantum Technologies Inc. +# Copyright 2021-2024 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,18 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. r""" -This module contains functions for converting Qiskit QuantumCircuit objects -into PennyLane circuit templates. +This module contains functions for converting between Qiskit QuantumCircuit objects +and PennyLane circuits. """ from typing import Dict, Any, Iterable, Sequence, Union import warnings from functools import partial, reduce import numpy as np +from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister +from qiskit.converters import circuit_to_dag, dag_to_circuit import qiskit.qasm2 -from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterExpression, ParameterVector from qiskit.circuit import Measure, Barrier, ControlFlowOp, Clbit +from qiskit.circuit import library as lib from qiskit.circuit.classical import expr from qiskit.circuit.controlflow.switch_case import _DefaultCaseType from qiskit.circuit.library import GlobalPhaseGate @@ -34,9 +36,51 @@ import pennylane as qml import pennylane.ops as pennylane_ops -from pennylane_qiskit.qiskit_device import QISKIT_OPERATION_MAP - -# pylint: disable=too-many-instance-attributes +from pennylane.tape.tape import rotations_and_diagonal_measurements + +QISKIT_OPERATION_MAP = { + # native PennyLane operations also native to qiskit + "PauliX": lib.XGate, + "PauliY": lib.YGate, + "PauliZ": lib.ZGate, + "Hadamard": lib.HGate, + "CNOT": lib.CXGate, + "CZ": lib.CZGate, + "SWAP": lib.SwapGate, + "ISWAP": lib.iSwapGate, + "RX": lib.RXGate, + "RY": lib.RYGate, + "RZ": lib.RZGate, + "Identity": lib.IGate, + "CSWAP": lib.CSwapGate, + "CRX": lib.CRXGate, + "CRY": lib.CRYGate, + "CRZ": lib.CRZGate, + "PhaseShift": lib.PhaseGate, + "QubitStateVector": lib.Initialize, + "StatePrep": lib.Initialize, + "Toffoli": lib.CCXGate, + "QubitUnitary": lib.UnitaryGate, + "U1": lib.U1Gate, + "U2": lib.U2Gate, + "U3": lib.U3Gate, + "IsingZZ": lib.RZZGate, + "IsingYY": lib.RYYGate, + "IsingXX": lib.RXXGate, + "S": lib.SGate, + "T": lib.TGate, + "SX": lib.SXGate, + "Adjoint(S)": lib.SdgGate, + "Adjoint(T)": lib.TdgGate, + "Adjoint(SX)": lib.SXdgGate, + "CY": lib.CYGate, + "CH": lib.CHGate, + "CPhase": lib.CPhaseGate, + "CCZ": lib.CCZGate, + "ECR": lib.ECRGate, + "Barrier": lib.Barrier, + "Adjoint(GlobalPhase)": lib.GlobalPhaseGate, +} inv_map = {v.__name__: k for k, v in QISKIT_OPERATION_MAP.items()} @@ -358,7 +402,7 @@ def load(quantum_circuit: QuantumCircuit, measurements=None): function: The resulting PennyLane template. """ - # pylint:disable=too-many-branches, fixme, protected-access + # pylint:disable=too-many-branches, fixme, protected-access, too-many-nested-blocks def _function(*args, params: dict = None, wires: list = None, **kwargs): """Returns a PennyLane quantum function created based on the input QuantumCircuit. Warnings are created for each of the QuantumCircuit instructions that were @@ -607,9 +651,141 @@ def load_qasm_from_file(file: str): Returns: function: the new PennyLane template """ + return load(QuantumCircuit.from_qasm_file(file), measurements=[]) +# diagonalize is currently only used if measuring +# maybe always diagonalize when measuring, and never when not? +# will this be used for a user-facing function to convert from PL to Qiskit as well? +def circuit_to_qiskit(circuit, register_size, diagonalize=True, measure=True): + """Builds the circuit objects based on the operations and measurements + specified to apply. + + Args: + circuit (QuantumTape): the circuit applied + to the device + register_size (int): the total number of qubits on the device the circuit is + executed on; this must include any qubits not used in the given + circuit to ensure correct indexing of the returned samples + + Keyword args: + diagonalize (bool): whether or not to apply diagonalizing gates before the + measurements + measure (bool): whether or not to apply measurements at the end of the circuit; + a full circuit is represented either as a Qiskit circuit with operations + and measurements (measure=True), or a Qiskit circuit with only operations, + paired with a Qiskit Estimator defining the measurement process. + + Returns: + QuantumCircuit: the qiskit equivalent of the given circuit + """ + + reg = QuantumRegister(register_size) + + if not measure: + qc = QuantumCircuit(reg, name="temp") + + for op in circuit.operations: + qc &= operation_to_qiskit(op, reg) + + return qc + + creg = ClassicalRegister(register_size) + qc = QuantumCircuit(reg, creg, name="temp") + + for op in circuit.operations: + qc &= operation_to_qiskit(op, reg, creg) + + # rotate the state for measurement in the computational basis + # ToDo: check this in cases with multiple different bases + if diagonalize: + rotations, measurements = rotations_and_diagonal_measurements(circuit) + for _, m in enumerate(measurements): + if m.obs is not None: + rotations.extend(m.obs.diagonalizing_gates()) + + for rot in rotations: + qc &= operation_to_qiskit(rot, reg, creg) + + # barrier ensures we first do all operations, then do all measurements + qc.barrier(reg) + # we always measure the full register + qc.measure(reg, creg) + + return qc + + +def operation_to_qiskit(operation, reg, creg=None): + """Take a Pennylane operator and convert to a Qiskit circuit + + Args: + operation (List[pennylane.Operation]): operation to be converted + reg (Quantum Register): the total number of qubits on the device + creg (Classical Register): classical register + + Returns: + QuantumCircuit: a quantum circuit objects containing the translated operation + """ + op_wires = operation.wires + par = operation.parameters + + for idx, p in enumerate(par): + if isinstance(p, np.ndarray): + # Convert arrays so that Qiskit accepts the parameter + par[idx] = p.tolist() + + operation = operation.name + + mapped_operation = QISKIT_OPERATION_MAP[operation] + + qregs = [reg[i] for i in op_wires.labels] + + # Need to revert the order of the quantum registers used in + # Qiskit such that it matches the PennyLane ordering + if operation in ("QubitUnitary", "QubitStateVector", "StatePrep"): + qregs = list(reversed(qregs)) + + if creg: + dag = circuit_to_dag(QuantumCircuit(reg, creg, name="")) + else: + dag = circuit_to_dag(QuantumCircuit(reg, name="")) + gate = mapped_operation(*par) + + dag.apply_operation_back(gate, qargs=qregs) + circuit = dag_to_circuit(dag) + + return circuit + + +def mp_to_pauli(mp, register_size): + """Convert a Pauli observable to a SparsePauliOp for measurement via Estimator + + Args: + mp(Union[ExpectationMP, VarianceMP]): MeasurementProcess to be converted to a SparsePauliOp + register_size(int): total size of the qubit register being measured + + Returns: + SparsePauliOp: the ``SparsePauliOp`` of the given Pauli observable + """ + op = mp.obs + + if op.pauli_rep: + pauli_strings = [ + "".join( + ["I" if i not in pauli_term.wires else pauli_term[i] for i in range(register_size)][ + ::-1 + ] ## Qiskit follows opposite wire order convention + ) + for pauli_term in op.pauli_rep.keys() + ] + coeffs = list(op.pauli_rep.values()) + else: + raise ValueError(f"The operator {op} does not have a representation for SparsePauliOp") + + return SparsePauliOp(data=pauli_strings, coeffs=coeffs).simplify() + + def load_pauli_op( pauli_op: SparsePauliOp, params: Any = None, diff --git a/pennylane_qiskit/ibmq.py b/pennylane_qiskit/ibmq.py deleted file mode 100644 index 8f088abf7..000000000 --- a/pennylane_qiskit/ibmq.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2019 Xanadu Quantum Technologies Inc. - -# 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. -""" -This module contains the :class:`~.IBMQDevice` class, a PennyLane device that allows -evaluation and differentiation of IBM Q's Quantum Processing Units (QPUs) -using PennyLane. -""" -import os - -from qiskit_ibm_provider import IBMProvider -from qiskit_ibm_provider.exceptions import IBMAccountError -from qiskit_ibm_provider.accounts.exceptions import AccountsError -from qiskit_ibm_provider.job import IBMJobError - -from .qiskit_device import QiskitDevice - - -class IBMQDevice(QiskitDevice): - """A PennyLane device for the IBMQ API (remote) backend. - - For more details, see the `Qiskit documentation `_ - - You need to register at `IBMQ `_ in order to - recieve a token that is used for authentication using the API. - - As of the writing of this documentation, the API is free of charge, although - there is a credit system to limit access to the quantum devices. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). Note that for some backends, the number - of wires has to match the number of qubits accessible. - provider (Provider): The IBM Q provider you wish to use. If not provided, - then the default provider returned by ``IBMProvider()`` is used. - backend (str): the desired provider backend - shots (int): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables - timeout_secs (int): A timeout value in seconds to wait for job results from an IBMQ backend. - The default value of ``None`` means no timeout - - Keyword Args: - ibmqx_token (str): The IBM Q API token. If not provided, the environment - variable ``IBMQX_TOKEN`` is used. - ibmqx_url (str): The IBM Q URL. If not provided, the environment - variable ``IBMQX_URL`` is used, followed by the default URL. - noise_model (NoiseModel): NoiseModel Object from ``qiskit_aer.noise``. - Only applicable for simulator backends. - hub (str): Name of the provider hub. - group (str): Name of the provider group. - project (str): Name of the provider project. - """ - - short_name = "qiskit.ibmq" - - def __init__( - self, - wires, - provider=None, - backend="ibmq_qasm_simulator", - shots=1024, - timeout_secs=None, - **kwargs, - ): # pylint:disable=too-many-arguments - # Connection to IBMQ - connect(kwargs) - - hub = kwargs.get("hub", "ibm-q") - group = kwargs.get("group", "open") - project = kwargs.get("project", "main") - instance = "/".join([hub, group, project]) - - # get a provider - p = provider or IBMProvider(instance=instance) - - super().__init__(wires=wires, provider=p, backend=backend, shots=shots, **kwargs) - self.timeout_secs = timeout_secs - - def batch_execute(self, circuits): # pragma: no cover, pylint:disable=arguments-differ - res = super().batch_execute(circuits, timeout=self.timeout_secs) - if self.tracker.active: - self._track_run() - return res - - def _track_run(self): # pragma: no cover - """Provide runtime information.""" - - expected_keys = {"created", "running", "finished"} - time_per_step = self._current_job.time_per_step() - if not set(time_per_step).issuperset(expected_keys): - # self._current_job.result() should have already run by now - # tests see a race condition, so this is ample time for that case - timeout_secs = self.timeout_secs or 60 - self._current_job.wait_for_final_state(timeout=timeout_secs) - self._current_job.refresh() - time_per_step = self._current_job.time_per_step() - if not set(time_per_step).issuperset(expected_keys): - raise IBMJobError( - f"time_per_step had keys {set(time_per_step)}, needs {expected_keys}. If your program takes a long time, you may want to configure the device with a higher `timeout_secs`" - ) - - job_time = { - "queued": (time_per_step["running"] - time_per_step["created"]).total_seconds(), - "running": (time_per_step["finished"] - time_per_step["running"]).total_seconds(), - } - self.tracker.update(job_time=job_time) - self.tracker.record() - - -def connect(kwargs): - """Function that allows connection to IBMQ. - - Args: - kwargs(dict): dictionary that contains the token and the url""" - - hub = kwargs.get("hub", "ibm-q") - group = kwargs.get("group", "open") - project = kwargs.get("project", "main") - instance = "/".join([hub, group, project]) - - token = kwargs.get("ibmqx_token", None) or os.getenv("IBMQX_TOKEN") - url = kwargs.get("ibmqx_url", None) or os.getenv("IBMQX_URL") - - saved_accounts = IBMProvider.saved_accounts() - if not token: - if not saved_accounts: - raise IBMAccountError("No active IBM Q account, and no IBM Q token provided.") - try: - IBMProvider(url=url, instance=instance) - except AccountsError as e: - raise AccountsError( - f"Accounts were found ({set(saved_accounts)}), but all failed to load." - ) from e - return - for account in saved_accounts.values(): - if account["token"] == token: - return - IBMProvider.save_account(token=token, url=url, instance=instance) diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py index b03a96922..ae8ae4b2f 100644 --- a/pennylane_qiskit/qiskit_device.py +++ b/pennylane_qiskit/qiskit_device.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 Xanadu Quantum Technologies Inc. +# Copyright 2019-2024 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,124 +12,287 @@ # See the License for the specific language governing permissions and # limitations under the License. r""" -This module contains a base class for constructing Qiskit devices for PennyLane. +This module contains a prototype base class for constructing Qiskit devices +for PennyLane with the new device API. """ # pylint: disable=too-many-instance-attributes,attribute-defined-outside-init -import abc -import inspect import warnings +import inspect +from typing import Union, Callable, Tuple, Sequence +from contextlib import contextmanager +from functools import wraps import numpy as np -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.circuit import library as lib +import pennylane as qml from qiskit.compiler import transpile -from qiskit.converters import circuit_to_dag, dag_to_circuit -from qiskit.providers import Backend, BackendV2, QiskitBackendNotFoundError +from qiskit.providers import BackendV2 + +from qiskit_ibm_runtime import Session, SamplerV2 as Sampler, EstimatorV2 as Estimator + +from pennylane import transform +from pennylane.transforms.core import TransformProgram +from pennylane.transforms import broadcast_expand, split_non_commuting +from pennylane.tape import QuantumTape, QuantumScript +from pennylane.typing import Result, ResultBatch +from pennylane.devices import Device +from pennylane.devices.execution_config import ExecutionConfig, DefaultExecutionConfig +from pennylane.devices.preprocess import ( + decompose, + validate_observables, + validate_measurements, + validate_device_wires, +) + +from pennylane.measurements import ExpectationMP, VarianceMP +from pennylane.devices.modifiers.simulator_tracking import simulator_tracking +from ._version import __version__ +from .converter import QISKIT_OPERATION_MAP, circuit_to_qiskit, mp_to_pauli -from pennylane import QubitDevice, DeviceError -from pennylane.measurements import SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP +QuantumTapeBatch = Sequence[QuantumTape] +QuantumTape_or_Batch = Union[QuantumTape, QuantumTapeBatch] +Result_or_ResultBatch = Union[Result, ResultBatch] -from ._version import __version__ -SAMPLE_TYPES = (SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP) - - -QISKIT_OPERATION_MAP = { - # native PennyLane operations also native to qiskit - "PauliX": lib.XGate, - "PauliY": lib.YGate, - "PauliZ": lib.ZGate, - "Hadamard": lib.HGate, - "CNOT": lib.CXGate, - "CZ": lib.CZGate, - "SWAP": lib.SwapGate, - "ISWAP": lib.iSwapGate, - "RX": lib.RXGate, - "RY": lib.RYGate, - "RZ": lib.RZGate, - "Identity": lib.IGate, - "CSWAP": lib.CSwapGate, - "CRX": lib.CRXGate, - "CRY": lib.CRYGate, - "CRZ": lib.CRZGate, - "PhaseShift": lib.PhaseGate, - "QubitStateVector": lib.Initialize, - "StatePrep": lib.Initialize, - "Toffoli": lib.CCXGate, - "QubitUnitary": lib.UnitaryGate, - "U1": lib.U1Gate, - "U2": lib.U2Gate, - "U3": lib.U3Gate, - "IsingZZ": lib.RZZGate, - "IsingYY": lib.RYYGate, - "IsingXX": lib.RXXGate, - "S": lib.SGate, - "T": lib.TGate, - "SX": lib.SXGate, - "Adjoint(S)": lib.SdgGate, - "Adjoint(T)": lib.TdgGate, - "Adjoint(SX)": lib.SXdgGate, - "CY": lib.CYGate, - "CH": lib.CHGate, - "CPhase": lib.CPhaseGate, - "CCZ": lib.CCZGate, - "ECR": lib.ECRGate, - "Barrier": lib.Barrier, - "Adjoint(GlobalPhase)": lib.GlobalPhaseGate, -} - - -def _get_backend_name(backend): +def custom_simulator_tracking(cls): + """Decorator that adds custom tracking to the device class.""" + + cls = simulator_tracking(cls) + tracked_execute = cls.execute + + @wraps(tracked_execute) + def execute(self, circuits, execution_config=DefaultExecutionConfig): + results = tracked_execute(self, circuits, execution_config) + if self.tracker.active: + res = [] + del self.tracker.totals["simulations"] + del self.tracker.history["simulations"] + del self.tracker.latest["simulations"] + for r in self.tracker.history["results"]: + while isinstance(r, (list, tuple)) and len(r) == 1: + r = r[0] + res.append(r) + self.tracker.history["results"] = res + return results + + cls.execute = execute + + return cls + + +# pylint: disable=protected-access +@contextmanager +def qiskit_session(device, **kwargs): + """A context manager that creates a Qiskit Session and sets it as a session + on the device while the context manager is active. Using the context manager + will ensure the Session closes properly and is removed from the device after + completing the tasks. Any Session that was initialized and passed into the + device will be overwritten by the Qiskit Session created by this context + manager. + + Args: + device (QiskitDevice2): the device that will create remote tasks using the session + **kwargs: session keyword arguments to be used for settings for the Session. At the + time of writing, the only relevant keyword argument is "max_time", which lets you + set the maximum amount of time the sessin is open. For the most up to date information, + please refer to the Qiskit Session + `documentation `_. + + **Example:** + + .. code-block:: python + + import pennylane as qml + from pennylane_qiskit import qiskit_session + from qiskit_ibm_runtime import QiskitRuntimeService + + # get backend + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(simulator=False, operational=True) + + # initialize device + dev = qml.device('qiskit.remote', wires=2, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, 0) + qml.CNOT([0, 1]) + return qml.expval(qml.PauliZ(1)) + + angle = 0.1 + + with qiskit_session(dev, max_time=60) as session: + # queue for the first execution + res = circuit(angle)[0] + + # then this loop executes immediately after without queueing again + while res > 0: + angle += 0.3 + res = circuit(angle)[0] + + Note that if you passed in a session to your device, that session will be overwritten + by `qiskit_session`. + + .. code-block:: python + + import pennylane as qml + from pennylane_qiskit import qiskit_session + from qiskit_ibm_runtime import QiskitRuntimeService, Session + + # get backend + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(simulator=False, operational=True) + + # initialize device + dev = qml.device('qiskit.remote', wires=2, backend=backend, session=Session(backend=backend, max_time=30)) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, 0) + qml.CNOT([0, 1]) + return qml.expval(qml.PauliZ(1)) + + angle = 0.1 + + # This session will have the Qiskit default settings max_time=900 + with qiskit_session(dev) as session: + res = circuit(angle)[0] + + while res > 0: + angle += 0.3 + res = circuit(angle)[0] + """ + # Code to acquire session: + existing_session = device._session + + session_options = {"backend": device.backend, "service": device.service} + + for k, v in kwargs.items(): + # Options like service and backend should be tied to the settings set on device + if k in session_options: + warnings.warn(f"Using '{k}' set in device, {getattr(device, k)}", UserWarning) + else: + session_options[k] = v + + session = Session(**session_options) + device._session = session try: - return backend.name() # BackendV1 - except TypeError: # pragma: no cover - return backend.name # BackendV2 + yield session + finally: + # Code to release session: + session.close() + device._session = existing_session + + +def accepted_sample_measurement(m: qml.measurements.MeasurementProcess) -> bool: + """Specifies whether or not a measurement is accepted when sampling.""" + + return isinstance( + m, + ( + qml.measurements.SampleMeasurement, + qml.measurements.ClassicalShadowMP, + qml.measurements.ShadowExpvalMP, + ), + ) -class QiskitDevice(QubitDevice, abc.ABC): - r"""Abstract Qiskit device for PennyLane. +@transform +def split_execution_types( + tape: qml.tape.QuantumTape, +) -> (Sequence[qml.tape.QuantumTape], Callable): + """Split into separate tapes based on measurement type. Counts and sample-based measurements + will use the Qiskit Sampler. ExpectationValue and Variance will use the Estimator, except + when the measured observable does not have a `pauli_rep`. In that case, the Sampler will be + used, and the raw samples will be processed to give an expectation value.""" + estimator = [] + sampler = [] + + for i, mp in enumerate(tape.measurements): + if isinstance(mp, (ExpectationMP, VarianceMP)): + if mp.obs.pauli_rep: + estimator.append((mp, i)) + else: + warnings.warn( + f"The observable measured {mp.obs} does not have a `pauli_rep` " + "and will be run without using the Estimator primitive. Instead, " + "raw samples from the Sampler will be used." + ) + sampler.append((mp, i)) + else: + sampler.append((mp, i)) + + order_indices = [[i for mp, i in group] for group in [estimator, sampler]] + + tapes = [] + if estimator: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in estimator], + shots=tape.shots, + ) + ] + ) + if sampler: + tapes.extend( + [ + qml.tape.QuantumScript( + tape.operations, + measurements=[mp for mp, i in sampler], + shots=tape.shots, + ) + ] + ) + + def reorder_fn(res): + """re-order the output to the original shape and order""" + + flattened_indices = [i for group in order_indices for i in group] + flattened_results = [r for group in res for r in group] + + if len(flattened_indices) != len(flattened_results): + raise ValueError( + "The lengths of flattened_indices and flattened_results do not match." + ) # pragma: no cover + + result = dict(zip(flattened_indices, flattened_results)) + + result = tuple(result[i] for i in sorted(result.keys())) + + return result[0] if len(result) == 1 else result + + return tapes, reorder_fn + + +@custom_simulator_tracking +class QiskitDevice(Device): + r"""Hardware/simulator Qiskit device for PennyLane. Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider | None): The Qiskit backend provider. - backend (str | Backend): the desired backend. If a string, a provider must be given. - shots (int or None): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. For state vector backends, - setting to ``None`` results in computing statistics like expectation values and variances analytically. + or strings (``['aux_wire', 'q1', 'q2']``). + backend (Backend): the initialized Qiskit backend Keyword Args: - name (str): The name of the circuit. Default ``'circuit'``. - compile_backend (BaseBackend): The backend used for compilation. If you wish - to simulate a device compliant circuit, you can specify a backend here. + shots (int or None): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. + session (Session): a Qiskit Session to use for device execution. If none is provided, a session will + be created at each device execution. + compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit that will be + sent to the backend device, to be set if the backend desired for compliation differs from the + backend used for execution. Defaults to ``None``, which means the primary backend will be used. + **kwargs: transpilation and runtime keyword arguments to be used for measurements with Primitives. + If an `options` dictionary is defined amongst the kwargs, and there are settings that overlap + with those in kwargs, the settings in `options` will take precedence over kwargs. Keyword + arguments accepted by both the transpiler and at runtime (e.g. ``optimization_level``) + will be passed to the transpiler rather than to the Primitive. """ - name = "Qiskit PennyLane plugin" - pennylane_requires = ">=0.37.0" - version = __version__ - plugin_version = __version__ - author = "Xanadu" - - _capabilities = { - "model": "qubit", - "tensor_observables": True, - "inverse_operations": True, - } - _operation_map = QISKIT_OPERATION_MAP - _state_backends = { - "statevector_simulator", - "simulator_statevector", - "unitary_simulator", - "aer_simulator_statevector", - "aer_simulator_unitary", - } - """set[str]: Set of backend names that define the backends - that support returning the underlying quantum statevector""" - - operations = set(_operation_map.keys()) + operations = set(QISKIT_OPERATION_MAP.keys()) observables = { "PauliX", "PauliY", @@ -138,399 +301,415 @@ class QiskitDevice(QubitDevice, abc.ABC): "Hadamard", "Hermitian", "Projector", + "Prod", + "Sum", + "LinearCombination", + "SProd", + # TODO Could support SparseHamiltonian } - analytic_warning_message = ( - "The analytic calculation of expectations, variances and " - "probabilities is only supported on statevector backends, not on the {}. " - "Such statistics obtained from this device are estimates based on samples." - ) - - _eigs = {} - - def __init__(self, wires, provider, backend, shots=1024, **kwargs): + # pylint:disable = too-many-arguments + def __init__( + self, + wires, + backend, + shots=1024, + session=None, + compile_backend=None, + **kwargs, + ): + + if shots is None: + warnings.warn( + "Expected an integer number of shots, but received shots=None. Defaulting " + "to 1024 shots. The analytic calculation of results is not supported on " + "this device. All statistics obtained from this device are estimates based " + "on samples.", + UserWarning, + ) + + shots = 1024 super().__init__(wires=wires, shots=shots) - self.provider = provider - - if isinstance(backend, Backend): - self._backend = backend - self.backend_name = _get_backend_name(backend) - elif provider is None: - raise ValueError("Must pass a provider if the backend is not a Backend instance.") - else: - try: - self._backend = provider.get_backend(backend) - except QiskitBackendNotFoundError as e: - available_backends = list(map(_get_backend_name, provider.backends())) - raise ValueError( - f"Backend '{backend}' does not exist. Available backends " - f"are:\n {available_backends}" - ) from e - - self.backend_name = _get_backend_name(self._backend) - - # Keep track if the user specified analytic to be True - if shots is None and not self._is_state_backend: - # Raise a warning if no shots were specified for a hardware device - warnings.warn(self.analytic_warning_message.format(backend), UserWarning) + self._backend = backend + self._compile_backend = compile_backend if compile_backend else backend - self.shots = 1024 + self._service = getattr(backend, "_service", None) + self._session = session - self._capabilities["returns_state"] = self._is_state_backend + kwargs["shots"] = shots # Perform validation against backend - backend_qubits = ( + available_qubits = ( backend.num_qubits if isinstance(backend, BackendV2) - else self.backend.configuration().n_qubits + else backend.configuration().n_qubits ) - if backend_qubits and len(self.wires) > int(backend_qubits): - raise ValueError(f"Backend '{backend}' supports maximum {backend_qubits} wires") + if len(self.wires) > int(available_qubits): + raise ValueError(f"Backend '{backend}' supports maximum {available_qubits} wires") - # Initialize inner state self.reset() + self._kwargs, self._transpile_args = self._process_kwargs( + kwargs + ) # processes kwargs and separates transpilation arguments to dev._transpile_args - self.process_kwargs(kwargs) - - def process_kwargs(self, kwargs): - """Processing the keyword arguments that were provided upon device initialization. + @property + def backend(self): + """The Qiskit backend object. - Args: - kwargs (dict): keyword arguments to be set for the device + Returns: + qiskit.providers.Backend: Qiskit backend object. """ - self.compile_backend = None - if "compile_backend" in kwargs: - self.compile_backend = kwargs.pop("compile_backend") - - if "noise_model" in kwargs: - noise_model = kwargs.pop("noise_model") - self.backend.set_options(noise_model=noise_model) - - # set transpile_args - self.set_transpile_args(**kwargs) - - # Get further arguments for run - self.run_args = {} - - # Specify to have a memory for hw/hw simulators - compile_backend = self.compile_backend or self.backend - memory = str(compile_backend) not in self._state_backends - - if memory: - kwargs["memory"] = True - - # Consider the remaining kwargs as keyword arguments to run - self.run_args.update(kwargs) - - @property - def _is_state_backend(self): - """Returns whether this device has a state backend.""" - return self.backend_name in self._state_backends or self.backend.options.get("method") in { - "unitary", - "statevector", - } + return self._backend @property - def _is_statevector_backend(self): - """Returns whether this device has a statevector backend.""" - method = "statevector" - return method in self.backend_name or self.backend.options.get("method") == method + def compile_backend(self): + """The ``compile_backend`` is a Qiskit backend object to be used for transpilation. + Returns: + qiskit.providers.backend: Qiskit backend object. + """ + return self._compile_backend @property - def _is_unitary_backend(self): - """Returns whether this device has a unitary backend.""" - method = "unitary" - return method in self.backend_name or self.backend.options.get("method") == method + def service(self): + """The QiskitRuntimeService service. - def set_transpile_args(self, **kwargs): - """The transpile argument setter. - - Keyword Args: - kwargs (dict): keyword arguments to be set for the Qiskit transpiler. For more details, see the - `Qiskit transpiler documentation `_ + Returns: + qiskit.qiskit_ibm_runtime.QiskitRuntimeService """ - transpile_sig = inspect.signature(transpile).parameters - self.transpile_args = {arg: kwargs[arg] for arg in transpile_sig if arg in kwargs} - self.transpile_args.pop("circuits", None) - self.transpile_args.pop("backend", None) + return self._service @property - def backend(self): - """The Qiskit backend object. + def session(self): + """The QiskitRuntimeService session. Returns: - qiskit.providers.backend: Qiskit backend object. + qiskit.qiskit_ibm_runtime.Session """ - return self._backend + return self._session - def reset(self): - """Reset the Qiskit backend device""" - # Reset only internal data, not the options that are determined on - # device creation - self._reg = QuantumRegister(self.num_wires, "q") - self._creg = ClassicalRegister(self.num_wires, "c") - self._circuit = QuantumCircuit(self._reg, self._creg, name="temp") + @property + def num_wires(self): + """Get the number of wires. - self._current_job = None - self._state = None # statevector of a simulator backend + Returns: + int: The number of wires. + """ + return len(self.wires) - def create_circuit_object(self, operations, **kwargs): - """Builds the circuit objects based on the operations and measurements - specified to apply. + def update_session(self, session): + """Update the session attribute. Args: - operations (list[~.Operation]): operations to apply to the device - - Keyword args: - rotations (list[~.Operation]): Operations that rotate the circuit - pre-measurement into the eigenbasis of the observables. + session: The new session to be set. """ - rotations = kwargs.get("rotations", []) - - applied_operations = self.apply_operations(operations) + self._session = session - # Rotating the state for measurement in the computational basis - rotation_circuits = self.apply_operations(rotations) - applied_operations.extend(rotation_circuits) + def reset(self): + """Reset the current job to None.""" + self._current_job = None - for circuit in applied_operations: - self._circuit &= circuit + def stopping_condition(self, op: qml.operation.Operator) -> bool: + """Specifies whether or not an Operator is accepted by QiskitDevice2.""" + return op.name in self.operations - if not self._is_state_backend: - # Add measurements if they are needed - for qr, cr in zip(self._reg, self._creg): - self._circuit.measure(qr, cr) - elif "aer" in self.backend_name: - self._circuit.save_state() + def observable_stopping_condition(self, obs: qml.operation.Operator) -> bool: + """Specifies whether or not an observable is accepted by QiskitDevice2.""" + return obs.name in self.observables - def apply(self, operations, **kwargs): - """Build the circuit object and apply the operations""" - self.create_circuit_object(operations, **kwargs) + def preprocess( + self, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Tuple[TransformProgram, ExecutionConfig]: + """This function defines the device transform program to be applied and an updated device configuration. - # These operations need to run for all devices - compiled_circuit = self.compile() - self.run(compiled_circuit) + Args: + execution_config (Union[ExecutionConfig, Sequence[ExecutionConfig]]): A data structure describing the + parameters needed to fully describe the execution. - def apply_operations(self, operations): - """Apply the circuit operations. + Returns: + TransformProgram, ExecutionConfig: A transform program that when called returns QuantumTapes that the device + can natively execute as well as a postprocessing function to be called after execution, and a configuration with + unset specifications filled in. - This method serves as an auxiliary method to :meth:`~.QiskitDevice.apply`. + This device: - Args: - operations (List[pennylane.Operation]): operations to be applied + * Supports any operations with explicit PennyLane to Qiskit gate conversions defined in the plugin + * Does not intrinsically support parameter broadcasting - Returns: - list[QuantumCircuit]: a list of quantum circuit objects that - specify the corresponding operations """ - circuits = [] + config = execution_config + config.use_device_gradient = False - for operation in operations: - # Apply the circuit operations - device_wires = self.map_wires(operation.wires) - par = operation.parameters + transform_program = TransformProgram() - for idx, p in enumerate(par): - if isinstance(p, np.ndarray): - # Convert arrays so that Qiskit accepts the parameter - par[idx] = p.tolist() + transform_program.add_transform(validate_device_wires, self.wires, name=self.name) + transform_program.add_transform( + decompose, + stopping_condition=self.stopping_condition, + name=self.name, + skip_initial_state_prep=False, + ) + transform_program.add_transform( + validate_measurements, + sample_measurements=accepted_sample_measurement, + name=self.name, + ) + transform_program.add_transform( + validate_observables, + stopping_condition=self.observable_stopping_condition, + name=self.name, + ) - operation = operation.name + transform_program.add_transform(broadcast_expand) + transform_program.add_transform(split_non_commuting) - mapped_operation = self._operation_map[operation] + transform_program.add_transform(split_execution_types) - self.qubit_state_vector_check(operation) + return transform_program, config - qregs = [self._reg[i] for i in device_wires.labels] + def _process_kwargs(self, kwargs): + """Processes kwargs given and separates them into kwargs and transpile_args. If given + a keyword argument 'options' that is a dictionary, a common practice in + Qiskit, the options in said dictionary take precedence over any overlapping keyword + arguments defined in the kwargs. - if operation in ("QubitUnitary", "QubitStateVector", "StatePrep"): - # Need to revert the order of the quantum registers used in - # Qiskit such that it matches the PennyLane ordering - qregs = list(reversed(qregs)) + Keyword Args: + kwargs (dict): keyword arguments that set either runtime options or transpilation + options. - if operation in ("Barrier",): - # Need to add the num_qubits for instantiating Barrier in Qiskit - par = [len(self._reg)] + Returns: + kwargs, transpile_args: keyword arguments for the runtime options and keyword + arguments for the transpiler + """ - dag = circuit_to_dag(QuantumCircuit(self._reg, self._creg, name="")) + if "noise_model" in kwargs: + noise_model = kwargs.pop("noise_model") + self.backend.set_options(noise_model=noise_model) - gate = mapped_operation(*par) + if "options" in kwargs: + for key, val in kwargs.pop("options").items(): + if key in kwargs: + warnings.warn( + "An overlap between what was passed in via options and what was passed in via kwargs was found." + f"The value set in options {key}={val} will be used." + ) + kwargs[key] = val - dag.apply_operation_back(gate, qargs=qregs) - circuit = dag_to_circuit(dag) - circuits.append(circuit) + shots = kwargs.pop("shots") - return circuits + if "default_shots" in kwargs: + warnings.warn( + f"default_shots was found in the keyword arguments, but it is not supported by {self.name}" + "Please use the `shots` keyword argument instead. The number of shots " + f"{shots} will be used instead." + ) + kwargs["default_shots"] = shots - def qubit_state_vector_check(self, operation): - """Input check for the StatePrepBase operations. + kwargs, transpile_args = self.get_transpile_args(kwargs) - Args: - operation (pennylane.Operation): operation to be checked + return kwargs, transpile_args - Raises: - DeviceError: If the operation is QubitStateVector or StatePrep - """ - if operation in ("QubitStateVector", "StatePrep"): - if self._is_unitary_backend: - raise DeviceError( - f"The {operation} operation " - "is not supported on the unitary simulator backend." - ) + @staticmethod + def get_transpile_args(kwargs): + """The transpile argument setter. This separates keyword arguments related to transpilation + from the rest of the keyword arguments and removes those keyword arguments from kwargs. - def compile(self): - """Compile the quantum circuit to target the provided compile_backend. + Keyword Args: + kwargs (dict): combined keyword arguments to be parsed for the Qiskit transpiler. For more details, see the + `Qiskit transpiler documentation `_ - If compile_backend is None, then the target is simply the - backend. + Returns: + kwargs (dict), transpile_args (dict): keyword arguments for the runtime options and keyword + arguments for the transpiler """ - compile_backend = self.compile_backend or self.backend - compiled_circuits = transpile(self._circuit, backend=compile_backend, **self.transpile_args) - return compiled_circuits - def run(self, qcirc): - """Run the compiled circuit and query the result. + transpile_sig = inspect.signature(transpile).parameters - Args: - qcirc (qiskit.QuantumCircuit): the quantum circuit to be run on the backend - """ - self._current_job = self.backend.run(qcirc, shots=self.shots, **self.run_args) - result = self._current_job.result() + transpile_args = {arg: kwargs.pop(arg) for arg in transpile_sig if arg in kwargs} + transpile_args.pop("circuits", None) + transpile_args.pop("backend", None) - if self._is_state_backend: - self._state = self._get_state(result) + return kwargs, transpile_args - def _get_state(self, result, experiment=None): - """Returns the statevector for state simulator backends. + def compile_circuits(self, circuits): + """Compiles multiple circuits one after the other. Args: - result (qiskit.Result): result object - experiment (str or None): the name of the experiment to get the state for. + circuits (list[QuantumCircuit]): the circuits to be compiled Returns: - array[float]: size ``(2**num_wires,)`` statevector + list[QuantumCircuit]: the list of compiled circuits """ - if self._is_statevector_backend: - state = np.asarray(result.get_statevector(experiment)) + # Compile each circuit object + compiled_circuits = [] + transpile_args = self._transpile_args - elif self._is_unitary_backend: - unitary = np.asarray(result.get_unitary(experiment)) - initial_state = np.zeros([2**self.num_wires]) - initial_state[0] = 1 + for i, circuit in enumerate(circuits): + compiled_circ = transpile(circuit, backend=self.compile_backend, **transpile_args) + compiled_circ.name = f"circ{i}" + compiled_circuits.append(compiled_circ) - state = unitary @ initial_state + return compiled_circuits - # reverse qubit order to match PennyLane convention - return state.reshape([2] * self.num_wires).T.flatten() + # pylint: disable=unused-argument, no-member + def execute( + self, + circuits: QuantumTape_or_Batch, + execution_config: ExecutionConfig = DefaultExecutionConfig, + ) -> Result_or_ResultBatch: + """Execute a circuit or a batch of circuits and turn it into results.""" + session = self._session or Session(backend=self.backend) - def generate_samples(self, circuit=None): - r"""Returns the computational basis samples generated for all wires. + results = [] - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. + if isinstance(circuits, QuantumScript): + circuits = [circuits] + + @contextmanager + def execute_circuits(session): + try: + for circ in circuits: + if circ.shots and len(circ.shots.shot_vector) > 1: + raise ValueError( + f"Setting shot vector {circ.shots.shot_vector} is not supported for {self.name}." + "Please use a single integer instead when specifying the number of shots." + ) + if isinstance(circ.measurements[0], (ExpectationMP, VarianceMP)) and getattr( + circ.measurements[0].obs, "pauli_rep", None + ): + execute_fn = self._execute_estimator + else: + execute_fn = self._execute_sampler + results.append(execute_fn(circ, session)) + yield results + finally: + session.close() + + with execute_circuits(session) as results: + return results + + def _execute_sampler(self, circuit, session): + """Returns the result of the execution of the circuit using the SamplerV2 Primitive. + Note that this result has been processed respective to the MeasurementProcess given. + E.g. `qml.expval` returns an expectation value whereas `qml.sample()` will return the raw samples. Args: - circuit (str or None): the name of the circuit to get the state for + circuits (list[QuantumCircuit]): the circuits to be executed via SamplerV2 + session (Session): the session that the execution will be performed with Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + result (tuple): the processed result from SamplerV2 """ + qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=True, measure=True)] + sampler = Sampler(session=session) + compiled_circuits = self.compile_circuits(qcirc) + sampler.options.update(**self._kwargs) - # branch out depending on the type of backend - if self._is_state_backend: - # software simulator: need to sample from probabilities - return super().generate_samples() + # len(compiled_circuits) is always 1 so the indexing does not matter. + result = sampler.run( + compiled_circuits, + shots=circuit.shots.total_shots if circuit.shots.total_shots else None, + ).result()[0] + classical_register_name = compiled_circuits[0].cregs[0].name + self._current_job = getattr(result.data, classical_register_name) - # hardware or hardware simulator - samples = self._current_job.result().get_memory(circuit) - # reverse qubit order to match PennyLane convention - return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) + # needs processing function to convert to the correct format for states, and + # also handle instances where wires were specified in probs, and for multiple probs measurements - @property - def state(self): - """Get state of the device""" - return self._state + self._samples = self.generate_samples(0) + res = [ + mp.process_samples(self._samples, wire_order=self.wires) for mp in circuit.measurements + ] - def analytic_probability(self, wires=None): - """Get the analytic probability of the device""" - if self._state is None: - return None + single_measurement = len(circuit.measurements) == 1 + res = (res[0],) if single_measurement else tuple(res) - prob = self.marginal_prob(np.abs(self._state) ** 2, wires) - return prob + return res - def compile_circuits(self, circuits): - r"""Compiles multiple circuits one after the other. + def _execute_estimator(self, circuit, session): + """Returns the result of the execution of the circuit using the EstimatorV2 Primitive. + Note that this result has been processed respective to the MeasurementProcess given. + E.g. `qml.expval` returns an expectation value whereas `qml.var` will return the variance. Args: - circuits (list[.tapes.QuantumTape]): the circuits to be compiled + circuits (list[QuantumCircuit]): the circuits to be executed via EstimatorV2 + session (Session): the session that the execution will be performed with Returns: - list[QuantumCircuit]: the list of compiled circuits + result (tuple): the processed result from EstimatorV2 """ - # Compile each circuit object - compiled_circuits = [] + # the Estimator primitive takes care of diagonalization and measurements itself, + # so diagonalizing gates and measurements are not included in the circuit + qcirc = [circuit_to_qiskit(circuit, self.num_wires, diagonalize=False, measure=False)] + estimator = Estimator(session=session) + + pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements] + compiled_circuits = self.compile_circuits(qcirc) + estimator.options.update(**self._kwargs) + # split into one call per measurement + # could technically be more efficient if there are some observables where we ask + # for expectation value and variance on the same observable, but spending time on + # that right now feels excessive + circ_and_obs = [(compiled_circuits[0], pauli_observables)] + result = estimator.run( + circ_and_obs, + precision=np.sqrt(1 / circuit.shots.total_shots) if circuit.shots else None, + ).result() + self._current_job = result + result = self._process_estimator_job(circuit.measurements, result) + + return result + + @staticmethod + def _process_estimator_job(measurements, job_result): + """Estimator returns the expectation value and standard error for each observable measured, + along with some metadata that contains the precision. Extracts the relevant number for each + measurement process and return the requested results from the Estimator executions. + + Note that for variance, we calculate the variance by using the standard error and the + precision value. - for circuit in circuits: - # We need to reset the device here, else it will - # not start the next computation in the zero state - self.reset() - self.create_circuit_object(circuit.operations, rotations=circuit.diagonalizing_gates) - - compiled_circ = self.compile() - compiled_circ.name = f"circ{len(compiled_circuits)}" - compiled_circuits.append(compiled_circ) - - return compiled_circuits - - def batch_execute(self, circuits, timeout: int = None): - """Batch execute the circuits on the device""" - - compiled_circuits = self.compile_circuits(circuits) + Args: + measurements (list[MeasurementProcess]): the measurements in the circuit + job_result (Any): the result from EstimatorV2 - if not compiled_circuits: - # At least one circuit must always be provided to the backend. - return [] + Returns: + result (tuple): the processed result from EstimatorV2 + """ + expvals = job_result[0].data.evs + variances = (job_result[0].data.stds / job_result[0].metadata["target_precision"]) ** 2 + result = [] + for i, mp in enumerate(measurements): + if isinstance(mp, ExpectationMP): + result.append(expvals[i]) + elif isinstance(mp, VarianceMP): + result.append(variances[i]) - # Send the batch of circuit objects using backend.run - self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args) + single_measurement = len(measurements) == 1 + result = (result[0],) if single_measurement else tuple(result) - try: - result = self._current_job.result(timeout=timeout) - except TypeError: # pragma: no cover - # timeout not supported - result = self._current_job.result() + return result - # increment counter for number of executions of qubit device - # pylint: disable=no-member - self._num_executions += 1 + def generate_samples(self, circuit=None): + r"""Returns the computational basis samples generated for all wires. - # Compute statistics using the state and/or samples - results = [] - for circuit, circuit_obj in zip(circuits, compiled_circuits): - # Update the tracker - if self.tracker.active: - self.tracker.update(executions=1, shots=self.shots) - self.tracker.record() - - if self._is_state_backend: - self._state = self._get_state(result, experiment=circuit_obj) - - # generate computational basis samples - if self.shots is not None or any( - isinstance(m, SAMPLE_TYPES) for m in circuit.measurements - ): - self._samples = self.generate_samples(circuit_obj) - - res = self.statistics(circuit) - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. - if self.tracker.active: - self.tracker.update(batches=1, batch_len=len(circuits)) - self.tracker.record() + Args: + circuit (int): position of the circuit in the batch. - return results + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + counts = self._current_job.get_counts() + # Batch of circuits + if not isinstance(counts, dict): + counts = self._current_job.get_counts()[circuit] + + samples = [] + for key, value in counts.items(): + samples.extend([key] * value) + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) diff --git a/pennylane_qiskit/qiskit_device_legacy.py b/pennylane_qiskit/qiskit_device_legacy.py new file mode 100644 index 000000000..d44a17d75 --- /dev/null +++ b/pennylane_qiskit/qiskit_device_legacy.py @@ -0,0 +1,491 @@ +# Copyright 2019-2021 Xanadu Quantum Technologies Inc. + +# 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. +r""" +This module contains a base class for constructing Qiskit devices for PennyLane. +""" +# pylint: disable=too-many-instance-attributes,attribute-defined-outside-init + + +import abc +import inspect +import warnings + +import numpy as np +from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.compiler import transpile +from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.providers import Backend, BackendV2, QiskitBackendNotFoundError + +from pennylane import QubitDevice, DeviceError +from pennylane.measurements import SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP + +from .converter import QISKIT_OPERATION_MAP +from ._version import __version__ + +SAMPLE_TYPES = (SampleMP, CountsMP, ClassicalShadowMP, ShadowExpvalMP) + + +def _get_backend_name(backend): + try: + return backend.name() # BackendV1 + except TypeError: # pragma: no cover + return backend.name # BackendV2 + + +class QiskitDeviceLegacy(QubitDevice, abc.ABC): + r"""Abstract Qiskit device for PennyLane. + + Args: + wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) + or strings (``['ancilla', 'q1', 'q2']``). + provider (Provider | None): The Qiskit backend provider. + backend (str | Backend): the desired backend. If a string, a provider must be given. + shots (int or None): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. For state vector backends, + setting to ``None`` results in computing statistics like expectation values and variances analytically. + + Keyword Args: + name (str): The name of the circuit. Default ``'circuit'``. + compile_backend (BaseBackend): The backend used for compilation. If you wish + to simulate a device compliant circuit, you can specify a backend here. + """ + + name = "Qiskit PennyLane plugin" + pennylane_requires = ">=0.30.0" + version = __version__ + plugin_version = __version__ + author = "Xanadu" + + _capabilities = { + "model": "qubit", + "tensor_observables": True, + "inverse_operations": True, + } + _operation_map = QISKIT_OPERATION_MAP + _state_backends = { + "statevector_simulator", + "simulator_statevector", + "unitary_simulator", + "aer_simulator_statevector", + "aer_simulator_unitary", + } + """set[str]: Set of backend names that define the backends + that support returning the underlying quantum statevector""" + + operations = set(_operation_map.keys()) + observables = { + "PauliX", + "PauliY", + "PauliZ", + "Identity", + "Hadamard", + "Hermitian", + "Projector", + } + + analytic_warning_message = ( + "The analytic calculation of expectations, variances and " + "probabilities is only supported on statevector backends, not on the {}. " + "Such statistics obtained from this device are estimates based on samples." + ) + + _eigs = {} + + def __init__(self, wires, provider, backend, shots=1024, **kwargs): + + super().__init__(wires=wires, shots=shots) + + self.provider = provider + + if isinstance(backend, Backend): + self._backend = backend + self.backend_name = _get_backend_name(backend) + elif provider is None: + raise ValueError("Must pass a provider if the backend is not a Backend instance.") + else: + try: + self._backend = provider.get_backend(backend) + except QiskitBackendNotFoundError as e: + available_backends = list(map(_get_backend_name, provider.backends())) + raise ValueError( + f"Backend '{backend}' does not exist. Available backends " + f"are:\n {available_backends}" + ) from e + + self.backend_name = _get_backend_name(self._backend) + + # Keep track if the user specified analytic to be True + if shots is None and not self._is_state_backend: + # Raise a warning if no shots were specified for a hardware device + warnings.warn(self.analytic_warning_message.format(backend), UserWarning) + + self.shots = 1024 + + self._capabilities["returns_state"] = self._is_state_backend + + # Perform validation against backend + backend_qubits = ( + backend.num_qubits + if isinstance(backend, BackendV2) + else self.backend.configuration().n_qubits + ) + if backend_qubits and len(self.wires) > int(backend_qubits): + raise ValueError(f"Backend '{backend}' supports maximum {backend_qubits} wires") + + # Initialize inner state + self.reset() + + self.process_kwargs(kwargs) + + def process_kwargs(self, kwargs): + """Processing the keyword arguments that were provided upon device initialization. + + Args: + kwargs (dict): keyword arguments to be set for the device + """ + self.compile_backend = None + if "compile_backend" in kwargs: + self.compile_backend = kwargs.pop("compile_backend") + + if "noise_model" in kwargs: + noise_model = kwargs.pop("noise_model") + self.backend.set_options(noise_model=noise_model) + + # set transpile_args + self.set_transpile_args(**kwargs) + + # Get further arguments for run + self.run_args = {} + + # Specify to have a memory for hw/hw simulators + compile_backend = self.compile_backend or self.backend + memory = str(compile_backend) not in self._state_backends + + if memory: + kwargs["memory"] = True + + # Consider the remaining kwargs as keyword arguments to run + self.run_args.update(kwargs) + + @property + def _is_state_backend(self): + """Returns whether this device has a state backend.""" + return self.backend_name in self._state_backends or self.backend.options.get("method") in { + "unitary", + "statevector", + } + + @property + def _is_statevector_backend(self): + """Returns whether this device has a statevector backend.""" + method = "statevector" + return method in self.backend_name or self.backend.options.get("method") == method + + @property + def _is_unitary_backend(self): + """Returns whether this device has a unitary backend.""" + method = "unitary" + return method in self.backend_name or self.backend.options.get("method") == method + + def set_transpile_args(self, **kwargs): + """The transpile argument setter. + + Keyword Args: + kwargs (dict): keyword arguments to be set for the Qiskit transpiler. For more details, see the + `Qiskit transpiler documentation `_ + """ + transpile_sig = inspect.signature(transpile).parameters + self.transpile_args = {arg: kwargs[arg] for arg in transpile_sig if arg in kwargs} + self.transpile_args.pop("circuits", None) + self.transpile_args.pop("backend", None) + + @property + def backend(self): + """The Qiskit backend object. + + Returns: + qiskit.providers.backend: Qiskit backend object. + """ + return self._backend + + def reset(self): + """Reset the Qiskit backend device""" + # Reset only internal data, not the options that are determined on + # device creation + self._reg = QuantumRegister(self.num_wires, "q") + self._creg = ClassicalRegister(self.num_wires, "c") + self._circuit = QuantumCircuit(self._reg, self._creg, name="temp") + + self._current_job = None + self._state = None # statevector of a simulator backend + + def create_circuit_object(self, operations, **kwargs): + """Builds the circuit objects based on the operations and measurements + specified to apply. + + Args: + operations (list[~.Operation]): operations to apply to the device + + Keyword args: + rotations (list[~.Operation]): Operations that rotate the circuit + pre-measurement into the eigenbasis of the observables. + """ + rotations = kwargs.get("rotations", []) + + applied_operations = self.apply_operations(operations) + + # Rotating the state for measurement in the computational basis + rotation_circuits = self.apply_operations(rotations) + applied_operations.extend(rotation_circuits) + + for circuit in applied_operations: + self._circuit &= circuit + + if not self._is_state_backend: + # Add measurements if they are needed + for qr, cr in zip(self._reg, self._creg): + self._circuit.measure(qr, cr) + elif "aer" in self.backend_name: + self._circuit.save_state() + + def apply(self, operations, **kwargs): + """Build the circuit object and apply the operations""" + self.create_circuit_object(operations, **kwargs) + + # These operations need to run for all devices + compiled_circuit = self.compile() + self.run(compiled_circuit) + + def apply_operations(self, operations): + """Apply the circuit operations. + + This method serves as an auxiliary method to :meth:`~.QiskitDevice.apply`. + + Args: + operations (List[pennylane.Operation]): operations to be applied + + Returns: + list[QuantumCircuit]: a list of quantum circuit objects that + specify the corresponding operations + """ + circuits = [] + + for operation in operations: + # Apply the circuit operations + device_wires = self.map_wires(operation.wires) + par = operation.parameters + + for idx, p in enumerate(par): + if isinstance(p, np.ndarray): + # Convert arrays so that Qiskit accepts the parameter + par[idx] = p.tolist() + + operation = operation.name + + mapped_operation = self._operation_map[operation] + + self.qubit_state_vector_check(operation) + + qregs = [self._reg[i] for i in device_wires.labels] + + if operation in ("QubitUnitary", "QubitStateVector", "StatePrep"): + # Need to revert the order of the quantum registers used in + # Qiskit such that it matches the PennyLane ordering + qregs = list(reversed(qregs)) + + if operation in ("Barrier",): + # Need to add the num_qubits for instantiating Barrier in Qiskit + par = [len(self._reg)] + + dag = circuit_to_dag(QuantumCircuit(self._reg, self._creg, name="")) + + gate = mapped_operation(*par) + + dag.apply_operation_back(gate, qargs=qregs) + circuit = dag_to_circuit(dag) + circuits.append(circuit) + + return circuits + + def qubit_state_vector_check(self, operation): + """Input check for the StatePrepBase operations. + + Args: + operation (pennylane.Operation): operation to be checked + + Raises: + DeviceError: If the operation is QubitStateVector or StatePrep + """ + if operation in ("QubitStateVector", "StatePrep"): + if self._is_unitary_backend: + raise DeviceError( + f"The {operation} operation " + "is not supported on the unitary simulator backend." + ) + + def compile(self): + """Compile the quantum circuit to target the provided compile_backend. + + If compile_backend is None, then the target is simply the + backend. + """ + compile_backend = self.compile_backend or self.backend + compiled_circuits = transpile(self._circuit, backend=compile_backend, **self.transpile_args) + return compiled_circuits + + def run(self, qcirc): + """Run the compiled circuit and query the result. + + Args: + qcirc (qiskit.QuantumCircuit): the quantum circuit to be run on the backend + """ + self._current_job = self.backend.run(qcirc, shots=self.shots, **self.run_args) + result = self._current_job.result() + + if self._is_state_backend: + self._state = self._get_state(result) + + def _get_state(self, result, experiment=None): + """Returns the statevector for state simulator backends. + + Args: + result (qiskit.Result): result object + experiment (str or None): the name of the experiment to get the state for. + + Returns: + array[float]: size ``(2**num_wires,)`` statevector + """ + if self._is_statevector_backend: + state = np.asarray(result.get_statevector(experiment)) + + elif self._is_unitary_backend: + unitary = np.asarray(result.get_unitary(experiment)) + initial_state = np.zeros([2**self.num_wires]) + initial_state[0] = 1 + + state = unitary @ initial_state + + # reverse qubit order to match PennyLane convention + return state.reshape([2] * self.num_wires).T.flatten() + + def generate_samples(self, circuit=None): + r"""Returns the computational basis samples generated for all wires. + + Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where + :math:`q_0` is the most significant bit. + + Args: + circuit (str or None): the name of the circuit to get the state for + + Returns: + array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` + """ + + # branch out depending on the type of backend + if self._is_state_backend: + # software simulator: need to sample from probabilities + return super().generate_samples() + + # hardware or hardware simulator + samples = self._current_job.result().get_memory(circuit) + # reverse qubit order to match PennyLane convention + return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) + + @property + def state(self): + """Get state of the device""" + return self._state + + def analytic_probability(self, wires=None): + """Get the analytic probability of the device""" + if self._state is None: + return None + + prob = self.marginal_prob(np.abs(self._state) ** 2, wires) + return prob + + def compile_circuits(self, circuits): + r"""Compiles multiple circuits one after the other. + + Args: + circuits (list[.tapes.QuantumTape]): the circuits to be compiled + + Returns: + list[QuantumCircuit]: the list of compiled circuits + """ + # Compile each circuit object + compiled_circuits = [] + + for circuit in circuits: + # We need to reset the device here, else it will + # not start the next computation in the zero state + self.reset() + self.create_circuit_object(circuit.operations, rotations=circuit.diagonalizing_gates) + + compiled_circ = self.compile() + compiled_circ.name = f"circ{len(compiled_circuits)}" + compiled_circuits.append(compiled_circ) + + return compiled_circuits + + def batch_execute(self, circuits, timeout: int = None): + """Batch execute the circuits on the device""" + + compiled_circuits = self.compile_circuits(circuits) + + if not compiled_circuits: + # At least one circuit must always be provided to the backend. + return [] + + # Send the batch of circuit objects using backend.run + self._current_job = self.backend.run(compiled_circuits, shots=self.shots, **self.run_args) + + try: + result = self._current_job.result(timeout=timeout) + except TypeError: # pragma: no cover + # timeout not supported + result = self._current_job.result() + + # increment counter for number of executions of qubit device + # pylint: disable=no-member + self._num_executions += 1 + + # Compute statistics using the state and/or samples + results = [] + for circuit, circuit_obj in zip(circuits, compiled_circuits): + # Update the tracker + if self.tracker.active: + self.tracker.update(executions=1, shots=self.shots) + self.tracker.record() + + if self._is_state_backend: + self._state = self._get_state(result, experiment=circuit_obj) + + # generate computational basis samples + if self.shots is not None or any( + isinstance(m, SAMPLE_TYPES) for m in circuit.measurements + ): + self._samples = self.generate_samples(circuit_obj) + + res = self.statistics(circuit) + single_measurement = len(circuit.measurements) == 1 + res = res[0] if single_measurement else tuple(res) + results.append(res) + + if self.tracker.active: + self.tracker.update(batches=1, batch_len=len(circuits)) + self.tracker.record() + + return results diff --git a/pennylane_qiskit/remote.py b/pennylane_qiskit/remote.py index 4be045527..92181c16d 100644 --- a/pennylane_qiskit/remote.py +++ b/pennylane_qiskit/remote.py @@ -24,20 +24,123 @@ class RemoteDevice(QiskitDevice): Args: wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider | None): provider to lookup the backend on (ignored if a backend instance is passed). - backend (str | Backend): the desired backend. Either a name to look up on a provider, or a - BackendV1 or BackendV2 instance. - shots (int or None): number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. For statevector backends, - setting to ``None`` results in computing statistics like expectation values and variances analytically. + or iterable that contains unique labels for the subsystems as numbers + (i.e., ``[-1, 0, 2]``) or strings (``['aux_wire', 'q1', 'q2']``). + backend (Backend): the initialized Qiskit backend Keyword Args: - name (str): The name of the circuit. Default ``'circuit'``. + shots (Union[int, None]): number of circuit evaluations/random samples used + to estimate expectation values and variances of observables. + session (Session): a Qiskit Session to use for device execution. If none is provided, + a session will be created at each device execution. + compile_backend (Union[Backend, None]): the backend to be used for compiling the circuit + that will be sent to the backend device, to be set if the backend desired for + compliation differs from the backend used for execution. Defaults to ``None``, + which means the primary backend will be used. + **kwargs: transpilation and runtime keyword arguments to be used for measurements with + Primitives. If an `options` dictionary is defined amongst the kwargs, and there are + settings that overlap with those in kwargs, the settings in `options` will take + precedence over kwargs. Keyword arguments accepted by both the transpiler and at + runtime (e.g. ``optimization_level``) will be passed to the transpiler rather + than to the Primitive. + + **Example:** + + .. code-block:: python + + import pennylane as qml + from qiskit_ibm_runtime import QiskitRuntimeService + + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.least_busy(n_qubits=127, simulator=False, operational=True) + dev = qml.device("qiskit.remote", wires=127, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(1)) + + >>> circuit(np.pi/3, shots=1024) + 0.529296875 + + This device also supports the use of local simulators such as ``AerSimulator`` or + fake backends such as ``FakeManila``. + + .. code-block:: python + + import pennylane as qml + from qiskit_aer import AerSimulator + + backend = AerSimulator() + dev = qml.device("qiskit.remote", wires=5, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(1)) + + >>> circuit(np.pi/3, shots=1024) + 0.49755859375 + + We can also change the number of shots, either when initializing the device or when we execute + the circuit. Note that the shots number specified on circuit execution will override whatever + was set on device initialization. + + .. code-block:: python + + dev = qml.device("qiskit.remote", wires=5, backend=backend, shots=2) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.sample(qml.PauliZ(1)) + + >>> circuit(np.pi/3) # this will run with 2 shots + array([-1., 1.]) + + >>> circuit(np.pi/3, shots=5) # this will run with 5 shots + array([-1., -1., 1., 1., 1.]) + + >>> circuit(np.pi/3) # this will run with 2 shots + array([-1., 1.]) + + Internally, the device uses the `EstimatorV2 `_ + and the `SamplerV2 `_ + runtime primitives to execute the measurements. To set options for + `transpilation `_ or + `runtime `_, simply pass + the keyword arguments into the device. If you wish to change options other than ``shots``, + PennyLane requires you to re-initialize the device to do so. + + .. code-block:: python + + import pennylane as qml + from qiskit_ibm_runtime.fake_provider import FakeManilaV2 + + backend = FakeManilaV2() + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=1, + seed_transpiler=42, + ) + # to change options, re-initialize the device + dev = qml.device( + "qiskit.remote", + wires=5, + backend=backend, + resilience_level=1, + optimization_level=2, + seed_transpiler=24, + ) """ short_name = "qiskit.remote" - def __init__(self, wires, backend, provider=None, shots=1024, **kwargs): - super().__init__(wires, provider=provider, backend=backend, shots=shots, **kwargs) + def __init__(self, wires, backend, shots=1024, **kwargs): + super().__init__(wires, backend=backend, shots=shots, **kwargs) diff --git a/pennylane_qiskit/runtime_devices.py b/pennylane_qiskit/runtime_devices.py deleted file mode 100644 index 174fd6c85..000000000 --- a/pennylane_qiskit/runtime_devices.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright 2021-2022 Xanadu Quantum Technologies Inc. - -# 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. -""" -This module contains classes for constructing Qiskit runtime devices for PennyLane. -""" -# pylint: disable=attribute-defined-outside-init, protected-access, arguments-renamed - -import numpy as np - -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime.constants import RunnerResult -from pennylane_qiskit.ibmq import IBMQDevice - - -class IBMQCircuitRunnerDevice(IBMQDevice): - r"""Class for a Qiskit runtime circuit-runner program device in PennyLane. Circuit runner is a - runtime program that takes one or more circuits, compiles them, executes them, and optionally - applies measurement error mitigation. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider): The Qiskit simulation provider - backend (str): the desired backend - shots (int): Number of circuit evaluations/random samples used to estimate expectation values and variances of - observables. Default=1024. - - Keyword Args: - initial_layout (array[int]): Initial position of virtual qubits on physical qubits. - layout_method (string): Name of layout selection pass ('trivial', 'dense', 'noise_adaptive', 'sabre') - routing_method (string): Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre'). - translation_method (string): Name of translation pass ('unroller', 'translator', 'synthesis'). - seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. - optimization_level (int): How much optimization to perform on the circuits (0-3). Higher levels generate more - optimized circuits. Default is 1. - init_qubits (bool): Whether to reset the qubits to the ground state for each shot. - rep_delay (float): Delay between programs in seconds. - transpiler_options (dict): Additional compilation options. - measurement_error_mitigation (bool): Whether to apply measurement error mitigation. Default is False. - """ - - short_name = "qiskit.ibmq.circuit_runner" - - def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): - self.kwargs = kwargs - super().__init__(wires=wires, provider=provider, backend=backend, shots=shots, **kwargs) - self.runtime_service = QiskitRuntimeService(channel="ibm_quantum") - - def batch_execute(self, circuits): - compiled_circuits = self.compile_circuits(circuits) - - program_inputs = {"circuits": compiled_circuits, "shots": self.shots} - - for kwarg in self.kwargs: - program_inputs[kwarg] = self.kwargs.get(kwarg) - - # Specify the backend. - options = {"backend": self.backend.name, "job_tags": self.kwargs.get("job_tags")} - - session_id = self.kwargs.get("session_id") - - # Send circuits to the cloud for execution by the circuit-runner program. - job = self.runtime_service.run( - program_id="circuit-runner", - options=options, - inputs=program_inputs, - session_id=session_id, - ) - self._current_job = job.result(decoder=RunnerResult) - - results = [] - - for index, circuit in enumerate(circuits): - self._samples = self.generate_samples(index) - res = self.statistics(circuit) - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) - - if self.tracker.active: - job_time = { - "total_time": self._current_job._metadata.get("time_taken"), - } - self.tracker.update(batches=1, batch_len=len(circuits), job_time=job_time) - self.tracker.record() - - return results - - def generate_samples(self, circuit=None): - r"""Returns the computational basis samples generated for all wires. - - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. - - Args: - circuit (int): position of the circuit in the batch. - - Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` - """ - counts = self._current_job.get_counts() - # Batch of circuits - if not isinstance(counts, dict): - counts = self._current_job.get_counts()[circuit] - - samples = [] - for key, value in counts.items(): - for _ in range(0, value): - samples.append(key) - return np.vstack([np.array([int(i) for i in s[::-1]]) for s in samples]) - - -class IBMQSamplerDevice(IBMQDevice): - r"""Class for a Qiskit runtime sampler program device in PennyLane. Sampler is a Qiskit runtime program - that samples distributions generated by given circuits executed on the target backend. - - Args: - wires (int or Iterable[Number, str]]): Number of subsystems represented by the device, - or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) - or strings (``['ancilla', 'q1', 'q2']``). - provider (Provider): the Qiskit simulation provider - backend (str): the desired backend - shots (int or None): Number of circuit evaluations/random samples used - to estimate expectation values and variances of observables. Default=1024. - - Keyword Args: - circuit_indices (bool): Indices of the circuits to evaluate. Default is ``range(0, len(circuits))``. - run_options (dict): A collection of kwargs passed to backend.run, if shots are given here it will take - precedence over the shots arg. - skip_transpilation (bool): Skip circuit transpilation. Default is False. - """ - - short_name = "qiskit.ibmq.sampler" - - def __init__(self, wires, provider=None, backend="ibmq_qasm_simulator", shots=1024, **kwargs): - self.kwargs = kwargs - super().__init__(wires=wires, provider=provider, backend=backend, shots=shots, **kwargs) - self.runtime_service = QiskitRuntimeService(channel="ibm_quantum") - - def batch_execute(self, circuits): - compiled_circuits = self.compile_circuits(circuits) - - program_inputs = {"circuits": compiled_circuits} - - if "circuits_indices" not in self.kwargs: - circuit_indices = list(range(len(compiled_circuits))) - program_inputs["circuit_indices"] = circuit_indices - else: - circuit_indices = self.kwargs.get("circuit_indices") - - if "run_options" in self.kwargs: - if "shots" not in self.kwargs["run_options"]: - self.kwargs["run_options"]["shots"] = self.shots - else: - self.kwargs["run_options"] = {"shots": self.shots} - - for kwarg in self.kwargs: - program_inputs[kwarg] = self.kwargs.get(kwarg) - - # Specify the backend. - options = {"backend": self.backend.name} - # Send circuits to the cloud for execution by the sampler program. - job = self.runtime_service.run(program_id="sampler", options=options, inputs=program_inputs) - self._current_job = job.result() - - results = [] - - counter = 0 - for index, circuit in enumerate(circuits): - if index in circuit_indices: - self._samples = self.generate_samples(counter) - counter += 1 - res = self.statistics(circuit) - single_measurement = len(circuit.measurements) == 1 - res = res[0] if single_measurement else tuple(res) - results.append(res) - - if self.tracker.active: - self.tracker.update(batches=1, batch_len=len(circuits)) - self.tracker.record() - - return results - - # pylint: disable=arguments-differ - def generate_samples(self, circuit_id=None): - r"""Returns the computational basis samples generated for all wires. - - Note that PennyLane uses the convention :math:`|q_0,q_1,\dots,q_{N-1}\rangle` where - :math:`q_0` is the most significant bit. - - Args: - circuit_id (int): position of the circuit in the batch. - - Returns: - array[complex]: array of samples in the shape ``(dev.shots, dev.num_wires)`` - """ - # We get nearest probability distribution because the quasi-distribution may contain negative probabilities - counts = ( - self._current_job.quasi_dists[circuit_id] - .nearest_probability_distribution() - .binary_probabilities() - ) - # Since qiskit does not return padded string we need to recover the number of qubits with self.num_wires - number_of_states = 2**self.num_wires - # Initialize probabilities to 0 - probs = [0] * number_of_states - # Fill in probabilities from counts: (state, prob) (e.g. ('010', 0.5)) - for state, prob in counts.items(): - # Formatting all strings to the same lenght - while len(state) < self.num_wires: - state = "0" + state[:] - # Inverting the order to recover Pennylane convention - probs[int(state[::-1], 2)] = prob - return self.states_to_binary( - self.sample_basis_states(number_of_states, probs), self.num_wires - ) diff --git a/requirements-ci-legacy.txt b/requirements-ci-legacy.txt deleted file mode 100644 index 91df63432..000000000 --- a/requirements-ci-legacy.txt +++ /dev/null @@ -1,5 +0,0 @@ -pennylane>=0.37 -qiskit<0.46 -qiskit-ibm-runtime<0.21 -numpy -sympy==1.12 diff --git a/requirements.txt b/requirements.txt index 22e5c05ce..ad1fe23f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,44 +1,4 @@ -appdirs==1.4.4 -autograd==1.6.2 -autoray==0.6.11 -cachetools==5.3.3 -certifi==2024.7.4 -cffi==1.16.0 -charset-normalizer==3.3.2 -cryptography==42.0.5 -Cython==3.0.8 -dill==0.3.8 -future==1.0.0 -idna==3.6 -mpmath==1.3.0 -networkx==3.2.1 -ninja==1.11.1.1 -ntlm-auth==1.5.0 -numpy==1.26.4 -orjson==3.9.15 -pbr==6.0.0 -pennylane==0.37 -PennyLane-Lightning==0.37 -ply==3.11 -psutil==5.9.8 -pycparser==2.21 -python-constraint==1.4.0 -python-dateutil==2.8.2 -qiskit==0.45.3 -qiskit-aer==0.13.3 -qiskit-ibm-runtime==0.20.0 -qiskit-ibm-provider==0.10.0 -qiskit-ignis==0.7.1 -qiskit-terra==0.45.3 -requests==2.31.0 -requests-ntlm==1.2.0 -retworkx==0.14.1 -scipy==1.12.0 -semantic-version==2.10.0 -six==1.16.0 -stevedore==5.2.0 -symengine==0.11.0 -sympy==1.12 -toml==0.10.2 -urllib3==2.2.1 -websocket-client==1.7.0 +pennylane>=0.32 +qiskit +numpy +sympy diff --git a/setup.py b/setup.py index ad805ba6c..9eea781c8 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,8 @@ requirements = [ "qiskit>=0.32", "qiskit-aer", - "qiskit-ibm-provider", "qiskit-ibm-runtime", + "qiskit-ibm-provider", "pennylane>=0.37", "numpy", "sympy<1.13", @@ -48,9 +48,6 @@ 'qiskit.aer = pennylane_qiskit:AerDevice', 'qiskit.basicaer = pennylane_qiskit:BasicAerDevice', 'qiskit.basicsim = pennylane_qiskit:BasicSimulatorDevice', - 'qiskit.ibmq = pennylane_qiskit:IBMQDevice', - 'qiskit.ibmq.circuit_runner = pennylane_qiskit:IBMQCircuitRunnerDevice', - 'qiskit.ibmq.sampler = pennylane_qiskit:IBMQSamplerDevice' ], 'pennylane.io': [ 'qiskit = pennylane_qiskit:load', diff --git a/tests/conftest.py b/tests/conftest.py index 06fdb56ee..7fcd26813 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,12 +18,10 @@ import os import pytest import numpy as np -import qiskit import pennylane as qml -from semantic_version import Version from qiskit_ibm_provider import IBMProvider -from pennylane_qiskit import AerDevice, BasicAerDevice, BasicSimulatorDevice +from pennylane_qiskit import AerDevice, BasicSimulatorDevice # pylint: disable=protected-access, unused-argument, redefined-outer-name @@ -40,22 +38,14 @@ A = np.array([[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]]) -if Version(qiskit.__version__) < Version("1.0.0"): - test_devices = [AerDevice, BasicAerDevice] - hw_backends = ["qasm_simulator", "aer_simulator"] - state_backends = [ - "statevector_simulator", - "unitary_simulator", - ] -else: - test_devices = [AerDevice, BasicSimulatorDevice] - hw_backends = ["qasm_simulator", "aer_simulator", "basic_simulator"] - state_backends = [ - "statevector_simulator", - "unitary_simulator", - "aer_simulator_statevector", - "aer_simulator_unitary", - ] +test_devices = [AerDevice, BasicSimulatorDevice] +hw_backends = ["qasm_simulator", "aer_simulator", "basic_simulator"] +state_backends = [ + "statevector_simulator", + "unitary_simulator", + "aer_simulator_statevector", + "aer_simulator_unitary", +] @pytest.fixture diff --git a/tests/test_base_device.py b/tests/test_base_device.py new file mode 100644 index 000000000..30d818860 --- /dev/null +++ b/tests/test_base_device.py @@ -0,0 +1,1583 @@ +# Copyright 2021-2024 Xanadu Quantum Technologies Inc. + +# 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. +""" +This module contains tests for the base Qiskit device for the new PennyLane device API +""" + +from unittest.mock import patch, Mock +from flaky import flaky +import numpy as np +from pennylane import numpy as pnp +from pydantic_core import ValidationError +import pytest + +import pennylane as qml +from pennylane.tape.qscript import QuantumScript +from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session +from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 +from qiskit_aer import AerSimulator + +# do not import Estimator (imported above) from qiskit.primitives - the identically +# named Estimator object has a different call signature than the remote device Estimator, +# and only runs local simulations. We need the Estimator from qiskit_ibm_runtime. They +# both use this EstimatorResults, however: +from qiskit.providers import BackendV1, BackendV2 + +from qiskit import QuantumCircuit, transpile +from pennylane_qiskit.qiskit_device import ( + QiskitDevice, + qiskit_session, + split_execution_types, +) +from pennylane_qiskit.converter import ( + circuit_to_qiskit, + QISKIT_OPERATION_MAP, + mp_to_pauli, +) + +# pylint: disable=protected-access, unused-argument, too-many-arguments, redefined-outer-name + + +# pylint: disable=too-few-public-methods +class Configuration: + def __init__(self, n_qubits, backend_name): + self.n_qubits = n_qubits + self.backend_name = backend_name + self.noise_model = None + + +class MockedBackend(BackendV2): + def __init__(self, num_qubits=10, name="mocked_backend"): + self._options = Configuration(num_qubits, name) + self._service = "SomeServiceProvider" + self.name = name + self._target = Mock() + self._target.num_qubits = num_qubits + + def set_options(self, noise_model): + self.options.noise_model = noise_model + + def _default_options(self): + return {} + + def max_circuits(self): + return 10 + + def run(self, *args, **kwargs): + return None + + @property + def target(self): + return self._target + + +class MockedBackendLegacy(BackendV1): + def __init__(self, num_qubits=10, name="mocked_backend_legacy"): + self._configuration = Configuration(num_qubits, backend_name=name) + self._service = "SomeServiceProvider" + self._options = self._default_options() + + def configuration(self): + return self._configuration + + def _default_options(self): + return {} + + def run(self, *args, **kwargs): + return None + + @property + def options(self): + return self._options + + +# pylint: disable=too-few-public-methods +class MockSession: + def __init__(self, backend, max_time=None): + self._backend = backend + self._max_time = max_time + self._args = "random" # this is to satisfy a mock + self._kwargs = "random" # this is to satisfy a mock + self.session_id = "123" + + def close(self): # This is just to appease a test + pass + + +mocked_backend = MockedBackend() +legacy_backend = MockedBackendLegacy() +aer_backend = AerSimulator() +test_dev = QiskitDevice(wires=5, backend=aer_backend) + + +class TestSupportForV1andV2: + """Tests compatibility with BackendV1 and BackendV2""" + + @pytest.mark.parametrize( + "backend", + [legacy_backend, aer_backend, mocked_backend], + ) + def test_v1_and_v2_mocked(self, backend): + """Test that device initializes with no error mocked""" + dev = QiskitDevice(wires=10, backend=backend) + assert dev._backend == backend + + @pytest.mark.parametrize( + "backend, shape", + [ + (FakeManila(), (1024,)), + (FakeManilaV2(), (1024,)), + ], + ) + def test_v1_and_v2_manila(self, backend, shape): + """Test that device initializes and runs without error with V1 and V2 backends by Qiskit""" + dev = QiskitDevice(wires=5, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return qml.sample(qml.PauliZ(0)) + + res = circuit(np.pi / 2) + + assert np.shape(res) == shape + assert dev._backend == backend + + +class TestDeviceInitialization: + def test_compile_backend_kwarg(self): + """Test that the compile_backend is set correctly if passed, and the main + backend is used otherwise""" + + compile_backend = MockedBackend(name="compile_backend") + main_backend = MockedBackend(name="main_backend") + + dev1 = QiskitDevice(wires=5, backend=main_backend) + dev2 = QiskitDevice(wires=5, backend=main_backend, compile_backend=compile_backend) + + assert dev1._compile_backend == dev1._backend == main_backend + + assert dev2._compile_backend != dev2._backend + assert dev2._compile_backend == compile_backend + + def test_no_shots_warns_and_defaults(self): + """Test that initializing with shots=None raises a warning indicating that + the device is sample based and will default to 1024 shots""" + + with pytest.warns( + UserWarning, + match="Expected an integer number of shots, but received shots=None", + ): + dev = QiskitDevice(wires=2, backend=aer_backend, shots=None) + + assert dev.shots.total_shots == 1024 + + @pytest.mark.parametrize("backend", [aer_backend, legacy_backend]) + def test_backend_wire_validation(self, backend): + """Test that an error is raised if the number of device wires exceeds + the number of wires available on the backend, for both backend versions""" + + with pytest.raises(ValueError, match="supports maximum"): + QiskitDevice(wires=500, backend=backend) + + def test_setting_simulator_noise_model(self): + """Test that the simulator noise model saved on a passed Options + object is used to set the backend noise model""" + + new_backend = MockedBackend() + dev1 = QiskitDevice(wires=3, backend=aer_backend) + dev2 = QiskitDevice(wires=3, backend=new_backend, noise_model={"placeholder": 1}) + + assert dev1.backend.options.noise_model is None + assert dev2.backend.options.noise_model == {"placeholder": 1} + + +class TestQiskitSessionManagement: + """Test using Qiskit sessions with the device""" + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_default_no_session_on_initialization(self, backend): + """Test that the default behaviour is no session at initialization""" + + dev = QiskitDevice(wires=2, backend=backend) + assert dev._session is None + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_initializing_with_session(self, backend): + """Test that you can initialize a device with an existing Qiskit session""" + + session = MockSession(backend=backend, max_time="1m") + dev = QiskitDevice(wires=2, backend=backend, session=session) + assert dev._session == session + + @patch("pennylane_qiskit.qiskit_device.Session") + @pytest.mark.parametrize("initial_session", [None, MockSession(aer_backend)]) + def test_using_session_context(self, mock_session, initial_session): + """Test that you can add a session within a context manager""" + + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) + + assert dev._session == initial_session + + with qiskit_session(dev) as session: + assert dev._session == session + assert dev._session != initial_session + + assert dev._session == initial_session + + def test_using_session_context_options(self): + """Test that you can set session options using qiskit_session""" + dev = QiskitDevice(wires=2, backend=aer_backend) + + assert dev._session is None + + with qiskit_session(dev, max_time=30) as session: + assert dev._session == session + assert dev._session is not None + assert dev._session._max_time == 30 + + assert dev._session is None + + def test_error_when_passing_unexpected_kwarg(self): + """Test that we accept any keyword argument that the user wants to supply so that if + Qiskit allows for more customization we can automatically accomodate those needs. Right + now there are no such keyword arguments, so an error on Qiskit's side is raised.""" + + dev = QiskitDevice(wires=2, backend=aer_backend) + + assert dev._session is None + + with pytest.raises( + TypeError, # Type error for wrong keyword argument differs across python versions + ): + with qiskit_session(dev, any_kwarg=30) as session: + assert dev._session == session + assert dev._session is not None + + assert dev._session is None + + def test_no_warning_when_using_initial_session_options(self): + initial_session = Session(backend=aer_backend, max_time=30) + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) + + assert dev._session == initial_session + + with qiskit_session(dev) as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._max_time == session._max_time + assert dev._session._max_time != initial_session._max_time + + assert dev._session == initial_session + assert dev._session._max_time == initial_session._max_time + + def test_warnings_when_overriding_session_context_options(self, recorder): + """Test that warnings are raised when the session options try to override either the + device's `backend` or `service`. Also ensures that the session options, even the + default options, passed in from the `qiskit_session` take precedence, barring + `backend` or `service`""" + initial_session = Session(backend=aer_backend) + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) + + assert dev._session == initial_session + + with pytest.warns( + UserWarning, + match="Using 'backend' set in device", + ): + with qiskit_session(dev, max_time=30, backend=FakeManilaV2()) as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._backend.name == "aer_simulator" + + with pytest.warns( + UserWarning, + match="Using 'service' set in device", + ): + with qiskit_session(dev, max_time=30, service="placeholder") as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._service != "placeholder" + + # device session should be unchanged by qiskit_session + assert dev._session == initial_session + + max_time_session = Session(backend=aer_backend, max_time=60) + dev = QiskitDevice(wires=2, backend=aer_backend, session=max_time_session) + with qiskit_session(dev, max_time=30) as session: + assert dev._session == session + assert dev._session != initial_session + assert dev._session._max_time == 30 + assert dev._session._max_time != 60 + + assert dev._session == max_time_session + assert dev._session._max_time == 60 + + @pytest.mark.parametrize("initial_session", [None, MockSession(aer_backend)]) + def test_update_session(self, initial_session): + """Test that you can update the session stored on the device""" + + dev = QiskitDevice(wires=2, backend=aer_backend, session=initial_session) + assert dev._session == initial_session + + new_session = MockSession(backend=aer_backend, max_time="1m") + dev.update_session(new_session) + + assert dev._session != initial_session + assert dev._session == new_session + + +class TestDevicePreprocessing: + """Tests the device preprocessing functions""" + + @pytest.mark.parametrize( + "measurements, expectation", + [ + ( + [ + qml.expval(qml.PauliZ(1)), + qml.counts(), + qml.var(qml.PauliY(0)), + qml.probs(wires=[2]), + ], + [ + [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], + [qml.counts(), qml.probs(wires=[2])], + ], + ), + ( + [ + qml.expval(qml.PauliZ(1)), + qml.expval(qml.PauliX(2)), + qml.var(qml.PauliY(0)), + qml.probs(wires=[2]), + ], + [ + [ + qml.expval(qml.PauliZ(1)), + qml.expval(qml.PauliX(2)), + qml.var(qml.PauliY(0)), + ], + [qml.probs(wires=[2])], + ], + ), + ( + [ + qml.expval(qml.PauliZ(1)), + qml.counts(), + qml.var(qml.PauliY(0)), + ], + [ + [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], + [qml.counts()], + ], + ), + ( + [ + qml.expval(qml.PauliZ(1)), + qml.var(qml.PauliY(0)), + ], + [ + [qml.expval(qml.PauliZ(1)), qml.var(qml.PauliY(0))], + ], + ), + ( + [qml.counts(), qml.sample(wires=[1, 0])], + [[qml.counts(), qml.sample(wires=[1, 0])]], + ), + ( + [qml.probs(wires=[2])], + [[qml.probs(wires=[2])]], + ), + ( + [ + qml.expval(qml.Hadamard(0)), + qml.expval(qml.PauliX(0)), + qml.var(qml.PauliZ(0)), + qml.counts(), + ], + [ + [qml.expval(qml.PauliX(0)), qml.var(qml.PauliZ(0))], + [qml.expval(qml.Hadamard(0)), qml.counts()], + ], + ), + ], + ) + @pytest.mark.filterwarnings("ignore::UserWarning") + def test_split_execution_types(self, measurements, expectation): + """Test that the split_execution_types transform splits measurements into Estimator-based + (expval, var) and Sampler-based (everything else)""" + + operations = [qml.PauliX(0), qml.PauliY(1), qml.Hadamard(2), qml.CNOT([2, 1])] + qs = QuantumScript(operations, measurements=measurements) + tapes, reorder_fn = split_execution_types(qs) + + # operations not modified + assert np.all([tape.operations == operations for tape in tapes]) + + # measurements split as expected + assert [tape.measurements for tape in tapes] == expectation + + # reorder_fn puts them back + assert ( + reorder_fn([tape.measurements for tape in tapes]) == qs.measurements[0] + if len(qs.measurements) == 1 + else reorder_fn([tape.measurements for tape in tapes]) == tuple(qs.measurements) + ) + + @pytest.mark.parametrize( + "op, expected", + [ + (qml.PauliX(0), True), + (qml.CRX(0.1, wires=[0, 1]), True), + (qml.sum(qml.PauliY(1), qml.PauliZ(0)), False), + (qml.pow(qml.RX(1.1, 0), 3), False), + (qml.adjoint(qml.S(0)), True), + (qml.adjoint(qml.RX(1.2, 0)), False), + ], + ) + def test_stopping_conditions(self, op, expected): + """Test that stopping_condition works""" + res = test_dev.stopping_condition(op) + assert res == expected + + @pytest.mark.parametrize( + "obs, expected", + [ + (qml.PauliX(0), True), + (qml.Hadamard(3), True), + (qml.prod(qml.PauliY(1), qml.PauliZ(0)), True), + (qml.prod(qml.PauliY(1), qml.PauliZ(0)), True), + ], + ) + def test_observable_stopping_condition(self, obs, expected): + """Test that observable_stopping_condition works""" + res = test_dev.observable_stopping_condition(obs) + assert res == expected + + @pytest.mark.parametrize( + "measurements, num_tapes", + [ + ( + [ + qml.expval(qml.X(0) + qml.Y(0) + qml.Z(0)), + ], + 3, + ), + ( + pytest.param( + [qml.var(qml.X(0) + qml.Y(0) + qml.Z(0))], + 1, + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ) + ), + ( + [ + qml.expval(qml.X(0)), + qml.expval(qml.Y(1)), + qml.expval(qml.Z(0) @ qml.Z(1)), + qml.expval(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + ), + ( + [ + qml.expval( + qml.prod(qml.X(0), qml.Z(0), qml.Z(0)) + 0.35 * qml.X(0) - 0.21 * qml.Z(0) + ) + ], + 2, + ), + ( + pytest.param( + [ + qml.counts(qml.X(0)), + qml.counts(qml.Y(1)), + qml.counts(qml.Z(0) @ qml.Z(1)), + qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ) + ), + ( + pytest.param( + [ + qml.sample(qml.X(0)), + qml.sample(qml.Y(1)), + qml.sample(qml.Z(0) @ qml.Z(1)), + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + ], + 3, + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ) + ), + ], + ) + def test_preprocess_split_non_commuting(self, measurements, num_tapes): + """Test that `split_non_commuting` works as expected in the preprocess function.""" + + dev = QiskitDevice(wires=5, backend=aer_backend) + qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) + + program, _ = dev.preprocess() + tapes, _ = program([qs]) + + assert len(tapes) == num_tapes + + @pytest.mark.parametrize( + "measurements,num_types", + [ + ([qml.expval(qml.PauliZ(0)), qml.probs(wires=[0, 1])], 2), + ([qml.expval(qml.PauliZ(0)), qml.sample(wires=[0, 1])], 2), + ([qml.counts(), qml.probs(wires=[0, 1]), qml.sample()], 1), + ([qml.var(qml.PauliZ(0)), qml.expval(qml.PauliX(1))], 1), + ([qml.probs(wires=[0]), qml.counts(), qml.var(qml.PauliY(2))], 2), + ], + ) + def test_preprocess_splits_incompatible_primitive_measurements(self, measurements, num_types): + """Test that the default behaviour for preprocess it to split the tapes based + on measurement type. Expval and Variance are one type (Estimator), Probs and raw-sample based measurements + are another type (Sampler).""" + + dev = QiskitDevice(wires=5, backend=aer_backend) + qs = QuantumScript([], measurements=measurements, shots=qml.measurements.Shots(1000)) + + program, _ = dev.preprocess() + tapes, _ = program([qs]) + + # measurements that are incompatible are split when use_primtives=True + assert len(tapes) == num_types + + def test_preprocess_decomposes_unsupported_operator(self): + """Test that the device preprocess decomposes operators that + aren't on the list of Qiskit-supported operators""" + qs = QuantumScript( + [qml.CosineWindow(wires=range(2))], measurements=[qml.expval(qml.PauliZ(0))] + ) + + # tape contains unsupported operations + assert not np.all([op in QISKIT_OPERATION_MAP for op in qs.operations]) + + program, _ = test_dev.preprocess() + tapes, _ = program([qs]) + + # tape no longer contained unsupporrted operations + assert np.all([op.name in QISKIT_OPERATION_MAP for op in tapes[0].operations]) + + def test_intial_state_prep_also_decomposes(self): + """Test that the device preprocess decomposes + unsupported operator even if they are state prep operators""" + + qs = QuantumScript( + [qml.AmplitudeEmbedding(features=[0.5, 0.5, 0.5, 0.5], wires=range(2))], + measurements=[qml.expval(qml.PauliZ(0))], + ) + + program, _ = test_dev.preprocess() + tapes, _ = program([qs]) + + assert np.all([op.name in QISKIT_OPERATION_MAP for op in tapes[0].operations]) + + +class TestKwargsHandling: + def test_warning_if_shots(self): + """Test that a warning is raised if the user attempts to specify shots by using + `default_shots`, and instead sets shots to the default amount of 1024.""" + + with pytest.warns( + UserWarning, + match="default_shots was found in the keyword arguments", + ): + dev = QiskitDevice(wires=2, backend=aer_backend, default_shots=333) + + # Qiskit takes in `default_shots` to define the # of shots, therefore we use + # the kwarg "default_shots" rather than shots to pass it to Qiskit. + assert dev._kwargs["default_shots"] == 1024 + + dev = QiskitDevice(wires=2, backend=aer_backend, shots=200) + assert dev._kwargs["default_shots"] == 200 + + with pytest.warns( + UserWarning, + match="default_shots was found in the keyword arguments", + ): + dev = QiskitDevice(wires=2, backend=aer_backend, options={"default_shots": 30}) + # resets to default since we reinitialize the device + assert dev._kwargs["default_shots"] == 1024 + + def test_warning_if_options_and_kwargs_overlap(self): + """Test that a warning is raised if the user has options that overlap with the kwargs""" + + with pytest.warns( + UserWarning, + match="An overlap between", + ): + dev = QiskitDevice( + wires=2, + backend=aer_backend, + options={"resilience_level": 1, "optimization_level": 1}, + resilience_level=2, + random_sauce="spaghetti", + ) + + assert dev._kwargs["resilience_level"] == 1 + assert dev._transpile_args["optimization_level"] == 1 + + # You can initialize the device with any kwarg, but you'll get a ValidationError + # when you run the circuit + assert dev._kwargs["random_sauce"] == "spaghetti" + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + with pytest.raises(ValidationError, match="Object has no attribute"): + circuit() + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_options_and_kwargs_combine_into_unified_kwargs(self, backend): + """Test that options set via the keyword argument options and options set via kwargs + will combine into a single unified kwargs that is passed to the device""" + + dev = QiskitDevice( + wires=5, + backend=backend, + options={"resilience_level": 1}, + execution={"init_qubits": False}, + ) + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + circuit() + assert dev._kwargs["resilience_level"] == 1 + assert dev._kwargs["execution"]["init_qubits"] is False + + circuit(shots=123) + assert dev._kwargs["resilience_level"] == 1 + assert dev._kwargs["execution"]["init_qubits"] is False + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_no_error_is_raised_if_transpilation_options_are_passed(self, backend): + """Tests that when transpilation options are passed in, they are properly + handled without error""" + + dev = QiskitDevice( + wires=5, + backend=backend, + options={"resilience_level": 1, "optimization_level": 1}, + seed_transpiler=42, + ) + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + circuit() + assert dev._kwargs["resilience_level"] == 1 + assert not hasattr(dev._kwargs, "seed_transpiler") + assert dev._transpile_args["seed_transpiler"] == 42 + + # Make sure that running the circuit again doesn't change the optios + circuit(shots=5) + assert dev._kwargs["resilience_level"] == 1 + assert not hasattr(dev._kwargs, "seed_transpiler") + assert dev._transpile_args["seed_transpiler"] == 42 + + +class TestDeviceProperties: + def test_name_property(self): + """Test the backend property""" + assert test_dev.name == "QiskitDevice" + + def test_backend_property(self): + """Test the backend property""" + assert test_dev.backend == test_dev._backend + assert test_dev.backend == aer_backend + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_compile_backend_property(self, backend): + """Test the compile_backend property""" + + compile_backend = MockedBackend(name="compile_backend") + dev = QiskitDevice(wires=5, backend=backend, compile_backend=compile_backend) + + assert dev.compile_backend == dev._compile_backend + assert dev.compile_backend == compile_backend + + def test_service_property(self): + """Test the service property""" + assert test_dev.service == test_dev._service + + def test_session_property(self): + """Test the session property""" + + session = MockSession(backend=aer_backend) + dev = QiskitDevice(wires=2, backend=aer_backend, session=session) + assert dev.session == dev._session + assert dev.session == session + + def test_num_wires_property(self): + """Test the num_wires property""" + + wires = [1, 2, 3] + dev = QiskitDevice(wires=wires, backend=aer_backend) + assert dev.num_wires == len(wires) + + +class TestTrackerFunctionality: + def test_tracker_batched(self): + """Test that the tracker works for batched circuits""" + dev = qml.device("default.qubit", wires=1, shots=10000) + qiskit_dev = QiskitDevice(wires=1, backend=AerSimulator(), shots=10000) + + x = pnp.array(0.1, requires_grad=True) + + @qml.qnode(dev, diff_method="parameter-shift") + def circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)) + + @qml.qnode(qiskit_dev, diff_method="parameter-shift") + def qiskit_circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)) + + with qml.Tracker(dev) as tracker: + qml.grad(circuit)(x) + + with qml.Tracker(qiskit_dev) as qiskit_tracker: + qml.grad(qiskit_circuit)(x) + + assert qiskit_tracker.history["batches"] == tracker.history["batches"] + assert tracker.history["shots"] == qiskit_tracker.history["shots"] + assert np.allclose(qiskit_tracker.history["results"], tracker.history["results"], atol=0.1) + assert np.shape(qiskit_tracker.history["results"]) == np.shape(tracker.history["results"]) + assert qiskit_tracker.history["resources"][0] == tracker.history["resources"][0] + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + def test_tracker_single_tape(self): + """Test that the tracker works for a single tape""" + dev = qml.device("default.qubit", wires=1, shots=10000) + qiskit_dev = QiskitDevice(wires=1, backend=AerSimulator(), shots=10000) + + tape = qml.tape.QuantumTape([qml.S(0)], [qml.expval(qml.X(0))]) + with qiskit_dev.tracker: + qiskit_out = qiskit_dev.execute(tape) + + with dev.tracker: + pl_out = dev.execute(tape) + + assert ( + qiskit_dev.tracker.history["resources"][0].shots + == dev.tracker.history["resources"][0].shots + ) + assert np.allclose(pl_out, qiskit_out, atol=0.1) + assert np.allclose( + qiskit_dev.tracker.history["results"], dev.tracker.history["results"], atol=0.1 + ) + + assert np.shape(qiskit_dev.tracker.history["results"]) == np.shape( + dev.tracker.history["results"] + ) + + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + def test_tracker_split_by_measurement_type(self): + """Test that the tracker works for as intended for circuits split by measurement type""" + qiskit_dev = QiskitDevice(wires=5, backend=AerSimulator(), shots=10000) + + x = 0.1 + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)), qml.counts(qml.X(1)) + + with qml.Tracker(qiskit_dev) as qiskit_tracker: + qiskit_circuit(x) + + assert qiskit_tracker.totals["executions"] == 2 + assert qiskit_tracker.totals["shots"] == 20000 + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + def test_tracker_split_by_non_commute(self): + """Test that the tracker works for as intended for circuits split by non commute""" + qiskit_dev = QiskitDevice(wires=5, backend=AerSimulator(), shots=10000) + + x = 0.1 + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.Z(0)), qml.expval(qml.X(0)) + + with qml.Tracker(qiskit_dev) as qiskit_tracker: + qiskit_circuit(x) + + assert qiskit_tracker.totals["executions"] == 2 + assert qiskit_tracker.totals["shots"] == 20000 + assert "simulations" not in qiskit_dev.tracker.history + assert "simulations" not in qiskit_dev.tracker.latest + assert "simulations" not in qiskit_dev.tracker.totals + + +class TestMockedExecution: + def test_get_transpile_args(self): + """Test that get_transpile_args works as expected by filtering out + kwargs that don't match the Qiskit transpile signature""" + + # on a device + transpile_args = { + "random_kwarg": 3, + "seed_transpiler": 42, + "optimization_level": 3, + "circuits": [], + } + compile_backend = MockedBackend(name="compile_backend") + dev = QiskitDevice( + wires=5, backend=aer_backend, compile_backend=compile_backend, **transpile_args + ) + assert dev._transpile_args == { + "optimization_level": 3, + "seed_transpiler": 42, + } + + @patch("pennylane_qiskit.qiskit_device.transpile") + @pytest.mark.parametrize("compile_backend", [None, MockedBackend(name="compile_backend")]) + def test_compile_circuits(self, transpile_mock, compile_backend): + """Tests compile_circuits with a mocked transpile function to avoid calling + a remote backend. Confirm compile_backend and transpile_args are used.""" + + transpile_args = {"seed_transpiler": 42, "optimization_level": 2} + dev = QiskitDevice( + wires=5, backend=aer_backend, compile_backend=compile_backend, **transpile_args + ) + + transpile_mock.return_value = QuantumCircuit(2) + + # technically this doesn't matter due to the mock, but this is the correct input format for the function + circuits = [ + QuantumScript([qml.PauliX(0)], measurements=[qml.expval(qml.PauliZ(0))]), + QuantumScript([qml.PauliX(0)], measurements=[qml.probs(wires=[0])]), + QuantumScript([qml.PauliX(0), qml.PauliZ(1)], measurements=[qml.counts()]), + ] + input_circuits = [circuit_to_qiskit(c, register_size=2) for c in circuits] + + with patch.object(dev, "get_transpile_args", return_value=transpile_args): + compiled_circuits = dev.compile_circuits(input_circuits) + + transpile_mock.assert_called_with( + input_circuits[2], backend=dev.compile_backend, **dev._transpile_args + ) + + assert len(compiled_circuits) == len(input_circuits) + for _, circuit in enumerate(compiled_circuits): + assert isinstance(circuit, QuantumCircuit) + + @pytest.mark.parametrize( + "results, index", + [ + ({"00": 125, "10": 500, "01": 250, "11": 125}, None), + ([{}, {"00": 125, "10": 500, "01": 250, "11": 125}], 1), + ([{}, {}, {"00": 125, "10": 500, "01": 250, "11": 125}], 2), + ], + ) + def test_generate_samples_mocked_single_result(self, results, index): + """Test generate_samples with a Mocked return for the job result + (integration test that runs with a Token is below)""" + + # create mocked Job with results dict + def get_counts(): + return results + + mock_job = Mock() + mock_job.configure_mock(get_counts=get_counts) + test_dev._current_job = mock_job + + samples = test_dev.generate_samples(circuit=index) + results_dict = results if index is None else results[index] + + assert len(samples) == sum(results_dict.values()) + assert len(samples[0]) == 2 + + assert len(np.argwhere([np.allclose(s, [0, 0]) for s in samples])) == results_dict["00"] + assert len(np.argwhere([np.allclose(s, [1, 1]) for s in samples])) == results_dict["11"] + + # order of samples is swapped compared to keys (Qiskit wire order convention is reverse of PennyLane) + assert len(np.argwhere([np.allclose(s, [0, 1]) for s in samples])) == results_dict["10"] + assert len(np.argwhere([np.allclose(s, [1, 0]) for s in samples])) == results_dict["01"] + + @patch("pennylane_qiskit.qiskit_device.QiskitDevice._execute_estimator") + def test_execute_pipeline_primitives_no_session(self, mocker): + """Test that a Primitives-based device initialized with no Session creates one for the + execution, and then returns the device session to None.""" + + dev = QiskitDevice(wires=5, backend=aer_backend, session=None) + + assert dev._session is None + + qs = QuantumScript([qml.PauliX(0), qml.PauliY(1)], measurements=[qml.expval(qml.PauliZ(0))]) + + with patch("pennylane_qiskit.qiskit_device.Session") as mock_session: + dev.execute(qs) + mock_session.assert_called_once() # a session was created + + assert dev._session is None # the device session is still None + + @pytest.mark.parametrize("backend", [aer_backend, legacy_backend, FakeManila(), FakeManilaV2()]) + def test_execute_pipeline_with_all_execute_types_mocked(self, mocker, backend): + """Test that a device executes measurements that require raw samples via the sampler, + and the relevant primitive measurements via the estimator""" + + dev = QiskitDevice(wires=5, backend=backend, session=MockSession(backend)) + + qs = QuantumScript( + [qml.PauliX(0), qml.PauliY(1)], + measurements=[ + qml.expval(qml.PauliZ(0)), + qml.probs(wires=[0, 1]), + qml.counts(), + qml.sample(), + ], + ) + tapes, _ = split_execution_types(qs) + + with patch.object(dev, "_execute_sampler", return_value="sampler_execute_res"): + with patch.object(dev, "_execute_estimator", return_value="estimator_execute_res"): + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + res = dev.execute(tapes) + + sampler_execute.assert_called_once() + estimator_execute.assert_called_once() + + assert res == [ + "estimator_execute_res", + "sampler_execute_res", + ] + + @patch("pennylane_qiskit.qiskit_device.Estimator") + @patch("pennylane_qiskit.qiskit_device.QiskitDevice._process_estimator_job") + @pytest.mark.parametrize("session", [None, MockSession(aer_backend)]) + def test_execute_estimator_mocked(self, mocked_estimator, mocked_process_fn, session): + """Test the _execute_estimator function using a mocked version of Estimator + that returns a meaningless result.""" + + qs = QuantumScript( + [qml.PauliX(0)], + measurements=[qml.expval(qml.PauliY(0)), qml.var(qml.PauliX(0))], + shots=100, + ) + result = test_dev._execute_estimator(qs, session) + + # to emphasize, this did nothing except appease CodeCov + assert isinstance(result, Mock) + + def test_shot_vector_error_mocked(self): + """Test that a device that executes a circuit with an array of shots raises the appropriate ValueError""" + + dev = QiskitDevice(wires=5, backend=aer_backend, session=MockSession(aer_backend)) + qs = QuantumScript( + measurements=[ + qml.expval(qml.PauliX(0)), + ], + shots=[5, 10, 2], + ) + + with patch.object(dev, "_execute_estimator"): + with pytest.raises(ValueError, match="Setting shot vector"): + dev.execute(qs) + + +class TestExecution: + + @pytest.mark.parametrize("wire", [0, 1]) + @pytest.mark.parametrize( + "angle, op, expectation", + [ + (np.pi / 2, qml.RX, [0, -1, 0, 1, 0, 1]), + (np.pi, qml.RX, [0, 0, -1, 1, 1, 0]), + (np.pi / 2, qml.RY, [1, 0, 0, 0, 1, 1]), + (np.pi, qml.RY, [0, 0, -1, 1, 1, 0]), + (np.pi / 2, qml.RZ, [0, 0, 1, 1, 1, 0]), + ], + ) + @flaky(max_runs=10, min_passes=7) + def test_estimator_with_different_pauli_obs(self, mocker, wire, angle, op, expectation): + """Test that the Estimator with various observables returns expected results. + Essentially testing that the conversion to PauliOps in _execute_estimator behaves as + expected. Iterating over wires ensures that the wire operated on and the wire measured + correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) + """ + + dev = QiskitDevice(wires=5, backend=aer_backend) + + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + qs = QuantumScript( + [op(angle, wire)], + measurements=[ + qml.expval(qml.PauliX(wire)), + qml.expval(qml.PauliY(wire)), + qml.expval(qml.PauliZ(wire)), + qml.var(qml.PauliX(wire)), + qml.var(qml.PauliY(wire)), + qml.var(qml.PauliZ(wire)), + ], + ) + + res = dev.execute(qs) + + sampler_execute.assert_not_called() + estimator_execute.assert_called_once() + + assert np.allclose(res, expectation, atol=0.1) + + @pytest.mark.parametrize("wire", [0, 1, 2, 3]) + @pytest.mark.parametrize( + "angle, op, multi_q_obs", + [ + ( + np.pi / 2, + qml.RX, + qml.ops.LinearCombination([1, 3], [qml.X(3) @ qml.Y(1), qml.Z(0) * 3]), + ), + ( + np.pi, + qml.RX, + qml.ops.LinearCombination([1, 3], [qml.X(3) @ qml.Y(1), qml.Z(0) * 3]) + - 4 * qml.X(2), + ), + (np.pi / 2, qml.RY, qml.sum(qml.PauliZ(0), qml.PauliX(1))), + (np.pi, qml.RY, qml.dot([2, 3], [qml.X(0), qml.Y(0)])), + ( + np.pi / 2, + qml.RZ, + qml.Hamiltonian([1], [qml.X(0) @ qml.Y(2)]) - 3 * qml.Z(3) @ qml.Z(1), + ), + ], + ) + @flaky(max_runs=10, min_passes=7) + def test_estimator_with_various_multi_qubit_pauli_obs( + self, mocker, wire, angle, op, multi_q_obs + ): + """Test that the Estimator with various multi-qubit observables returns expected results. + Essentially testing that the conversion to PauliOps in _execute_estimator behaves as + expected. Iterating over wires ensures that the wire operated on and the wire measured + correspond correctly (wire ordering convention in Qiskit and PennyLane don't match.) + """ + + pl_dev = qml.device("default.qubit", wires=[0, 1, 2, 3]) + dev = QiskitDevice(wires=[0, 1, 2, 3], backend=aer_backend) + + sampler_execute = mocker.spy(dev, "_execute_sampler") + estimator_execute = mocker.spy(dev, "_execute_estimator") + + qs = QuantumScript( + [op(angle, wire)], + measurements=[ + qml.expval(multi_q_obs), + qml.var(multi_q_obs), + ], + shots=10000, + ) + + res = dev.execute(qs) + expectation = pl_dev.execute(qs) + + sampler_execute.assert_not_called() + estimator_execute.assert_called_once() + + assert np.allclose(res[0], expectation, atol=0.3) ## atol is high due to high variance + + def test_tape_shots_used_for_estimator(self, mocker): + """Tests that device uses tape shots rather than device shots for estimator""" + dev = QiskitDevice(wires=5, backend=aer_backend, shots=2) + + estimator_execute = mocker.spy(dev, "_execute_estimator") + + @qml.qnode(dev) + def circuit(): + return qml.expval(qml.PauliX(0)) + + circuit(shots=[5]) + + estimator_execute.assert_called_once() + # calculates # of shots executed from precision + assert int(np.ceil(1 / dev._current_job[0].metadata["target_precision"] ** 2)) == 5 + + circuit() + assert int(np.ceil(1 / dev._current_job[0].metadata["target_precision"] ** 2)) == 2 + + @pytest.mark.parametrize( + "measurements, expectation", + [ + ([qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0))], (1, 0)), + ([qml.var(qml.PauliX(0))], (1)), + ( + [ + qml.expval(qml.PauliX(0)), + qml.expval(qml.PauliZ(0)), + qml.var(qml.PauliX(0)), + ], + (0, 1, 1), + ), + ([qml.expval(0.5 * qml.Y(0) + 0.5 * qml.Y(0) - 1.5 * qml.X(0) - 0.5 * qml.Y(0))], (0)), + ( + [ + qml.expval( + qml.ops.LinearCombination( + [1, 3, 4], [qml.X(3) @ qml.Y(2), qml.Y(4) - qml.X(2), qml.Z(2) * 3] + ) + + qml.X(4) + ) + ], + (16), + ), + ], + ) + @flaky(max_runs=10, min_passes=7) + def test_process_estimator_job(self, measurements, expectation): + """Tests that the estimator returns expected and accurate results for an ``expval`` and ``var`` for a variety of multi-qubit observables""" + + # make PennyLane circuit + qs = QuantumScript([], measurements=measurements) + + # convert to Qiskit circuit information + qcirc = circuit_to_qiskit(qs, register_size=qs.num_wires, diagonalize=False, measure=False) + pauli_observables = [mp_to_pauli(mp, qs.num_wires) for mp in qs.measurements] + + # run on simulator via Estimator + estimator = Estimator(backend=aer_backend) + compiled_circuits = [transpile(qcirc, backend=aer_backend)] + circ_and_obs = [(compiled_circuits[0], pauli_observables)] + result = estimator.run(circ_and_obs).result() + + assert isinstance(result[0].data.evs, np.ndarray) + assert result[0].data.evs.size == len(qs.measurements) + + assert isinstance(result[0].metadata, dict) + + processed_result = QiskitDevice._process_estimator_job(qs.measurements, result) + assert isinstance(processed_result, tuple) + assert np.allclose(processed_result, expectation, atol=0.1) + + @pytest.mark.parametrize("num_wires", [1, 3, 5]) + @pytest.mark.parametrize("num_shots", [50, 100]) + def test_generate_samples(self, num_wires, num_shots): + qs = QuantumScript([], measurements=[qml.expval(qml.PauliX(0))]) + dev = QiskitDevice(wires=num_wires, backend=aer_backend, shots=num_shots) + dev._execute_sampler(circuit=qs, session=Session(backend=aer_backend)) + + samples = dev.generate_samples(0) + + assert len(samples) == num_shots + assert len(samples[0]) == num_wires + + # we expect the samples to be orderd such that q0 has a 50% chance + # of being excited, and everything else is in the ground state + exp_res0 = np.zeros(num_wires) + exp_res1 = np.zeros(num_wires) + exp_res1[0] = 1 + + # the two expected results are in samples + assert exp_res1 in samples + assert exp_res0 in samples + + # nothing else is in samples + assert [s for s in samples if not s in np.array([exp_res0, exp_res1])] == [] + + def test_tape_shots_used_for_sampler(self, mocker): + """Tests that device uses tape shots rather than device shots for sampler""" + dev = QiskitDevice(wires=5, backend=aer_backend, shots=2) + + sampler_execute = mocker.spy(dev, "_execute_sampler") + + @qml.qnode(dev) + def circuit(): + qml.PauliX(0) + return qml.probs(wires=[0, 1]) + + circuit(shots=[5]) + + sampler_execute.assert_called_once() + assert dev._current_job.num_shots == 5 + + # Should reset to device shots if circuit ran again without shots defined + circuit() + assert dev._current_job.num_shots == 2 + + def test_error_for_shot_vector(self): + """Tests that a ValueError is raised if a shot vector is passed.""" + dev = QiskitDevice(wires=5, backend=aer_backend, shots=2) + + @qml.qnode(dev) + def circuit(): + return qml.sample(qml.PauliX(0)) + + with pytest.raises(ValueError, match="Setting shot vector"): + circuit(shots=[5, 10, 2]) + + # Should reset to device shots if circuit ran again without shots defined + circuit() + assert dev._current_job.num_shots == 2 + + @pytest.mark.parametrize( + "observable", + [ + [qml.Hadamard(0), qml.PauliX(1)], + [qml.PauliZ(0), qml.Hadamard(1)], + [qml.PauliZ(0), qml.Hadamard(0)], + ], + ) + @pytest.mark.filterwarnings("ignore::UserWarning") + @flaky(max_runs=10, min_passes=7) + def test_no_pauli_observable_gives_accurate_answer(self, mocker, observable): + """Test that the device uses _sampler and _execute_estimator appropriately and + provides an accurate answer for measurements with observables that don't have a pauli_rep. + """ + + dev = QiskitDevice(wires=5, backend=aer_backend) + + pl_dev = qml.device("default.qubit", wires=5) + + estimator_execute = mocker.spy(dev, "_execute_estimator") + sampler_execute = mocker.spy(dev, "_execute_sampler") + + @qml.qnode(dev) + def circuit(): + qml.X(0) + qml.Hadamard(0) + return qml.expval(observable[0]), qml.expval(observable[1]) + + @qml.qnode(pl_dev) + def pl_circuit(): + qml.X(0) + qml.Hadamard(0) + return qml.expval(observable[0]), qml.expval(observable[1]) + + res = circuit() + pl_res = pl_circuit() + + estimator_execute.assert_called_once() + sampler_execute.assert_called_once() + + assert np.allclose(res, pl_res, atol=0.1) + + def test_warning_for_split_execution_types_when_observable_no_pauli(self): + """Test that a warning is raised when device is passed a measurement on + an observable that does not have a pauli_rep.""" + + dev = QiskitDevice(wires=5, backend=aer_backend) + + @qml.qnode(dev) + def circuit(): + qml.X(0) + qml.Hadamard(0) + return qml.expval(qml.Hadamard(0)) + + with pytest.warns( + UserWarning, + match="The observable measured", + ): + circuit() + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_qiskit_probability_output_format(self, backend): + """Test that the format and values of the Qiskit device's output for `qml.probs` is + the same as pennylane's.""" + + dev = qml.device("default.qubit", wires=[0, 1, 2, 3, 4]) + qiskit_dev = QiskitDevice(wires=[0, 1, 2, 3, 4], backend=backend) + + @qml.qnode(dev) + def circuit(): + qml.Hadamard(0) + return [qml.probs(wires=[0, 1])] + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.Hadamard(0) + return [qml.probs(wires=[0, 1])] + + res = circuit() + qiskit_res = qiskit_circuit() + + assert np.shape(res) == np.shape(qiskit_res) + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_sampler_output_shape(self, backend): + """Test that the shape of the results produced from the sampler for the Qiskit device + is consistent with Pennylane""" + dev = qml.device("default.qubit", wires=5, shots=1024) + qiskit_dev = QiskitDevice(wires=5, backend=backend) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))] + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))] + + res = circuit(np.pi / 2) + qiskit_res = qiskit_circuit(np.pi / 2) + + assert np.shape(res) == np.shape(qiskit_res) + + @pytest.mark.parametrize("backend", [aer_backend, FakeManila(), FakeManilaV2()]) + def test_sampler_output_shape_multi_measurements(self, backend): + """Test that the shape of the results produced from the sampler for the Qiskit device + is consistent with Pennylane for circuits with multiple measurements""" + dev = qml.device("default.qubit", wires=5, shots=10) + qiskit_dev = QiskitDevice(wires=5, backend=backend, shots=10) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return ( + qml.sample(), + qml.sample(qml.Y(0)), + qml.expval(qml.X(1)), + qml.var(qml.Y(0)), + qml.counts(), + ) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(x): + qml.RX(x, wires=[0]) + qml.CNOT(wires=[0, 1]) + return ( + qml.sample(), + qml.sample(qml.Y(0)), + qml.expval(qml.X(1)), + qml.var(qml.Y(0)), + qml.counts(), + ) + + res = circuit(np.pi / 2) + qiskit_res = qiskit_circuit(np.pi / 2) + + assert np.shape(res[0]) == np.shape(qiskit_res[0]) + assert np.shape(res[1]) == np.shape(qiskit_res[1]) + assert len(res) == len(qiskit_res) + + @pytest.mark.parametrize( + "observable", + [ + lambda: [qml.expval(qml.Hadamard(0)), qml.expval(qml.Hadamard(0))], + lambda: [ + qml.var(qml.Hadamard(0)), + qml.var(qml.Hadamard(0)), + ], + lambda: [ + qml.expval(qml.X(0)), + qml.expval(qml.Y(1)), + qml.expval(0.5 * qml.Y(1)), + qml.expval(qml.Z(0) @ qml.Z(1)), + qml.expval(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.expval( + qml.ops.LinearCombination( + [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] + ) + ), + qml.expval( + qml.ops.LinearCombination( + [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" + ) + ), + ], + lambda: [ + qml.expval( + qml.Hamiltonian([0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) + ) + ], + lambda: [qml.expval(qml.X(0) @ qml.Z(1) + qml.Z(0))], + pytest.param( + [qml.var(qml.X(0) + qml.Z(0))], + marks=pytest.mark.xfail(reason="Qiskit itself is bugged when given Sum"), + ), + lambda: [ + qml.expval(qml.Hadamard(0)), + qml.expval(qml.Hadamard(1)), + qml.expval(qml.Hadamard(0) @ qml.Hadamard(1)), + qml.expval( + qml.Hadamard(0) @ qml.Hadamard(1) + 0.5 * qml.Hadamard(1) + qml.Hadamard(0) + ), + ], + ], + ) + @flaky(max_runs=10, min_passes=7) + def test_observables_that_need_split_non_commuting(self, observable): + """Tests that observables that have non-commuting measurements are + processed correctly when executed by the Estimator or, in the case of + qml.Hadamard, executed by the Sampler via expval() or var""" + qiskit_dev = QiskitDevice(wires=3, backend=aer_backend, shots=30000) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + dev = qml.device("default.qubit", wires=3, shots=30000) + + @qml.qnode(dev) + def circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + qiskit_res = qiskit_circuit() + res = circuit() + + assert np.allclose(res, qiskit_res, atol=0.05) + + @pytest.mark.parametrize( + "observable", + [ + pytest.param( + lambda: [qml.counts(qml.X(0) + qml.Y(0)), qml.counts(qml.X(0))], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.counts(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.counts(0.5 * qml.Y(1)), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + ], + ) + @flaky(max_runs=10, min_passes=7) + def test_observables_that_need_split_non_commuting_counts(self, observable): + """Tests that observables that have non-commuting measurents are processed + correctly when executed by the Sampler via counts()""" + qiskit_dev = QiskitDevice(wires=3, backend=aer_backend, shots=4000) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + dev = qml.device("default.qubit", wires=3, shots=4000) + + @qml.qnode(dev) + def circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + qiskit_res = qiskit_circuit() + res = circuit() + + assert len(qiskit_res) == len(res) + for res1, res2 in zip(qiskit_res, res): + assert all(res1[key] - res2.get(key, 0) < 300 for key in res1) + + @pytest.mark.parametrize( + "observable", + [ + pytest.param( + lambda: [qml.sample(qml.X(0) + qml.Y(0)), qml.sample(qml.X(0))], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.sample(0.5 * qml.Y(1)), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample(qml.X(0)), + qml.sample(qml.Y(1)), + qml.sample(0.5 * qml.Y(1)), + qml.sample(qml.Z(0) @ qml.Z(1)), + qml.sample(qml.X(0) @ qml.Z(1) + 0.5 * qml.Y(1) + qml.Z(0)), + qml.sample( + qml.ops.LinearCombination( + [0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.X(2)] + ) + ), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample( + qml.ops.LinearCombination( + [1.0, 2.0, 3.0], [qml.X(0), qml.X(1), qml.Z(0)], grouping_type="qwc" + ) + ), + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + pytest.param( + lambda: [ + qml.sample( + qml.Hamiltonian([0.35, 0.46], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) + ) + ], + marks=pytest.mark.xfail(reason="Split non commuting discussion pending"), + ), + ], + ) + @flaky(max_runs=10, min_passes=7) + def test_observables_that_need_split_non_commuting_samples(self, observable): + """Tests that observables that have non-commuting measurents are processed + correctly when executed by the Sampler via sample()""" + qiskit_dev = QiskitDevice(wires=3, backend=aer_backend, shots=20000) + + @qml.qnode(qiskit_dev) + def qiskit_circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + dev = qml.device("default.qubit", wires=3, shots=20000) + + @qml.qnode(dev) + def circuit(): + qml.RX(np.pi / 3, 0) + qml.RZ(np.pi / 3, 0) + return observable() + + qiskit_res = qiskit_circuit() + res = circuit() + + assert np.allclose(np.mean(qiskit_res, axis=1), np.mean(res, axis=1), atol=0.05) diff --git a/tests/test_converter.py b/tests/test_converter.py index 6d13f55f1..ba61ab33d 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -27,7 +27,9 @@ from qiskit.quantum_info import SparsePauliOp import pennylane as qml +from pennylane import I, X, Y, Z from pennylane import numpy as np +from pennylane.tape.qscript import QuantumScript from pennylane.measurements import MidMeasureMP from pennylane.wires import Wires from pennylane_qiskit.converter import ( @@ -36,6 +38,9 @@ load_qasm, load_qasm_from_file, map_wires, + circuit_to_qiskit, + operation_to_qiskit, + mp_to_pauli, _format_params_dict, _check_parameter_bound, ) @@ -48,7 +53,7 @@ VARPHI = np.linspace(0.02, 3, 5) -class TestConverter: +class TestConverterQiskitToPennyLane: """Tests the converter function that allows converting QuantumCircuit objects to Pennylane templates.""" @@ -419,7 +424,7 @@ def test_wires_pass_different_wires_than_for_circuit(self, recorder): assert recorder.queue[0].wires == Wires(three_wires) -class TestConverterGates: +class TestConverterGatesQiskitToPennyLane: """Tests over specific gate related tests""" @pytest.mark.parametrize( @@ -1641,6 +1646,374 @@ def test_diff_meas_circuit(self): assert qtemp()[0] != qtemp2()[0] and qtemp2()[0] == qml.expval(qml.PauliZ(0)) +class TestConverterPennyLaneCircuitToQiskit: + def test_circuit_to_qiskit(self): + """Test that a simple PennyLane circuit is converted to the expected Qiskit circuit""" + + qscript = QuantumScript([qml.Hadamard(1), qml.CNOT([1, 0])]) + qc = circuit_to_qiskit(qscript, len(qscript.wires), diagonalize=False, measure=False) + + operation_names = [instruction.operation.name for instruction in qc.data] + + assert operation_names == ["h", "cx"] + + def test_circuit_to_qiskit_with_parameterized_gate(self): + """Test that a simple PennyLane circuit is converted to the expected Qiskit circuit""" + angle = 1.2 + + qscript = QuantumScript([qml.Hadamard(1), qml.CNOT([1, 0]), qml.RX(angle, 2)]) + qc = circuit_to_qiskit(qscript, len(qscript.wires), diagonalize=False, measure=False) + + operation_names = [instruction.operation.name for instruction in qc.data] + operation_params = [instruction.operation.params for instruction in qc.data] + + assert operation_names == ["h", "cx", "rx"] + assert operation_params == [[], [], [angle]] + + @pytest.mark.parametrize("operations", [[], [qml.PauliX(0), qml.PauliY(1)], [qml.Hadamard(0)]]) + @pytest.mark.parametrize("register_size", [2, 5]) + def test_circuit_to_qiskit_register_size(self, operations, register_size): + """Test that the regsiter_size determines the shape of the Qiskit + QuantumCircuit register""" + + qc = circuit_to_qiskit(QuantumScript(operations), register_size) + + # there is a single classical and a single quantum register + assert len(qc.cregs) == len(qc.qregs) == 1 + + # the register contains qubits equal to the register size + assert len(qc.qubits) == register_size + + @pytest.mark.parametrize( + "operations, final_op_name", + [([qml.PauliX(0), qml.PauliY(1)], "y"), ([[qml.CNOT([0, 1]), qml.Hadamard(1)], "h"])], + ) + @pytest.mark.parametrize("measure", [True, False]) + def test_circuit_to_qiskit_measure_kwarg(self, operations, final_op_name, measure): + """Test that measurements are added to the circuit if and only if measure=True""" + + qc = circuit_to_qiskit(QuantumScript(operations), 2, measure=measure) + final_instruction = qc.data[-1] + + if measure: + assert final_instruction.operation.name == "measure" + + @pytest.mark.parametrize("diagonalize", [True, False]) + def test_circuit_to_qiskit_diagonalize_kwarg(self, diagonalize): + """Test that diagonalizing gates are included in the circuit if diagonalize=True""" + + qscript = QuantumScript( + [qml.Hadamard(1), qml.CNOT([1, 0])], measurements=[qml.expval(qml.PauliY(1))] + ) + assert qscript.diagonalizing_gates == [qml.PauliZ(1), qml.S(1), qml.Hadamard(1)] + + qc = circuit_to_qiskit(qscript, 2, diagonalize=diagonalize, measure=True) + + # get list of instruction names up to the barrier (played right before measurements) + instructions = [] + for instruction in qc.data: + if instruction.operation.name == "barrier": + break + instructions.append(instruction.operation.name) + + # check length of instructions matches length of expected gates + expected_gates = qscript.operations + if diagonalize: + expected_gates += qscript.diagonalizing_gates + + assert len(instructions) == len(expected_gates) + + def test_circuit_to_qiskit_measurements_with_overlapping_wires(self): + """Test that diagonalizing gates work for circuits with + measurements on overlapping wires""" + + measurements = [qml.sample(qml.X(0) @ qml.Y(1)), qml.sample(qml.X(0))] + tape = qml.tape.QuantumScript(measurements=measurements) + + qc = circuit_to_qiskit(tape, 2, diagonalize=True, measure=True) + + # get list of instruction names up to the barrier (played right before measurements) + instructions = [] + for instruction in qc.data: + if instruction.operation.name == "barrier": + break + instructions.append(instruction.operation.name) + + # manually diagonalized test case since Qiskit transpiles whatever we had before + # and that results is different from PL's diagonalization + expected_gates = ["ry", "rx"] + + assert len(instructions) == len(expected_gates) + assert instructions == expected_gates + + +class TestConverterGatePennyLaneToQiskit: + def test_non_parameteric_operation_to_qiskit(self): + """Test that a non-parameteric operation is correctly converted to a + Qiskit circuit with a single operation""" + + op = qml.PauliX(0) + + qc = operation_to_qiskit(op, QuantumRegister(1)) + ops = [instruction.operation.name for instruction in qc.data] + qubits = [instruction.qubits for instruction in qc.data][0] + wires = [qc.find_bit(q).index for q in qubits] + + assert ops == ["x"] + assert wires == [0] + + def test_parameteric_operation_to_qiskit(self): + """Test that a parameteric operation is correctly converted to a + Qiskit circuit with a single operation""" + + op = qml.RX(1.23, 2) + + qc = operation_to_qiskit(op, QuantumRegister(3)) + ops = [instruction.operation.name for instruction in qc.data] + qubits = [instruction.qubits for instruction in qc.data][0] + wires = [qc.find_bit(q).index for q in qubits] + params = [instruction.operation.params for instruction in qc.data] + + assert ops == ["rx"] + assert wires == [2] + assert params == [[1.23]] + + # ToDo: add custom wire label support? Or have we already mapped to integers here? Story #55168 + @pytest.mark.parametrize("op_wires", ([0, 1], [2, 4])) + def test_multi_wire_operation_to_qiskit(self, op_wires): + """Test that an operation with multiple wires is correctly converted to a + Qiskit circuit with a single operation""" + + op = qml.CNOT(op_wires) + + qc = operation_to_qiskit(op, QuantumRegister(5)) + ops = [instruction.operation.name for instruction in qc.data] + qubits = [instruction.qubits for instruction in qc.data][0] + qc_wires = [qc.find_bit(q).index for q in qubits] + + assert ops == ["cx"] + assert qc_wires == op_wires + + @pytest.mark.parametrize( + "op", + [ + qml.QubitUnitary( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], wires=[0, 1] + ), + qml.StatePrep(np.array([1, 0, 0, 0]), wires=[0, 1]), + qml.QubitStateVector(np.array([1, 0, 0, 0]), wires=[0, 1]), + ], + ) + def test_state_prep_ops_have_reversed_register(self, op): + """Tests that the wire order is reversed when applying matrix-based operators from PennyLane, + because the Qiskit convention for inferring wire order for matrices is the reverse of the + PennyLane convention""" + + qc = operation_to_qiskit(op, reg=QuantumRegister(3)) + qubits = qc[0].qubits + wires = [qc.find_bit(q).index for q in qubits] + + # wires on the qiskit circuit are the PL wires reversed + assert Wires(wires) == op.wires[::-1] + + def test_with_predefined_creg(self): + """Test that it also works if passing in an already existing classical register""" + + creg = ClassicalRegister(3) + + op = qml.RX(1.23, 2) + + qc1 = operation_to_qiskit(op, QuantumRegister(3), creg=creg) + qc2 = operation_to_qiskit(op, QuantumRegister(3), creg=None) + + ops1 = [instruction.operation.name for instruction in qc1.data] + params1 = [instruction.operation.params for instruction in qc1.data] + ops2 = [instruction.operation.name for instruction in qc2.data] + params2 = [instruction.operation.params for instruction in qc2.data] + + qubits1 = [instruction.qubits for instruction in qc1.data][0] + wires1 = [qc1.find_bit(q).index for q in qubits1] + qubits2 = [instruction.qubits for instruction in qc2.data][0] + wires2 = [qc2.find_bit(q).index for q in qubits2] + + assert ops1 == ops2 == ["rx"] + assert wires1 == wires2 == [2] + assert params1 == params2 == [[1.23]] + + +# pylint:disable=too-few-public-methods +class TestConverterUtilsPennyLaneToQiskit: + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize( + "operator, expected", + [ + (qml.X(0), SparsePauliOp("IIIIX")), + (qml.I(1), SparsePauliOp("IIIII")), + (Y(0), SparsePauliOp("IIIIY")), + (qml.PauliZ(0), SparsePauliOp("IIIIZ")), + ( + X(0) + I(0) + 2 * Y(1) + I(1), + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIII") + + 2 * SparsePauliOp("IIIYI") + + SparsePauliOp("IIIII"), + ), + ( + qml.X(0) + qml.X(0) + qml.Y(1) + qml.Z(2), + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIYI") + + SparsePauliOp("IIZII"), + ), + ( + qml.sum(X(0) + X(0) + Y(1) + Z(2)), + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIIX") + + SparsePauliOp("IIIYI") + + SparsePauliOp("IIZII"), + ), + ( + (qml.X(0) + 2 * qml.Y(1)), + SparsePauliOp("IIIIX") + 2 * SparsePauliOp("IIIYI"), + ), + ( + qml.sum(X(0) + qml.s_prod(2, Y(1))), + SparsePauliOp("IIIIX") + 2 * SparsePauliOp("IIIYI"), + ), + (qml.X(0) + qml.Y(0), SparsePauliOp("IIIIX") + SparsePauliOp("IIIIY")), + ( + 0.5 * X(0) + 3 * (X(2) + qml.PauliY(1)), + 0.5 * SparsePauliOp("IIIIX") + + 3 * (SparsePauliOp("IIXII") + SparsePauliOp("IIIYI")), + ), + ( + 0.5 * X(0) + 0.5 * qml.Y(0) - 1.5 * qml.X(0) - 0.5 * qml.Y(0), + 0.5 * SparsePauliOp("IIIIX") + + 0.5 * SparsePauliOp("IIIIY") + - 1.5 * SparsePauliOp("IIIIX") + - 0.5 * SparsePauliOp("IIIIY"), + ), + ( + qml.ops.LinearCombination( + [1, 3, 4], + [X(3) @ Y(2), Y(4) - X(2), Z(2) * 3], + ) + + qml.X(4), + 1 * SparsePauliOp("IXIII") @ SparsePauliOp("IIYII") + + 3 * (SparsePauliOp("YIIII") - SparsePauliOp("IIXII")) + + 3 * 4 * SparsePauliOp("IIZII") + + SparsePauliOp("XIIII"), + ), + ], + ) + def test_mp_to_pauli_for_general_operator(self, measurement_type, operator, expected): + """Tests that a SparsePauliOp is created given any general operator that has a Pauli representation, and that it has the expected format""" + obs = measurement_type(operator) + register_size = 5 + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op.equiv(expected.simplify()) + + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize( + "operator, expected", + [ + (X(0) @ Y(1), SparsePauliOp("IIX") @ (SparsePauliOp("IYI"))), + ( + (X(0) + Y(1)) @ Y(1), + (SparsePauliOp("IIX") + SparsePauliOp("IYI")) @ (SparsePauliOp("IYI")), + ), + ( + (X(0) + Y(1)) @ (Z(0) + Z(1)), + (SparsePauliOp("IIX") + SparsePauliOp("IYI")) + @ (SparsePauliOp("IIZ") + SparsePauliOp("IZI")), + ), + ( + 2 * (X(0) + Y(1)) @ ((Z(0) + Z(1)) @ Z(2)), + 2 + * (SparsePauliOp("IIX") + SparsePauliOp("IYI")) + @ (SparsePauliOp("IIZ") + SparsePauliOp("IZI")) + @ SparsePauliOp("ZII"), + ), + ( + 0.5 * (X(0) @ X(1)) + 0.7 * (X(1) @ X(2)) + 0.8 * (X(2) @ X(1)), + 0.5 * (SparsePauliOp("IIX") @ SparsePauliOp("IXI")) + + 0.7 * (SparsePauliOp("IXI") @ SparsePauliOp("XII")) + + 0.8 * (SparsePauliOp("XII") @ SparsePauliOp("IXI")), + ), + ], + ) + def test_mp_to_pauli_tensor_products(self, measurement_type, operator, expected): + """Tests that a SparsePauliOp is created given any general operator that has a Pauli representation, and that it is accurate""" + obs = measurement_type(operator) + register_size = 3 + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op.equiv(expected.simplify()) + + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + @pytest.mark.parametrize( + "hamiltonian, expected", + [ + ( + qml.Hamiltonian([1, 2], [qml.X(0), qml.X(1)]), + SparsePauliOp(["IIIIX", "IIIXI"], [1, 2]), + ), + ( + qml.Hamiltonian([3, -2], [qml.X(0), qml.X(0)]), + SparsePauliOp(["IIIIX", "IIIIX"], [3, -2]), + ), + ( + qml.Hamiltonian([-3, 3, 0.5, 5], [qml.X(0), qml.X(0), qml.Z(1), qml.Y(2)]), + SparsePauliOp(["IIIIX", "IIIIX", "IIIZI", "IIYII"], [-3, 3, 0.5, 5]), + ), + ( + qml.Hamiltonian([1], [qml.X(0)]) + 2 * qml.Z(0) @ qml.Z(1), + SparsePauliOp("IIIIX") + 2 * SparsePauliOp("IIIIZ") @ SparsePauliOp("IIIZI"), + ), + ( + qml.Hamiltonian([1], [qml.X(0) @ Y(2)]) - 3 * qml.Z(4) @ qml.Z(1), + (SparsePauliOp("IIIIX") @ SparsePauliOp("IIYII")) + - 3 * SparsePauliOp("ZIIII") @ SparsePauliOp("IIIZI"), + ), + ], + ) + def test_mp_to_pauli_for_hamiltonian(self, measurement_type, hamiltonian, expected): + """Tests that a SparsePauliOp is created from a Hamiltonian, and that + it has the expected format""" + + obs = measurement_type(hamiltonian) + register_size = 5 + + pauli_op = mp_to_pauli(obs, register_size) + assert isinstance(pauli_op, SparsePauliOp) + + pauli_op_list = list(pauli_op.paulis.to_labels()[0]) + # all qubits in register are accounted for + assert len(pauli_op_list) == register_size + assert pauli_op.equiv(expected.simplify()) + + @pytest.mark.parametrize("measurement_type", [qml.expval, qml.var]) + def test_mp_to_pauli_error_for_no_pauli_rep(self, measurement_type): + """Tests that an error is raised when mp_to_pauli is given an operator that does not have a pauli representation""" + + obs = measurement_type(qml.X(0) @ qml.Hadamard(2)) + + assert not obs.obs.pauli_rep + with pytest.raises(ValueError, match="The operator"): + mp_to_pauli(obs, 5) + + +# pylint:disable=not-context-manager class TestControlOpIntegration: """Test the controlled flows integration with PennyLane""" @@ -1861,6 +2234,7 @@ def circuit_native_pennylane(angle): assert np.allclose(qnode(0.543), circuit_native_pennylane(0.543)) + # pylint:disable=unused-variable def test_mid_circuit_as_terminal(self): """Test the control workflows where mid-circuit measurements disguise as terminal ones""" diff --git a/tests/test_ibmq.py b/tests/test_ibmq.py deleted file mode 100644 index 36257b58d..000000000 --- a/tests/test_ibmq.py +++ /dev/null @@ -1,323 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# 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. -r""" -This module contains tests for PennyLane IBMQ devices. -""" -from unittest.mock import patch -from functools import partial - -import numpy as np -import pennylane as qml -import pytest - -from qiskit_ibm_provider import IBMProvider -from qiskit_ibm_provider.exceptions import IBMAccountError -from qiskit_ibm_provider.job import IBMJobError, IBMCircuitJob - -from pennylane_qiskit import IBMQDevice -from pennylane_qiskit import ibmq - -# pylint: disable=protected-access, unused-argument, too-few-public-methods - - -class MockQiskitDeviceInit: - """A mocked version of the QiskitDevice __init__ method which - is called on by the IBMQDevice""" - - # pylint: disable=attribute-defined-outside-init - def mocked_init(self, wires, provider, backend, shots, **kwargs): - """Stores the provider which QiskitDevice.__init__ was - called with.""" - self.provider = provider - - -def test_multi_load_changing_token(monkeypatch): - """Test multiple account loads with changing tokens.""" - with monkeypatch.context() as m: - # unrelated mock - m.setattr(ibmq.QiskitDevice, "__init__", lambda self, *a, **k: None) - - # mock save_account to save the token, saved_accounts lists those tokens - tokens = [] - - def saved_accounts(): - return {f"account-{i}": {"token": t} for i, t in enumerate(tokens)} - - def save_account(token=None, **kwargs): - tokens.append(token) - - m.setattr(IBMProvider, "saved_accounts", saved_accounts) - m.setattr(IBMProvider, "save_account", save_account) - m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) - - m.setenv("IBMQX_TOKEN", "T1") - IBMQDevice(wires=1) - assert tokens == ["T1"] - IBMQDevice(wires=1) - assert tokens == ["T1"] - - m.setenv("IBMQX_TOKEN", "T2") - IBMQDevice(wires=1) - assert tokens == ["T1", "T2"] - - -def test_load_kwargs_takes_precedence(monkeypatch, mocker): - """Test that with a potentially valid token stored as an environment - variable, passing the token as a keyword argument takes precedence.""" - monkeypatch.setenv("IBMQX_TOKEN", "SomePotentiallyValidToken") - mock = mocker.patch("qiskit_ibm_provider.IBMProvider.save_account") - - with monkeypatch.context() as m: - m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) - m.setattr(ibmq.QiskitDevice, "__init__", lambda self, *a, **k: None) - IBMQDevice(wires=1, ibmqx_token="TheTrueToken") - - mock.assert_called_with(token="TheTrueToken", url=None, instance="ibm-q/open/main") - - -def test_custom_provider(monkeypatch): - """Tests that a custom provider can be passed when creating an IBMQ - device.""" - mock_provider = "MockProvider" - mock_qiskit_device = MockQiskitDeviceInit() - monkeypatch.setenv("IBMQX_TOKEN", "1") - - with monkeypatch.context() as m: - m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) - m.setattr(ibmq.QiskitDevice, "__init__", mock_qiskit_device.mocked_init) - m.setattr(IBMProvider, "saved_accounts", lambda: {"my-account": {"token": "1"}}) - IBMQDevice(wires=2, backend="ibmq_qasm_simulator", provider=mock_provider) - - assert mock_qiskit_device.provider == mock_provider - - -def test_default_provider(monkeypatch): - """Tests that the default provider is used when no custom provider was - specified.""" - mock_qiskit_device = MockQiskitDeviceInit() - monkeypatch.setenv("IBMQX_TOKEN", "1") - - def provider_init(self, instance=None): - self.instance = instance - - with monkeypatch.context() as m: - m.setattr(ibmq.QiskitDevice, "__init__", mock_qiskit_device.mocked_init) - m.setattr(IBMProvider, "__init__", provider_init) - m.setattr(IBMProvider, "saved_accounts", lambda: {"my-account": {"token": "1"}}) - IBMQDevice(wires=2, backend="ibmq_qasm_simulator") - - assert isinstance(mock_qiskit_device.provider, IBMProvider) - assert mock_qiskit_device.provider.instance == "ibm-q/open/main" - - -def test_custom_provider_hub_group_project_url(monkeypatch, mocker): - """Tests that the custom arguments passed during device instantiation are - used when calling IBMProvider.save_account""" - monkeypatch.setenv("IBMQX_TOKEN", "1") - mock = mocker.patch("qiskit_ibm_provider.IBMProvider.save_account") - - custom_hub = "SomeHub" - custom_group = "SomeGroup" - custom_project = "SomeProject" - instance = f"{custom_hub}/{custom_group}/{custom_project}" - - with monkeypatch.context() as m: - m.setattr(ibmq.QiskitDevice, "__init__", lambda *a, **k: None) - m.setattr(IBMProvider, "__init__", lambda self, *a, **k: None) - IBMQDevice( - wires=2, - backend="ibmq_qasm_simulator", - hub=custom_hub, - group=custom_group, - project=custom_project, - ibmqx_url="example.com", - ) - - mock.assert_called_with(token="1", url="example.com", instance=instance) - - -@pytest.mark.usefixtures("skip_if_account_saved") -class TestMustNotHaveAccount: - """Tests that require the user _not_ have an IBMQ account loaded.""" - - def test_load_env_empty_string_has_short_error(self, monkeypatch): - """Test that the empty string is treated as a missing token.""" - monkeypatch.setenv("IBMQX_TOKEN", "") - with pytest.raises(IBMAccountError, match="No active IBM Q account"): - IBMQDevice(wires=1) - - def test_account_error(self, monkeypatch): - """Test that an error is raised if there is no active IBMQ account.""" - - # Token is passed such that the test is skipped if no token was provided - with pytest.raises(IBMAccountError, match="No active IBM Q account"): - with monkeypatch.context() as m: - m.delenv("IBMQX_TOKEN", raising=False) - IBMQDevice(wires=1) - - -@pytest.mark.usefixtures("skip_if_no_account") -class TestIBMQWithRealAccount: - """Tests that require an active IBMQ account.""" - - def test_load_from_env_multiple_device(self): - """Test creating multiple IBMQ devices when the environment variable - for the IBMQ token was set.""" - dev1 = IBMQDevice(wires=1) - dev2 = IBMQDevice(wires=1) - assert dev1 is not dev2 - - @pytest.mark.parametrize("shots", [1000]) - def test_simple_circuit(self, tol, shots): - """Test executing a simple circuit submitted to IBMQ.""" - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [1000]) - def test_simple_circuit_with_batch_params(self, tol, shots, mocker): - """Test that executing a simple circuit with batched parameters is - submitted to IBMQ once.""" - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @partial(qml.batch_params, all_operations=True) - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - # Check that we run only once - spy1 = mocker.spy(dev, "batch_execute") - spy2 = mocker.spy(dev.backend, "run") - - # Batch the input parameters - batch_dim = 3 - theta = np.linspace(0, 0.543, batch_dim) - phi = np.linspace(0, 0.123, batch_dim) - - res = circuit(theta, phi) - assert np.allclose(res[0], np.cos(theta), **tol) - assert np.allclose(res[1], np.cos(theta) * np.cos(phi), **tol) - - # Check that IBMQBackend.run was called once - assert spy1.call_count == 1 - assert spy2.call_count == 1 - - @pytest.mark.parametrize("shots", [1000]) - def test_batch_execute_parameter_shift(self, tol, shots, mocker): - """Test that devices provide correct result computing the gradient of a - circuit using the parameter-shift rule and the batch execution pipeline.""" - dev = IBMQDevice(wires=3, backend="ibmq_qasm_simulator", shots=shots) - - spy1 = mocker.spy(dev, "batch_execute") - spy2 = mocker.spy(dev.backend, "run") - - @qml.qnode(dev, diff_method="parameter-shift") - def circuit(x, y): - qml.RX(x, wires=[0]) - qml.RY(y, wires=[1]) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0) @ qml.PauliX(1) @ qml.PauliZ(2)) - - x = qml.numpy.array(0.543, requires_grad=True) - y = qml.numpy.array(0.123, requires_grad=True) - - res = qml.grad(circuit)(x, y) - expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) - assert np.allclose(res, expected, **tol) - - # Check that QiskitDevice.batch_execute was called once - assert spy1.call_count == 2 - - # Check that run was called twice: for the partial derivatives and for - # running the circuit - assert spy2.call_count == 2 - - @pytest.mark.parametrize("shots", [1000]) - def test_probability(self, tol, shots): - """Test that the probs function works.""" - dev = IBMQDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - dev_analytic = qml.device("default.qubit", wires=2, shots=None) - - x = [0.2, 0.5] - - def circuit(x): - qml.RX(x[0], wires=0) - qml.RY(x[1], wires=0) - qml.CNOT(wires=[0, 1]) - return qml.probs(wires=[0, 1]) - - prob = qml.QNode(circuit, dev) - prob_analytic = qml.QNode(circuit, dev_analytic) - - # Calling the hardware only once - hw_prob = prob(x) - - assert np.isclose(hw_prob.sum(), 1, **tol) - assert np.allclose(prob_analytic(x), hw_prob, **tol) - assert not np.array_equal(prob_analytic(x), hw_prob) - - def test_track(self): - """Test that the tracker works.""" - dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) - dev.tracker.active = True - - @qml.qnode(dev) - def circuit(): - qml.PauliX(wires=0) - return qml.probs(wires=0) - - circuit() - - assert "job_time" in dev.tracker.history - assert set(dev.tracker.history["job_time"][0]) == {"queued", "running"} - - @patch( - "qiskit_ibm_provider.job.ibm_circuit_job.IBMCircuitJob.time_per_step", - return_value={"CREATING": "1683149330"}, - ) - @pytest.mark.parametrize("timeout", [None, 120]) - def test_track_fails_with_unexpected_metadata(self, mock_time_per_step, timeout, mocker): - """Tests that the tracker fails when it doesn't get the required metadata.""" - batch_execute_spy = mocker.spy(ibmq.QiskitDevice, "batch_execute") - wait_spy = mocker.spy(IBMCircuitJob, "wait_for_final_state") - - dev = IBMQDevice(wires=1, backend="ibmq_qasm_simulator", shots=1, timeout_secs=timeout) - dev.tracker.active = True - - @qml.qnode(dev) - def circuit(): - qml.PauliX(wires=0) - return qml.probs(wires=0) - - with pytest.raises(IBMJobError, match="time_per_step had keys"): - circuit() - - assert mock_time_per_step.call_count == 2 - batch_execute_spy.assert_called_with(dev, mocker.ANY, timeout=timeout) - wait_spy.assert_called_with(mocker.ANY, timeout=timeout or 60) diff --git a/tests/test_integration.py b/tests/test_integration.py index 39d7f93f3..1e1d3db5d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -23,42 +23,29 @@ import pennylane as qml from pennylane.numpy import tensor -from semantic_version import Version import pytest import qiskit import qiskit_aer from qiskit.providers import QiskitBackendNotFoundError -from pennylane_qiskit.qiskit_device import QiskitDevice +from qiskit.providers.basic_provider import BasicProvider +from pennylane_qiskit.qiskit_device_legacy import QiskitDeviceLegacy # pylint: disable=protected-access, unused-argument, ungrouped-imports, too-many-arguments, too-few-public-methods -if Version(qiskit.__version__) < Version("1.0.0"): - pldevices = [("qiskit.aer", qiskit_aer.Aer), ("qiskit.basicaer", qiskit.BasicAer)] - def check_provider_backend_compatibility(pldevice, backend_name): - """check compatibility of provided backend""" - dev_name, _ = pldevice - if (dev_name == "qiskit.aer" and "aer" not in backend_name) or ( - dev_name == "qiskit.basicaer" and "aer" in backend_name - ): - return (False, "Only the AerSimulator is supported on AerDevice") - return True, None - -else: - from qiskit.providers.basic_provider import BasicProvider +pldevices = [("qiskit.aer", qiskit_aer.Aer), ("qiskit.basicsim", BasicProvider())] - pldevices = [("qiskit.aer", qiskit_aer.Aer), ("qiskit.basicsim", BasicProvider())] - def check_provider_backend_compatibility(pldevice, backend_name): - """check compatibility of provided backend""" - dev_name, _ = pldevice - if dev_name == "qiskit.aer" and backend_name == "basic_simulator": - return (False, "basic_simulator is not supported on the AerDevice") +def check_provider_backend_compatibility(pldevice, backend_name): + """Check the compatibility of provided backend""" + dev_name, _ = pldevice + if dev_name == "qiskit.aer" and backend_name == "basic_simulator": + return (False, "basic_simulator is not supported on the AerDevice") - if dev_name == "qiskit.basicsim" and backend_name != "basic_simulator": - return (False, "Only the basic_simulator backend works with the BasicSimulatorDevice") - return True, None + if dev_name == "qiskit.basicsim" and backend_name != "basic_simulator": + return (False, "Only the basic_simulator backend works with the BasicSimulatorDevice") + return True, None class TestDeviceIntegration: @@ -78,7 +65,6 @@ def test_load_device(self, d, backend): assert dev.shots == 1024 assert dev.short_name == d[0] assert dev.provider == d[1] - assert dev.capabilities()["returns_state"] == (backend in state_backends) @pytest.mark.parametrize("d", pldevices) def test_load_remote_device_with_backend_instance(self, d, backend): @@ -90,32 +76,18 @@ def test_load_remote_device_with_backend_instance(self, d, backend): except QiskitBackendNotFoundError: pytest.skip("Backend is not compatible with specified device") - dev = qml.device("qiskit.remote", wires=2, backend=backend_instance, shots=1024) - assert dev.num_wires == 2 - assert dev.shots == 1024 - assert dev.short_name == "qiskit.remote" - assert dev.provider is None - assert dev.capabilities()["returns_state"] == (backend in state_backends) - - @pytest.mark.parametrize("d", pldevices) - def test_load_remote_device_by_name(self, d, backend): - """Test that the qiskit.remote device loads correctly when passed a provider and a backend - name. This test is equivalent to `test_load_device` but on the qiskit.remote device instead - of specialized devices that expose more configuration options.""" - - # check compatibility between provider and backend, and skip if incompatible - is_compatible, failure_msg = check_provider_backend_compatibility(d, backend) - if not is_compatible: - pytest.skip(failure_msg) - - _, provider = d + if backend_instance.configuration().n_qubits is None: + pytest.skip("No qubits?") - dev = qml.device("qiskit.remote", wires=2, provider=provider, backend=backend, shots=1024) - assert dev.num_wires == 2 - assert dev.shots == 1024 + dev = qml.device( + "qiskit.remote", + wires=backend_instance.configuration().n_qubits, + backend=backend_instance, + shots=1024, + ) + assert dev.num_wires == backend_instance.configuration().n_qubits + assert dev.shots.total_shots == 1024 assert dev.short_name == "qiskit.remote" - assert dev.provider == provider - assert dev.capabilities()["returns_state"] == (backend in state_backends) def test_incorrect_backend(self): """Test that exception is raised if name is incorrect""" @@ -129,12 +101,6 @@ def test_incorrect_backend_wires(self): ): qml.device("qiskit.aer", wires=100, method="statevector") - def test_remote_device_no_provider(self): - """Test that the qiskit.remote device raises a ValueError if passed a backend - by name but no provider to look up the name on.""" - with pytest.raises(ValueError, match=r"Must pass a provider"): - qml.device("qiskit.remote", wires=2, backend="aer_simulator_statevector") - def test_args(self): """Test that the device requires correct arguments""" with pytest.raises(TypeError, match="missing 1 required positional argument"): @@ -591,7 +557,7 @@ def test_one_qubit_circuit_batch_params(self, shots, d, backend, tol, mocker): b = np.linspace(0, 0.123, batch_dim) c = np.linspace(0, 0.987, batch_dim) - spy1 = mocker.spy(QiskitDevice, "batch_execute") + spy1 = mocker.spy(QiskitDeviceLegacy, "batch_execute") spy2 = mocker.spy(dev.backend, "run") @partial(qml.batch_params, all_operations=True) @@ -605,7 +571,7 @@ def circuit(x, y, z): assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - # Check that QiskitDevice.batch_execute was called + # Check that QiskitDeviceLegacy.batch_execute was called assert spy1.call_count == 1 assert spy2.call_count == 1 @@ -625,7 +591,7 @@ def test_batch_execute_parameter_shift(self, shots, d, backend, tol, mocker): dev = qml.device(d[0], wires=3, backend=backend, shots=shots) - spy1 = mocker.spy(QiskitDevice, "batch_execute") + spy1 = mocker.spy(QiskitDeviceLegacy, "batch_execute") spy2 = mocker.spy(dev.backend, "run") @qml.qnode(dev, diff_method="parameter-shift") @@ -642,7 +608,7 @@ def circuit(x, y): expected = np.array([[-np.sin(y) * np.sin(x), np.cos(y) * np.cos(x)]]) assert np.allclose(res, expected, **tol) - # Check that QiskitDevice.batch_execute was called twice + # Check that QiskitDeviceLegacy.batch_execute was called twice assert spy1.call_count == 2 # Check that run was called twice: for the partial derivatives and for diff --git a/tests/test_new_qiskit_temp.py b/tests/test_new_qiskit_temp.py deleted file mode 100644 index 82c090373..000000000 --- a/tests/test_new_qiskit_temp.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# 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. -r""" -This module contains tests for testing backends and providers for PennyLane IBMQ devices. -""" -import pytest -import pennylane as qml -import qiskit - -from semantic_version import Version - -from pennylane_qiskit import BasicSimulatorDevice - -# pylint: disable= unused-argument - - -@pytest.mark.skipif( - Version(qiskit.__version__) < Version("1.0.0"), - reason="versions below 1.0 are compatible with BasicAer", -) -def test_error_is_raised_if_initalizing_basicaer_device(monkeypatch): - """Test that when Qiskit 1.0 is installed, an error is raised if you try - to initialize the 'qiskit.basicaer' device.""" - - # test that the correct error is actually raised in Qiskit 1.0 (rather than fx an import error) - with pytest.raises( - RuntimeError, - match="Qiskit has discontinued the BasicAer device", - ): - qml.device("qiskit.basicaer", wires=2) - - -@pytest.mark.skipif( - Version(qiskit.__version__) >= Version("1.0.0"), - reason="versions 1.0 and above are compatible with BasicSimulator", -) -def test_error_is_raised_if_initalizing_basic_simulator_device(monkeypatch): - """Test that when a version of Qiskit below 1.0 is installed, an error is raised if you try - to initialize the BasicSimulatorDevice.""" - - # test that the correct error is actually raised in Qiskit 1.0 (rather than fx an import error) - with pytest.raises( - RuntimeError, - match="device is not compatible with version of Qiskit prior to 1.0", - ): - BasicSimulatorDevice(wires=2) diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py index 020a7f159..8674301da 100644 --- a/tests/test_qiskit_device.py +++ b/tests/test_qiskit_device.py @@ -15,19 +15,17 @@ This module contains tests qiskit devices for PennyLane IBMQ devices. """ from unittest.mock import Mock -from packaging.version import Version import numpy as np import pytest -import qiskit as qk from qiskit_aer import noise from qiskit.providers import BackendV1, BackendV2 from qiskit_ibm_runtime.fake_provider import FakeManila, FakeManilaV2 import pennylane as qml from pennylane_qiskit import AerDevice -from pennylane_qiskit.qiskit_device import QiskitDevice +from pennylane_qiskit.qiskit_device_legacy import QiskitDeviceLegacy # pylint: disable=protected-access, unused-argument, too-few-public-methods @@ -111,7 +109,7 @@ class TestSupportForV1andV2: ) def test_v1_and_v2_mocked(self, dev_backend): """Test that device initializes with no error mocked""" - dev = qml.device("qiskit.remote", wires=10, backend=dev_backend, use_primitives=True) + dev = qml.device("qiskit.aer", wires=10, backend=dev_backend) assert dev._backend == dev_backend @pytest.mark.parametrize( @@ -123,7 +121,7 @@ def test_v1_and_v2_mocked(self, dev_backend): ) def test_v1_and_v2_manila(self, dev_backend): """Test that device initializes with no error with V1 and V2 backends by Qiskit""" - dev = qml.device("qiskit.remote", wires=5, backend=dev_backend, use_primitives=True) + dev = qml.device("qiskit.aer", wires=5, backend=dev_backend) @qml.qnode(dev) def circuit(x): @@ -185,7 +183,7 @@ def test_warning_raised_for_hardware_backend_analytic_expval(self, recorder): ) # Two warnings are being raised: one about analytic calculations and another about deprecation. - assert len(record) == (2 if Version("1.0") < Version(qk.__version__) else 1) + assert len(record) == 2 @pytest.mark.parametrize("method", ["unitary", "statevector"]) def test_no_warning_raised_for_software_backend_analytic_expval( @@ -199,7 +197,7 @@ def test_no_warning_raised_for_software_backend_analytic_expval( # These simulators are being deprecated. Warning is raised in Qiskit 1.0 # Migrate to AerSimulator with AerSimulator(method=method) and append # run circuits with the `save_state` instruction. - assert len(recwarn) == (1 if Version("1.0") < Version(qk.__version__) else 0) + assert len(recwarn) == 1 class TestAerBackendOptions: @@ -239,12 +237,12 @@ def test_calls_to_execute(self, device, n_tapes, mocker): called and not the general execute method.""" dev = device(2) - spy = mocker.spy(QiskitDevice, "execute") + spy = mocker.spy(QiskitDeviceLegacy, "execute") tapes = [self.tape1] * n_tapes dev.batch_execute(tapes) - # Check that QiskitDevice.execute was not called + # Check that QiskitDeviceLegacyLegacy.execute was not called assert spy.call_count == 0 @pytest.mark.parametrize("n_tapes", [1, 2, 3]) @@ -253,7 +251,7 @@ def test_calls_to_reset(self, n_tapes, mocker, device): times.""" dev = device(2) - spy = mocker.spy(QiskitDevice, "reset") + spy = mocker.spy(QiskitDeviceLegacy, "reset") tapes = [self.tape1] * n_tapes dev.batch_execute(tapes) diff --git a/tests/test_runtime.py b/tests/test_runtime.py deleted file mode 100644 index 23d83d18f..000000000 --- a/tests/test_runtime.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# 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. -r""" -This module contains tests for PennyLane runtime programs. -""" - -from functools import partial -import numpy as np -import pennylane as qml -import pytest - -from pennylane_qiskit import IBMQCircuitRunnerDevice, IBMQSamplerDevice - - -@pytest.mark.usefixtures("skip_if_no_account") -class TestCircuitRunner: - """Test class for the circuit runner IBMQ runtime device.""" - - def test_short_name(self): - """Test that we can call the circuit runner using its shortname.""" - dev = qml.device("qiskit.ibmq.circuit_runner", wires=1) - assert isinstance(dev, IBMQCircuitRunnerDevice) - - @pytest.mark.parametrize("shots", [8000]) - def test_simple_circuit(self, tol, shots): - """Test executing a simple circuit submitted to IBMQ circuit runner runtime program.""" - - dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - @pytest.mark.parametrize( - "kwargs", - [ - { - "layout_method": "trivial", - "routing_method": "basic", - "translation_method": "translator", - "seed_transpiler": 42, - "optimization_level": 2, - "init_qubits": True, - "rep_delay": 0.01, - "transpiler_options": {"approximation_degree": 1.0}, - "measurement_error_mmitigation": True, - } - ], - ) - def test_kwargs_circuit(self, tol, shots, kwargs): - """Test executing a simple circuit submitted to IBMQ circuit runner runtime program with kwargs.""" - - dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots, **kwargs) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - def test_batch_circuits(self, tol, shots): - """Test that we can send batched circuits to the circuit runner runtime program.""" - - dev = IBMQCircuitRunnerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - # Batch the input parameters - batch_dim = 3 - a = np.linspace(0, 0.543, batch_dim) - b = np.linspace(0, 0.123, batch_dim) - c = np.linspace(0, 0.987, batch_dim) - - @partial(qml.batch_params, all_operations=True) - @qml.qnode(dev) - def circuit(x, y, z): - """Reference QNode""" - qml.PauliX(0) - qml.Hadamard(wires=0) - qml.Rot(x, y, z, wires=0) - return qml.expval(qml.PauliZ(0)) - - assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - - def test_track_circuit_runner(self): - """Test that the tracker works.""" - - dev = IBMQCircuitRunnerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) - dev.tracker.active = True - - @qml.qnode(dev) - def circuit(): - qml.PauliX(wires=0) - return qml.probs(wires=0) - - circuit() - - assert "job_time" in dev.tracker.history - if "job_time" in dev.tracker.history: - assert "total_time" in dev.tracker.history["job_time"][0] - assert len(dev.tracker.history["job_time"][0]) == 1 - - -@pytest.mark.usefixtures("skip_if_no_account") -class TestSampler: - """Test class for the sampler IBMQ runtime device.""" - - def test_short_name(self): - dev = qml.device("qiskit.ibmq.sampler", wires=1) - assert isinstance(dev, IBMQSamplerDevice) - - @pytest.mark.parametrize("shots", [8000]) - def test_simple_circuit(self, tol, shots): - """Test executing a simple circuit submitted to IBMQ using the Sampler device.""" - - dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - @pytest.mark.parametrize( - "kwargs", - [ - { - "circuit_indices": [0], - "run_options": {"seed_simulator": 42}, - "skip_transpilation": False, - } - ], - ) - def test_kwargs_circuit(self, tol, shots, kwargs): - """Test executing a simple circuit submitted to IBMQ using the Sampler device with kwargs.""" - - dev = IBMQSamplerDevice(wires=2, backend="ibmq_qasm_simulator", shots=shots, **kwargs) - - @qml.qnode(dev) - def circuit(theta, phi): - qml.RX(theta, wires=0) - qml.RX(phi, wires=1) - qml.CNOT(wires=[0, 1]) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) - - theta = 0.432 - phi = 0.123 - - res = circuit(theta, phi) - expected = np.array([np.cos(theta), np.cos(theta) * np.cos(phi)]) - assert np.allclose(res, expected, **tol) - - @pytest.mark.parametrize("shots", [8000]) - def test_batch_circuits(self, tol, shots): - """Test executing batched circuits submitted to IBMQ using the Sampler device.""" - - dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=shots) - - # Batch the input parameters - batch_dim = 3 - a = np.linspace(0, 0.543, batch_dim) - b = np.linspace(0, 0.123, batch_dim) - c = np.linspace(0, 0.987, batch_dim) - - @partial(qml.batch_params, all_operations=True) - @qml.qnode(dev) - def circuit(x, y, z): - """Reference QNode""" - qml.PauliX(0) - qml.Hadamard(wires=0) - qml.Rot(x, y, z, wires=0) - return qml.expval(qml.PauliZ(0)) - - assert np.allclose(circuit(a, b, c), np.cos(a) * np.sin(b), **tol) - - def test_track_sampler(self): - """Test that the tracker works.""" - - dev = IBMQSamplerDevice(wires=1, backend="ibmq_qasm_simulator", shots=1) - dev.tracker.active = True - - @qml.qnode(dev) - def circuit(): - qml.PauliX(wires=0) - return qml.probs(wires=0) - - circuit() - - assert len(dev.tracker.history) == 2 diff --git a/tests/test_sample.py b/tests/test_sample.py deleted file mode 100644 index c5d620459..000000000 --- a/tests/test_sample.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright 2021-2024 Xanadu Quantum Technologies Inc. - -# 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. -r""" -This module contains tests for sampling from PennyLane IBMQ devices. -""" -import pytest - -import numpy as np -from flaky import flaky -import pennylane as qml - -# pylint: disable=protected-access, unused-argument, too-many-arguments - -np.random.seed(42) - -THETA = np.linspace(0.11, 1, 3) -PHI = np.linspace(0.32, 1, 3) -VARPHI = np.linspace(0.02, 1, 3) - - -@pytest.mark.parametrize("shots", [8192]) -class TestSample: - """Tests for the sample return type""" - - def test_sample_values(self, device, shots, tol): - """Tests if the samples returned by sample have - the correct values - """ - dev = device(1) - par = 1.5708 - - observable = qml.PauliZ(wires=[0]) - - dev.apply( - [ - qml.RX(par, wires=[0]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain 1 and -1 - assert np.allclose(s1**2, 1, **tol) - - @pytest.mark.parametrize("theta", THETA) - def test_sample_values_hermitian(self, theta, device, shots, tol): - """Tests if the samples of a Hermitian observable returned by sample have - the correct values - """ - dev = device(1) - - A = np.array([[1, 2j], [-2j, 0]]) - - observable = qml.Hermitian(A, wires=[0]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain the eigenvalues of - # the hermitian matrix - eigvals = np.linalg.eigvalsh(A) - assert set(np.round(s1, 8)).issubset(set(np.round(eigvals, 8))) - - # the analytic mean is 2*sin(theta)+0.5*cos(theta)+0.5 - assert np.allclose(np.mean(s1), 2 * np.sin(theta) + 0.5 * np.cos(theta) + 0.5, **tol) - - # the analytic variance is 0.25*(sin(theta)-4*cos(theta))^2 - assert np.allclose(np.var(s1), 0.25 * (np.sin(theta) - 4 * np.cos(theta)) ** 2, **tol) - - @pytest.mark.parametrize("theta", THETA) - def test_sample_values_hermitian_multi_qubit(self, theta, device, shots, tol): - """Tests if the samples of a multi-qubit Hermitian observable returned by sample have - the correct values - """ - dev = device(2) - - A = np.array( - [ - [1, 2j, 1 - 2j, 0.5j], - [-2j, 0, 3 + 4j, 1], - [1 + 2j, 3 - 4j, 0.75, 1.5 - 2j], - [-0.5j, 1, 1.5 + 2j, -1], - ] - ) - - observable = qml.Hermitian(A, wires=[0, 1]) - - dev.apply( - [qml.RX(theta, wires=[0]), qml.RY(2 * theta, wires=[1]), qml.CNOT(wires=[0, 1])], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain the eigenvalues of - # the hermitian matrix - eigvals = np.linalg.eigvalsh(A) - assert set(np.round(s1, 8)).issubset(set(np.round(eigvals, 8))) - - # make sure the mean matches the analytic mean - expected = ( - 88 * np.sin(theta) - + 24 * np.sin(2 * theta) - - 40 * np.sin(3 * theta) - + 5 * np.cos(theta) - - 6 * np.cos(2 * theta) - + 27 * np.cos(3 * theta) - + 6 - ) / 32 - assert np.allclose(np.mean(s1), expected, **tol) - - -@pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) -@pytest.mark.parametrize("shots", [8192]) -class TestTensorSample: - """Test tensor expectation values""" - - def test_paulix_pauliy(self, theta, phi, varphi, device, shots, tol): - """Test that a tensor product involving PauliX and PauliY works correctly""" - dev = device(3) - - observable = qml.PauliX(wires=[0]) @ qml.PauliY(wires=[2]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - qml.RX(phi, wires=[1]), - qml.RX(varphi, wires=[2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain 1 and -1 - assert np.allclose(s1**2, 1, **tol) - - mean = np.mean(s1) - expected = np.sin(theta) * np.sin(phi) * np.sin(varphi) - assert np.allclose(mean, expected, **tol) - - var = np.var(s1) - expected = ( - 8 * np.sin(theta) ** 2 * np.cos(2 * varphi) * np.sin(phi) ** 2 - - np.cos(2 * (theta - phi)) - - np.cos(2 * (theta + phi)) - + 2 * np.cos(2 * theta) - + 2 * np.cos(2 * phi) - + 14 - ) / 16 - assert np.allclose(var, expected, **tol) - - def test_pauliz_hadamard_pauliy(self, theta, phi, varphi, device, shots, tol): - """Test that a tensor product involving PauliZ and PauliY and hadamard works correctly""" - dev = device(3) - - observable = qml.PauliZ(wires=[0]) @ qml.Hadamard(wires=[1]) @ qml.PauliY(wires=[2]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - qml.RX(phi, wires=[1]), - qml.RX(varphi, wires=[2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain 1 and -1 - assert np.allclose(s1**2, 1, **tol) - - mean = np.mean(s1) - expected = -(np.cos(varphi) * np.sin(phi) + np.sin(varphi) * np.cos(theta)) / np.sqrt(2) - assert np.allclose(mean, expected, **tol) - - var = np.var(s1) - expected = ( - 3 - + np.cos(2 * phi) * np.cos(varphi) ** 2 - - np.cos(2 * theta) * np.sin(varphi) ** 2 - - 2 * np.cos(theta) * np.sin(phi) * np.sin(2 * varphi) - ) / 4 - assert np.allclose(var, expected, **tol) - - @flaky(max_runs=5, min_passes=3) - def test_hermitian(self, theta, phi, varphi, device, shots, tol): - """Test that a tensor product involving qml.Hermitian works correctly""" - dev = device(3) - - A = np.array( - [ - [-6, 2 + 1j, -3, -5 + 2j], - [2 - 1j, 0, 2 - 1j, -5 + 4j], - [-3, 2 + 1j, 0, -4 + 3j], - [-5 - 2j, -5 - 4j, -4 - 3j, -6], - ] - ) - observable = qml.PauliZ(wires=[0]) @ qml.Hermitian(A, wires=[1, 2]) - - dev.apply( - [ - qml.RX(theta, wires=[0]), - qml.RX(phi, wires=[1]), - qml.RX(varphi, wires=[2]), - qml.CNOT(wires=[0, 1]), - qml.CNOT(wires=[1, 2]), - ], - rotations=[*observable.diagonalizing_gates()], - ) - - dev._samples = dev.generate_samples() - - s1 = dev.sample(observable) - - # s1 should only contain the eigenvalues of - # the hermitian matrix tensor product Z - Z = np.diag([1, -1]) - eigvals = np.linalg.eigvalsh(np.kron(Z, A)) - assert set(np.round(s1, 8)).issubset(set(np.round(eigvals, 8))) - - mean = np.mean(s1) - expected = 0.5 * ( - -6 * np.cos(theta) * (np.cos(varphi) + 1) - - 2 * np.sin(varphi) * (np.cos(theta) + np.sin(phi) - 2 * np.cos(phi)) - + 3 * np.cos(varphi) * np.sin(phi) - + np.sin(phi) - ) - assert np.allclose(mean, expected, **tol) - - var = np.var(s1) - expected = ( - 1057 - - np.cos(2 * phi) - + 12 * (27 + np.cos(2 * phi)) * np.cos(varphi) - - 2 * np.cos(2 * varphi) * np.sin(phi) * (16 * np.cos(phi) + 21 * np.sin(phi)) - + 16 * np.sin(2 * phi) - - 8 * (-17 + np.cos(2 * phi) + 2 * np.sin(2 * phi)) * np.sin(varphi) - - 8 * np.cos(2 * theta) * (3 + 3 * np.cos(varphi) + np.sin(varphi)) ** 2 - - 24 * np.cos(phi) * (np.cos(phi) + 2 * np.sin(phi)) * np.sin(2 * varphi) - - 8 - * np.cos(theta) - * ( - 4 - * np.cos(phi) - * ( - 4 - + 8 * np.cos(varphi) - + np.cos(2 * varphi) - - (1 + 6 * np.cos(varphi)) * np.sin(varphi) - ) - + np.sin(phi) - * ( - 15 - + 8 * np.cos(varphi) - - 11 * np.cos(2 * varphi) - + 42 * np.sin(varphi) - + 3 * np.sin(2 * varphi) - ) - ) - ) / 16 - assert np.allclose(var, expected, **tol)