diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ae14019eb..0de4c4fd1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,10 +3,19 @@ Changelog ========= +Version 21.0 +============ + +* Refactor the transpilation code, make its details private, improve the docs. + `#156 `_ +* By default :func:`.transpile_insert_moves` now keeps any existing MOVE gates in the circuit. + `#156 `_ +* Add the ``delay`` operation. `#156 `_ + Version 20.15 ============= -* Relax version ranges of `numpy`, `packaging`. `#165 `_ +* Relax version ranges of ``numpy``, ``packaging``. `#165 `_ Version 20.14 ============= diff --git a/INTEGRATION_GUIDE.rst b/INTEGRATION_GUIDE.rst index b922cd92f..3d7bd6e25 100644 --- a/INTEGRATION_GUIDE.rst +++ b/INTEGRATION_GUIDE.rst @@ -101,28 +101,28 @@ Currently, there are three mutually exclusive ways of providing an authenticatio token to IQM client: 1. The recommended way is to use `Cortex CLI `_ -to manage the authentication tokens and store them into a file. IQM client can then read -the token from the file and use it for authentication. The file path can be provided to -IQM client in environment variable ``IQM_TOKENS_FILE``. -Alternatively, the tokens file path can be provided as argument ``tokens_file`` to -:class:`.IQMClient` constructor. + to manage the authentication tokens and store them into a file. IQM client can then read + the token from the file and use it for authentication. The file path can be provided to + IQM client in environment variable :envvar:`IQM_TOKENS_FILE`. + Alternatively, the tokens file path can be provided as argument ``tokens_file`` to + :class:`.IQMClient` constructor. 2. It is also possible to use plaintext token obtained from a server dashboard. These -tokens may have longer lifespan than access tokens generated by Cortex CLI, and thus -IQM client won't attempt to refresh them. The generated token can be provided to IQM -client in environment variable ``IQM_TOKEN``. -Alternatively, the token can be provided as argument ``token`` to :class:`.IQMClient` -constructor. + tokens may have longer lifespan than access tokens generated by Cortex CLI, and thus + IQM client won't attempt to refresh them. The generated token can be provided to IQM + client in environment variable :envvar:`IQM_TOKEN`. + Alternatively, the token can be provided as argument ``token`` to :class:`.IQMClient` + constructor. 3. The third way is to provide server URL, username and password for obtaining the -token from an authentication server. IQM client will maintain a login session with -the authentication server and read and refresh the token as needed. The server URL, -username and password can be provided to IQM client in environment variables -``IQM_AUTH_SERVER``, ``IQM_AUTH_USERNAME`` and ``IQM_AUTH_PASSWORD``. -Alternatively, the values can be provided as arguments ``auth_server_url``, -``username`` and ``password`` to :class:`.IQMClient` constructor. -Note, that all the values must be provided as either environment variables or -as constructor arguments, not mixed. + token from an authentication server. IQM client will maintain a login session with + the authentication server and read and refresh the token as needed. The server URL, + username and password can be provided to IQM client in environment variables + :envvar:`IQM_AUTH_SERVER`, :envvar:`IQM_AUTH_USERNAME` and :envvar:`IQM_AUTH_PASSWORD`. + Alternatively, the values can be provided as arguments ``auth_server_url``, + ``username`` and ``password`` to :class:`.IQMClient` constructor. + Note, that all the values must be provided as either environment variables or + as constructor arguments, not mixed. Circuit transpilation --------------------- @@ -131,55 +131,35 @@ IQM does not provide an open source circuit transpilation library, so this will by the quantum computing framework or a third party library. To obtain the necessary information for circuit transpilation, :meth:`.IQMClient.get_dynamic_quantum_architecture` returns the names of the QPU components (qubits and computational resonators), and the native operations available -in the given calibration set. This information should enable circuit transpilation for IQM quantum architectures. - -The notable exception is the transpilation of MOVE gates for IQM quantum computers with -computational resonators, for which some specialized transpilation logic is provided. The MOVE gate -moves the state of a qubit to an empty computational resonator, and vice versa, so that the qubit -can interact with other qubits connected to the resonator. For this, we provide users with two -transpilation functions: :func:`.transpile_insert_moves` and :func:`.transpile_remove_moves`. These -functions can be used to insert or remove MOVE gates from the circuit, respectively. - -:func:`.transpile_insert_moves` is a transpiler pass for inserting MOVE gates into a circuit for -devices with a computational resonator. It assumes that the circuit is already transpiled by third -party software to an architecture where the computational resonator has been abstracted away. To -abstract away the computational resonator, the connectivity graph is modified such that all the -qubits connected to a common resonator are instead connected directly to each other. The function -can take a ``qubit_mapping`` to rename the qubits in the circuit to match the physical qubit names. -Additionally, the function can take the optional argument ``existing_moves`` to specify how this -transpiler pass should handle the case where some MOVE gates are already present in the circuit. The -options are specified by the enum :class:`.ExistingMoveHandlingOptions`. By default the function -warns the user if MOVE gates are already present in the circuit but the ``existing_moves`` argument -is not given, before proceeding to remove the existing MOVE gates and inserting new ones. - -:func:`.transpile_remove_moves` is a helper function for :func:`.transpile_insert_moves` to remove -existing MOVE gates from a quantum circuit. It can be also used standalone to remove the MOVE gates -from an existing circuit such that it can be used on a device without a computational resonator, or -optimized by third party software that does not support the MOVE gate. For example, a user might -want to run a circuit that was originally transpiled for a device with a computational resonator on -a device without a computational resonator. This function allows the user to remove the MOVE gates -from the circuit before transpiling it to another quantum architecture. +in the given calibration set. This information should enable circuit transpilation for the +IQM Crystal quantum architectures. +The notable exception is the transpilation for the IQM Star quantum architectures, which have +computational resonators in addition to qubits. Some specialized transpilation logic involving +the MOVE gates specific to these architectures is provided, in the form of the fuctions +:func:`.transpile_insert_moves` and :func:`.transpile_remove_moves`. +See :mod:`iqm.iqm_client.transpile` for the details. + +A typical Star architecture use case would look something like this: .. code-block:: python - from iqm.iqm_client import Circuit, IQMClient, transpile_insert_moves, transpile_remove_moves + from iqm.iqm_client import Circuit, IQMClient, simplified_architecture, transpile_insert_moves, transpile_remove_moves + + client = IQMClient(URL_TO_STAR_SERVER) + dqa = client.get_dynamic_quantum_architecture() + simplified_dqa = simplified_architecture(dqa) + # circuit valid for simplified_dqa circuit = Circuit(name="quantum_circuit", instructions=[...]) - backend_with_resonator = IQMClient("url_to_backend_with_resonator") - backend_without_resonator = IQMClient("url_to_backend_without_resonator") # intended use - circuit_with_moves = transpile_insert_moves(circuit, backend_with_resonator.get_dynamic_quantum_architecture()) - circuit_without_moves = transpile_remove_moves(circuit_with_moves) + circuit_with_moves = transpile_insert_moves(circuit, dqa) + client.submit_circuits([circuit_with_moves]) - backend_with_resonator.submit_circuits([circuit_with_moves]) - backend_without_resonator.submit_circuits([circuit_without_moves]) - - # Using the transpile_insert_moves on a device that does not support MOVE gates does nothing. - assert circuit == transpile_insert_moves(circuit, backend_without_resonator.get_dynamic_quantum_architecture()) - # Unless the circuit had MOVE gates, then it can remove them with the existing_moves argument. - alt_circuit_without_moves = transpile_insert_moves(circuit, backend_without_resonator.get_dynamic_quantum_architecture(), existing_moves=ExistingMoveHandlingOptions.REMOVE) + # back to simplified dqa + circuit_without_moves = transpile_remove_moves(circuit_with_moves) + assert circuit == circuit_without_moves Note on qubit mapping diff --git a/README.rst b/README.rst index a84efdfa6..f53c448bf 100644 --- a/README.rst +++ b/README.rst @@ -87,4 +87,4 @@ Copyright IQM client is free software, released under the Apache License, version 2.0. -Copyright 2021-2024 IQM client developers. +Copyright 2021-2025 IQM client developers. diff --git a/pyproject.toml b/pyproject.toml index 18201b65f..5e85c0b44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,16 +37,16 @@ Documentation = "https://iqm-finland.github.io/iqm-client" [project.optional-dependencies] testing = [ - "black == 24.3.0", - "isort == 5.12.0", - "pylint == 3.0.2", - "mypy == 1.7.1", + "black == 24.10.0", + "isort == 5.13.2", + "pylint == 3.3.3", + "mypy == 1.14.1", "mockito == 1.4.0", - "pytest == 7.4.3", - "pytest-cov == 4.1.0", - "pytest-isort == 3.1.0", + "pytest == 8.3.4", + "pytest-cov == 6.0.0", + "pytest-isort == 4.0.0", "pytest-pylint == 0.21.0", - "pylint-pydantic == 0.3.0", + "pylint-pydantic == 0.3.5", "types-requests == 2.28.9", "jsons == 1.6.1", "freezegun == 1.5.1", diff --git a/requirements.txt b/requirements.txt index 3016e17f5..f55c5f768 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,38 +7,38 @@ alabaster==0.7.16 \ annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 -astroid==3.0.3 \ - --hash=sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93 \ - --hash=sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17 +astroid==3.3.8 \ + --hash=sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c \ + --hash=sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b babel==2.16.0 \ --hash=sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b \ --hash=sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316 beautifulsoup4==4.12.3 \ --hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \ --hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed -black==24.3.0 \ - --hash=sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f \ - --hash=sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93 \ - --hash=sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11 \ - --hash=sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0 \ - --hash=sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9 \ - --hash=sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5 \ - --hash=sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213 \ - --hash=sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d \ - --hash=sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7 \ - --hash=sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837 \ - --hash=sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f \ - --hash=sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395 \ - --hash=sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995 \ - --hash=sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f \ - --hash=sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597 \ - --hash=sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959 \ - --hash=sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5 \ - --hash=sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb \ - --hash=sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4 \ - --hash=sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7 \ - --hash=sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd \ - --hash=sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7 +black==24.10.0 \ + --hash=sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f \ + --hash=sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd \ + --hash=sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea \ + --hash=sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981 \ + --hash=sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b \ + --hash=sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7 \ + --hash=sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8 \ + --hash=sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175 \ + --hash=sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d \ + --hash=sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392 \ + --hash=sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad \ + --hash=sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f \ + --hash=sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f \ + --hash=sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b \ + --hash=sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875 \ + --hash=sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3 \ + --hash=sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800 \ + --hash=sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65 \ + --hash=sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2 \ + --hash=sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812 \ + --hash=sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50 \ + --hash=sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e build==1.0.3 \ --hash=sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b \ --hash=sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f @@ -222,9 +222,9 @@ imagesize==1.4.1 \ iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 -isort==5.12.0 \ - --hash=sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504 \ - --hash=sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6 +isort==5.13.2 \ + --hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \ + --hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6 jinja2==3.1.5 \ --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb @@ -298,34 +298,45 @@ mccabe==0.7.0 \ mockito==1.4.0 \ --hash=sha256:1719c6bec3523f9b465c86d247bb76027f53ab10f76b2a126dde409d0492fe3e \ --hash=sha256:409ab604c9ebe1bb7dc18ec6b0ed98a8ad5127b08273f5804b22f4d1b51e5222 -mypy==1.7.1 \ - --hash=sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340 \ - --hash=sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49 \ - --hash=sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82 \ - --hash=sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce \ - --hash=sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb \ - --hash=sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51 \ - --hash=sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5 \ - --hash=sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e \ - --hash=sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7 \ - --hash=sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33 \ - --hash=sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9 \ - --hash=sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1 \ - --hash=sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6 \ - --hash=sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a \ - --hash=sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe \ - --hash=sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7 \ - --hash=sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200 \ - --hash=sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7 \ - --hash=sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a \ - --hash=sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28 \ - --hash=sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea \ - --hash=sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120 \ - --hash=sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d \ - --hash=sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42 \ - --hash=sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea \ - --hash=sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2 \ - --hash=sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a +mypy==1.14.1 \ + --hash=sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c \ + --hash=sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd \ + --hash=sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f \ + --hash=sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0 \ + --hash=sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9 \ + --hash=sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b \ + --hash=sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14 \ + --hash=sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35 \ + --hash=sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319 \ + --hash=sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc \ + --hash=sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb \ + --hash=sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb \ + --hash=sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e \ + --hash=sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60 \ + --hash=sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31 \ + --hash=sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f \ + --hash=sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6 \ + --hash=sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107 \ + --hash=sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11 \ + --hash=sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a \ + --hash=sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837 \ + --hash=sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6 \ + --hash=sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b \ + --hash=sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d \ + --hash=sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255 \ + --hash=sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae \ + --hash=sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1 \ + --hash=sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8 \ + --hash=sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b \ + --hash=sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac \ + --hash=sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9 \ + --hash=sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9 \ + --hash=sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1 \ + --hash=sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34 \ + --hash=sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427 \ + --hash=sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1 \ + --hash=sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c \ + --hash=sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89 mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 @@ -513,26 +524,26 @@ pydata-sphinx-theme==0.16.1 \ pygments==2.19.1 \ --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \ --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c -pylint==3.0.2 \ - --hash=sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496 \ - --hash=sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda +pylint==3.3.3 \ + --hash=sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a \ + --hash=sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183 pylint-plugin-utils==0.8.2 \ --hash=sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507 \ --hash=sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4 -pylint-pydantic==0.3.0 \ - --hash=sha256:040603055f45168048fe86e07bb4b6d672ddc931e00a318102f6da42f6c65564 +pylint-pydantic==0.3.5 \ + --hash=sha256:e7a54f09843b000676633ed02d5985a4a61c8da2560a3b0d46082d2ff171c4a1 pyproject-hooks==1.2.0 \ --hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \ --hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 -pytest==7.4.3 \ - --hash=sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac \ - --hash=sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5 -pytest-cov==4.1.0 \ - --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \ - --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a -pytest-isort==3.1.0 \ - --hash=sha256:067801dc5e54a474330d074d521c815948ff6d5cf0ed3b9d057b78216851186c \ - --hash=sha256:13e68d84b35d4f79d20d3d165f491bffc9e4b9509f420381a4186118c4454bd3 +pytest==8.3.4 \ + --hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6 \ + --hash=sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761 +pytest-cov==6.0.0 \ + --hash=sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35 \ + --hash=sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0 +pytest-isort==4.0.0 \ + --hash=sha256:00e99642e282b00b849cf9b49d9102a02ab8c4ec549ace57d7868b723713aaa9 \ + --hash=sha256:14bb3281bab587d6beb53129481e8885232249ec5cfeaf5d903a561ff0589620 pytest-pylint==0.21.0 \ --hash=sha256:88764b8e1d5cfa18809248e0ccc2fc05035f08c35f0b0222ddcfea1c3c4e553e \ --hash=sha256:f10d9eaa72b9fbe624ee4b55da0481f56482eee0a467afc1ee3ae8b1fefbd0b4 diff --git a/src/iqm/iqm_client/__init__.py b/src/iqm/iqm_client/__init__.py index 9f6c189ba..699db67df 100644 --- a/src/iqm/iqm_client/__init__.py +++ b/src/iqm/iqm_client/__init__.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Client-side library for connecting to and executing quantum circuits on IQM quantum computers. -""" +"""Client-side library for connecting to and executing quantum circuits on IQM quantum computers.""" from importlib.metadata import PackageNotFoundError, version import sys import warnings diff --git a/src/iqm/iqm_client/authentication.py b/src/iqm/iqm_client/authentication.py index e972bced3..323712b3f 100644 --- a/src/iqm/iqm_client/authentication.py +++ b/src/iqm/iqm_client/authentication.py @@ -75,6 +75,7 @@ def __init__( username: Optional[str] = None, password: Optional[str] = None, ): + # pylint: disable=too-many-positional-arguments def _format_names(variable_names: list[str]) -> str: """Format a list of variable names""" return ', '.join(f'"{name}"' for name in variable_names) diff --git a/src/iqm/iqm_client/iqm_client.py b/src/iqm/iqm_client/iqm_client.py index efc8575d6..46a06664b 100644 --- a/src/iqm/iqm_client/iqm_client.py +++ b/src/iqm/iqm_client/iqm_client.py @@ -243,7 +243,11 @@ def create_run_request( # validate the circuit against the calibration-dependent dynamic quantum architecture self._validate_circuit_instructions( - architecture, circuits, qubit_mapping, validate_moves=options.move_gate_validation + architecture, + circuits, + qubit_mapping, + validate_moves=options.move_gate_validation, + must_close_sandwiches=False, ) return RunRequest( qubit_mapping=serialized_qubit_mapping, @@ -356,6 +360,8 @@ def _validate_circuit_instructions( circuits: CircuitBatch, qubit_mapping: Optional[dict[str, str]] = None, validate_moves: MoveGateValidationMode = MoveGateValidationMode.STRICT, + *, + must_close_sandwiches: bool = True, ) -> None: """Validate the given circuits against the given quantum architecture. @@ -366,6 +372,7 @@ def _validate_circuit_instructions( Can be set to ``None`` if all ``circuits`` already use physical qubit names. Note that the ``qubit_mapping`` is used for all ``circuits``. validate_moves: determines how MOVE gate validation works + must_close_sandwiches: Iff True, MOVE sandwiches cannot be left open when the circuit ends. Raises: CircuitValidationError: validation failed @@ -380,7 +387,13 @@ def _validate_circuit_instructions( if key in measurement_keys: raise CircuitValidationError(f'Circuit {index}: {instr!r} has a non-unique measurement key.') measurement_keys.add(key) - IQMClient._validate_circuit_moves(architecture, circuit, qubit_mapping, validate_moves=validate_moves) + IQMClient._validate_circuit_moves( + architecture, + circuit, + qubit_mapping, + validate_moves=validate_moves, + must_close_sandwiches=must_close_sandwiches, + ) @staticmethod def _validate_instruction( @@ -461,7 +474,7 @@ def check_locus_components(allowed_components: Iterable[str], msg: str) -> None: raise CircuitValidationError( f"{instruction.qubits} = {tuple(mapped_qubits)} is not allowed as locus for '{instruction_name}'" if qubit_mapping - else f"'{instruction.qubits} is not allowed as locus for '{instruction_name}'" + else f"{instruction.qubits} is not allowed as locus for '{instruction_name}'" ) @staticmethod @@ -470,6 +483,8 @@ def _validate_circuit_moves( circuit: Circuit, qubit_mapping: Optional[dict[str, str]] = None, validate_moves: MoveGateValidationMode = MoveGateValidationMode.STRICT, + *, + must_close_sandwiches: bool = True, ) -> None: """Raises an error if the MOVE gates in the circuit are not valid in the given architecture. @@ -479,6 +494,7 @@ def _validate_circuit_moves( qubit_mapping: Mapping of logical qubit names to physical qubit names. Can be set to ``None`` if the ``circuit`` already uses physical qubit names. validate_moves: Option for bypassing full or partial MOVE gate validation. + must_close_sandwiches: Iff True, MOVE sandwiches cannot be left open when the circuit ends. Raises: CircuitValidationError: validation failed """ @@ -543,8 +559,8 @@ def _validate_circuit_moves( f'are in a resonator. Current resonator occupation: {resonator_occupations}.' ) - # Finally validate that all moves have been ended before the circuit ends - if resonator_occupations: + # Finally validate that all MOVE sandwiches have been ended before the circuit ends + if must_close_sandwiches and resonator_occupations: raise CircuitValidationError( f'Circuit ends while qubit state(s) are still in a resonator: {resonator_occupations}.' ) diff --git a/src/iqm/iqm_client/models.py b/src/iqm/iqm_client/models.py index b91edc932..3e6f3d86f 100644 --- a/src/iqm/iqm_client/models.py +++ b/src/iqm/iqm_client/models.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-lines """This module contains the data models used by IQMClient.""" +# pylint: disable=too-many-lines, no-member +# no-member: see https://github.com/pylint-dev/pylint/issues/8759 from __future__ import annotations @@ -20,7 +21,7 @@ from enum import Enum from functools import cached_property import re -from typing import Any, Final, Optional, Union +from typing import Any, Optional, Union from uuid import UUID from pydantic import BaseModel, Field, StrictStr, field_validator @@ -60,6 +61,7 @@ class NativeOperation: op.name: op for op in [ NativeOperation('barrier', 0, symmetric=True, no_calibration_needed=True), + NativeOperation('delay', 0, {'duration': (float,)}, symmetric=True, no_calibration_needed=True), NativeOperation('measure', 0, {'key': (str,)}, {'feedback_key': (str,)}, factorizable=True), NativeOperation( 'prx', @@ -224,6 +226,38 @@ class Instruction(BaseModel): One-qubit barriers will not have any effect on circuit's compilation and execution. Higher layers that sit on top of IQM Client can make actual use of one-qubit barriers (e.g. during circuit optimization), therefore having them is allowed. + + Delay + ----- + + Forces a delay between the preceding and following circuit operations. + It can be applied to any number of qubits. Takes one argument, ``duration``, which is the minimum + duration of the delay in seconds. It will be rounded up to the nearest possible duration the + hardware can handle. + + .. code-block:: python + :caption: Example + + Instruction(name='delay', qubits=('alice', 'bob'), args={'duration': 80e-9}) + + + .. note:: + + We can only guarantee that the delay is *at least* of the requested duration, due to both + hardware and practical constraints, but could be much more depending on the other operations + in the circuit. To see why, consider e.g. the circuit + + .. code-block:: python + + [ + Instruction(name='cz', qubits=('alice', 'bob'), args={}), + Instruction(name='delay', qubits=('alice',), args={'duration': 1e-9}), + Instruction(name='delay', qubits=('bob',), args={'duration': 100e-9}), + Instruction(name='cz', qubits=('alice', 'bob'), args={}), + ] + + In this case the actual delay between the two CZ gates will be 100 ns rounded up to + hardware granularity, even though only 1 ns was requested for `alice`. """ name: str = Field(..., examples=['measure']) @@ -591,7 +625,7 @@ class DynamicQuantumArchitecture(BaseModel): @cached_property def components(self) -> tuple[str, ...]: - """Returns all locus components (qubits and computational resonators) sorted. + """All locus components (qubits and computational resonators) sorted. The components are first sorted alphabetically based on their non-numeric part, and then components with the same non-numeric part are sorted numerically. An example of components @@ -618,33 +652,33 @@ class HeraldingMode(str, Enum): class MoveGateValidationMode(str, Enum): """MOVE gate validation mode for circuit compilation. This options is meant for advanced users.""" - STRICT: Final[str] = 'strict' + STRICT = 'strict' """Perform standard MOVE gate validation: MOVE gates must only appear in sandwiches, with no gates acting on the MOVE qubit inside the sandwich.""" - ALLOW_PRX: Final[str] = 'allow_prx' + ALLOW_PRX = 'allow_prx' """Allow PRX gates on the MOVE qubit inside MOVE sandwiches during validation.""" - NONE: Final[str] = 'none' + NONE = 'none' """Do not perform any MOVE gate validation.""" class MoveGateFrameTrackingMode(str, Enum): """MOVE gate frame tracking mode for circuit compilation. This option is meant for advanced users.""" - FULL: Final[str] = 'full' + FULL = 'full' """Perform complete MOVE gate frame tracking.""" - NO_DETUNING_CORRECTION: Final[str] = 'no_detuning_correction' + NO_DETUNING_CORRECTION = 'no_detuning_correction' """Do not add the phase detuning corrections to the pulse schedule for the MOVE gate. The user is expected to do these manually.""" - NONE: Final[str] = 'none' + NONE = 'none' """Do not perform any MOVE gate frame tracking. The user is expected to do these manually.""" class DDMode(str, Enum): """Dynamical Decoupling (DD) mode for circuit execution.""" - DISABLED: Final[str] = 'disabled' + DISABLED = 'disabled' """Do not apply dynamical decoupling.""" - ENABLED: Final[str] = 'enabled' + ENABLED = 'enabled' """Apply dynamical decoupling.""" diff --git a/src/iqm/iqm_client/transpile.py b/src/iqm/iqm_client/transpile.py index 137eeeac8..8c5bcd004 100644 --- a/src/iqm/iqm_client/transpile.py +++ b/src/iqm/iqm_client/transpile.py @@ -1,4 +1,4 @@ -# Copyright 2021-2024 IQM client developers +# Copyright 2021-2025 IQM client developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,12 +11,40 @@ # 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. +"""Transpiling circuits to IQM devices involving computational resonators. + +In the IQM Star architecture, computational resonators are connected to multiple qubits. +A MOVE gate can be used to move the state of a qubit to a connected, empty computational resonator, +and back *to the same qubit*. Additionally, two-qubit gates like CZ can be applied +between a qubit and a connected resonator, so that effectively the qubit can be made to interact +with other qubits connected to the resonator. However, the resonators cannot be measured, +and no single-qubit gates can be applied on them. + +To enable third-party transpilers to work on the IQM Star architecture, we may abstract away the +resonators and replace the real dynamic quantum architecture with a *simplified architecture*. +Specifically, this happens by removing the resonators from the architecture, and for +each resonator ``r``, and for each pair of supported native qubit-resonator gates ``(G(q1, r), MOVE(q2, r))`` +adding the fictional gate ``G(q1, q2)`` to the simplified architecture (since the latter can be +implemented as the sequence ``MOVE(q2, r), G(q1, r), MOVE(q2, r)``). + +Before a circuit transpiled to a simplified architecture can be executed it must be further +transpiled to the real Star architecture using :func:`transpile_insert_moves`, which will introduce +the resonators, add MOVE gates as necessary to move the states, and convert the fictional two-qubit +gates into real native gates acting on qubit-resonator pairs. + +Likewise :func:`transpile_remove_moves` can be used to perform the opposite transformation, +converting a circuit valid for the real Star architecture into an equivalent circuit for the +corresponding simplified architecture, e.g. so that the circuit can be retranspiled or optimized +using third-party tools that do not support the MOVE gate. + +Given a :class:`DynamicQuantumArchitecture` for a Star architecture, the corresponding simplified +version can be obtained using :func:`simplify_architecture`. """ -Collection of transpilation functions needed for transpiling to specific devices. -""" +from __future__ import annotations + +from collections.abc import Collection, Iterable from enum import Enum -from typing import Any, Iterable, Optional -import warnings +from typing import Optional from iqm.iqm_client import ( Circuit, @@ -26,89 +54,171 @@ Instruction, IQMClient, ) +from iqm.iqm_client.models import GateImplementationInfo, GateInfo, Locus, _op_is_symmetric class ExistingMoveHandlingOptions(str, Enum): - """Transpile options for handling of existing MOVE instructions.""" + """Options for how :func:`transpile_insert_moves` should handle existing MOVE instructions + in the circuit.""" KEEP = 'keep' - """Transpiler will keep the MOVE instructions as specified checking if they are correct, adding more as needed.""" - REMOVE = 'remove' - """Transpiler will remove the instructions and add new ones.""" + """Strict mode. The circuit, including existing MOVE instructions in it, is validated first. + Then, any fictional two-qubit gates in the circuit are implemented with qubit-resonator gates.""" TRUST = 'trust' - """Transpiler will keep the MOVE instructions without checking if they are correct, and add more as needed.""" + """Lenient mode. Same as KEEP, but does not validate the circuit first. + Will attempt to fix any apparent user errors in the circuit by adding extra MOVE gates. + """ + REMOVE = 'remove' + """Removes existing MOVE instructions from the circuit using :func:`transpile_remove_moves`, and + then does the same as TRUST. This may produce a more optimized end result.""" -class ResonatorStateTracker: - r"""Class for tracking the location of the :math:`|0\rangle` state of the resonators on the - quantum computer as they are moved with the MOVE gates because the MOVE gate is not defined - when acting on a :math:`|11\rangle` state. This is equivalent to tracking - which qubit state has been moved into which resonator. +def _map_loci( + instructions: Iterable[Instruction], + qubit_mapping: dict[str, str], + inverse: bool = False, +) -> list[Instruction]: + """Map the loci of the given instructions using the given qubit mapping, or its inverse. Args: - available_moves: A dictionary describing between which qubits a MOVE gate is - available, for each resonator, i.e. ``available_moves[resonator] = [qubit]`` + instructions: Instructions whose loci are to be mapped. + qubit_mapping: Mapping from one set of qubit names to another. Assumed to be injective. + inverse: Invert ``qubit_mapping`` before using it. + Returns: + Copies of ``instructions`` with mapped loci. """ + if inverse: + qubit_mapping = {phys: log for log, phys in qubit_mapping.items()} + return list( + inst.model_copy(update={'qubits': tuple(qubit_mapping[q] for q in inst.qubits)}) for inst in instructions + ) - move_gate = 'move' - def __init__(self, available_moves: dict[str, list[str]]) -> None: - self.available_moves = available_moves - self.res_qb_map = {r: r for r in self.resonators} +class _ResonatorStateTracker: + r"""Tracks the qubit states stored in computational resonators on a Star architecture QPU + as they are moved with MOVE gates. - @staticmethod - def from_dynamic_architecture(arch: DynamicQuantumArchitecture) -> 'ResonatorStateTracker': - """Constructor to make the ResonatorStateTracker from a DynamicQuantumArchitecture. + Since the MOVE gate is not defined when acting on a :math:`|11\rangle` state, + and involves an unknown phase, it is not equivalent to a SWAP. + The state must always be moved back to its original qubit (this reverses the unknown phase), + and no gates may be applied on the qubit while its state is in a resonator (to avoid populating + the :math:`|11\rangle` state). - Args: - arch: The architecture to track the resonator state on. - """ - available_moves: dict[str, list[str]] = { - r: [q for q, r2 in arch.gates[ResonatorStateTracker.move_gate].loci if r == r2] - for r in arch.computational_resonators - } - return ResonatorStateTracker(available_moves) + Also contains information on available qubit-resonator gate loci. - @staticmethod - def from_circuit(circuit: Circuit) -> 'ResonatorStateTracker': - """Constructor to make the ResonatorStateTracker from a circuit. + Args: + qr_gates: Mapping from qubit-resonator gate name to mapping from resonator to qubits with + which it has the gate available. + """ - Infers the resonator connectivity from the MOVE gates in the circuit. + move_gate = 'move' + """Name of the MOVE gate in the architecture.""" + qr_gate_names = frozenset(('move', 'cz')) + """Names of all arity-2 gates that *can* (in principle) be applied on a (qubit, resonator) locus + in the real Star architecture. They *may* also have (qubit, qubit) loci available if the architecture + allows it. + Other arity-2 gates in the real architecture are assumed to *require* a (qubit, qubit) locus.""" + + def __init__(self, qr_gates: dict[str, dict[str, set[str]]]) -> None: + # NOTE: For now the transpilation logic assumes all qr gates other than MOVE are symmetric + # in their effect, though not in implementation. + for op in self.qr_gate_names: + if op != self.move_gate and not _op_is_symmetric(op): + raise ValueError(f'QR gate {op} is not symmetric.') + + def invert_locus_mapping(mapping: dict[str, set[str]]) -> dict[str, set[str]]: + """Invert the give mapping of resonators to a list of connected qubits, returning + a mapping of qubits to connected resonators.""" + inverse: dict[str, set[str]] = {} + for r, qubits in mapping.items(): + for q in qubits: + inverse.setdefault(q, set()).add(r) + return inverse + + self.move_r2q = qr_gates.pop(self.move_gate, {}) + """Mapping from resonator to qubits whose state can be MOVEd into it.""" + self.move_q2r = invert_locus_mapping(self.move_r2q) + """Mapping from qubit to resonators it can be MOVEd into.""" + self.qr_gates_r2q = qr_gates + """Mapping from QR gate name to mapping from resonator to qubits with which it has the gate available.""" + self.qr_gates_q2r = {gate_name: invert_locus_mapping(mapping) for gate_name, mapping in qr_gates.items()} + """Mapping from QR gate name to mapping from qubit to resonators with which it has the gate available.""" + + self.res_state_owner = {r: r for r in self.resonators} + """Maps resonator to the QPU component whose state it currently holds.""" + + @classmethod + def from_dynamic_architecture(cls, arch: DynamicQuantumArchitecture) -> _ResonatorStateTracker: + """Constructor to make a _ResonatorStateTracker from a dynamic quantum architecture. Args: - circuit: The circuit to track the resonator state on. + arch: Architecture that determines the available gate loci. + Returns: + Tracker for ``arch``. """ - return ResonatorStateTracker.from_instructions(circuit.instructions) + resonators = set(arch.computational_resonators) + qr_gates: dict[str, dict[str, set[str]]] = {} + for gate_name in cls.qr_gate_names: + if gate_info := arch.gates.get(gate_name): + qr_loci: dict[str, set[str]] = {} + for q, r in gate_info.loci: + if gate_name == cls.move_gate: + # MOVE must have a (q, r) locus + if q in resonators or r not in resonators: + raise ValueError(f'MOVE gate locus {q, r} is not of the form (qubit, resonator)') + elif q in resonators: + # Other QR gates + raise ValueError(f'Gate {gate_name} locus {q, r} is not of the form (qubit, *)') + qr_loci.setdefault(r, set()).add(q) + qr_gates[gate_name] = qr_loci + return cls(qr_gates) + + @classmethod + def from_circuit(cls, circuit: Circuit) -> _ResonatorStateTracker: + """Constructor to make the _ResonatorStateTracker from a circuit. + + Infers the resonator connectivity and gate loci from the given the circuit. - @staticmethod - def from_instructions(instructions: Iterable[Instruction]) -> 'ResonatorStateTracker': - """Constructor to make the ResonatorStateTracker from a sequence of instructions. + Args: + circuit: The circuit to track the qubit states on. + Returns: + Tracker for the architecture inferred from ``circuit``. + """ + return cls.from_instructions(circuit.instructions) + + @classmethod + def from_instructions(cls, instructions: Iterable[Instruction]) -> _ResonatorStateTracker: + """Constructor to make the _ResonatorStateTracker from a sequence of instructions. Infers the resonator connectivity from the MOVE gates. Args: - instructions: The instructions to track the resonator state on. + instructions: The instructions to track the qubit states on. + Returns: + Tracker for the architecture inferred from the given instructions. """ - available_moves: dict[str, list[str]] = {} + qr_gates: dict[str, dict[str, set[str]]] = {} for i in instructions: - if i.name == ResonatorStateTracker.move_gate: + if i.name in cls.qr_gate_names: q, r = i.qubits - available_moves.setdefault(r, []).append(q) - return ResonatorStateTracker(available_moves) + qr_gates.setdefault(i.name, {}).setdefault(r, set()).add(q) + return cls(qr_gates) @property - def resonators(self) -> Iterable[str]: - """Getter for the resonator registers that are being tracked.""" - return self.available_moves + def resonators(self) -> Collection[str]: + """Computational resonators that are being tracked.""" + return self.move_r2q.keys() @property - def supports_move(self) -> bool: - """Bool whether any MOVE gate is allowed.""" - return bool(self.available_moves) + def qubit_state_holder(self) -> dict[str, str]: + """Qubits not found in the dict hold their own states.""" + # TODO reverses res_state_owner, maybe this is the one we need to track instead? + resonators = self.resonators + # qubits whose states are in resonators + return {c: r for r, c in self.res_state_owner.items() if c not in resonators} def apply_move(self, qubit: str, resonator: str) -> None: - """Apply the logical changes of the resonator state location when a MOVE gate between qubit and resonator is - applied. + """Record changes to qubit state location when a MOVE gate is applied. Args: qubit: The moved qubit. @@ -116,316 +226,471 @@ def apply_move(self, qubit: str, resonator: str) -> None: Raises: CircuitTranspilationError: MOVE is not allowed, either because the resonator does not exist, - the MOVE gate is not available between this qubit-resonator pair, or the resonator state - is currently in a different qubit register. + the MOVE gate is not available between this qubit-resonator pair, or the resonator is + currently holding the state of a different qubit. """ if ( resonator in self.resonators - and qubit in self.available_moves[resonator] - and self.res_qb_map[resonator] in [qubit, resonator] + and qubit in self.move_r2q[resonator] + and (owner := self.res_state_owner[resonator]) in [qubit, resonator] ): - self.res_qb_map[resonator] = qubit if self.res_qb_map[resonator] == resonator else resonator + self.res_state_owner[resonator] = qubit if owner == resonator else resonator else: - raise CircuitTranspilationError('Attempted move is not allowed.') + raise CircuitTranspilationError(f'MOVE locus {qubit, resonator} is not allowed.') def create_move_instructions( self, qubit: str, resonator: str, - apply_move: Optional[bool] = True, - alt_qubit_names: Optional[dict[str, str]] = None, ) -> Iterable[Instruction]: - """Create the MOVE instructions needed to move the given resonator state into the resonator if needed and then - move resonator state to the given qubit. + """MOVE instruction(s) to move the state of the given qubit into the given resonator, + or back to the qubit. + + If the resonator has the state of another qubit in it, or if qubit's state is in another resonator, + restore them first using additional MOVE instructions. + + Applies the returned MOVE instructions on the tracker state. Args: qubit: The qubit resonator: The resonator - apply_move: Whether the moves should be applied to the resonator tracking state. - alt_qubit_names: Mapping of logical qubit names to physical qubit names. Yields: - The one or two MOVE instructions needed. + 1, 2 or 3 MOVE instructions. """ - if self.res_qb_map[resonator] not in [qubit, resonator]: - other = self.res_qb_map[resonator] - if apply_move: - self.apply_move(other, resonator) - qbs = tuple(alt_qubit_names[q] if alt_qubit_names else q for q in [other, resonator]) - yield Instruction(name=self.move_gate, qubits=qbs, args={}) - if apply_move: - self.apply_move(qubit, resonator) - qbs = tuple(alt_qubit_names[q] if alt_qubit_names else q for q in [qubit, resonator]) - yield Instruction(name=self.move_gate, qubits=qbs, args={}) + # if the resonator has another qubit's state in it, restore it + owner = self.res_state_owner[resonator] + if owner not in [qubit, resonator]: + locus = (owner, resonator) + self.apply_move(*locus) + yield Instruction(name=self.move_gate, qubits=locus, args={}) + + # if the qubit does not hold its own state, restore it, unless it's in the resonator + # find where the qubit state is (it can be in at most one resonator) + res = [r for r, q in self.res_state_owner.items() if q == qubit] # TODO not efficient + if res and (holder := res[0]) != resonator: + locus = (qubit, holder) + self.apply_move(*locus) + yield Instruction(name=self.move_gate, qubits=locus, args={}) + + # move qubit state to resonator, or back to the qubit + locus = (qubit, resonator) + self.apply_move(*locus) + yield Instruction(name=self.move_gate, qubits=locus, args={}) def reset_as_move_instructions( self, resonators: Optional[Iterable[str]] = None, - apply_move: Optional[bool] = True, - alt_qubit_names: Optional[dict[str, str]] = None, ) -> list[Instruction]: - """Creates the MOVE instructions needed to move all resonator states to their original state. + """MOVE instructions that move the states held in the given resonators back to their qubits. + + Applies the returned MOVE instructions on the tracker state. Args: - resonators: The set of resonators to reset, if None, all resonators will be reset. - apply_move: Whether the moves should be applied to the resonator tracking state. + resonators: Resonators that (may) hold qubit states that should be moved back to the qubits. + If ``None``, the states in all known resonators will be returned to the qubits. Returns: - The instructions needed to move all qubit states out of the resonators. + MOVE instructions needed to move the states out of ``resonators`` into + the qubits they belong to. """ if resonators is None: resonators = self.resonators + instructions: list[Instruction] = [] - for r, q in [(r, q) for r, q in self.res_qb_map.items() if r != q and r in resonators]: - instructions += self.create_move_instructions(q, r, apply_move, alt_qubit_names) + for r in resonators: + q = self.res_state_owner[r] + # if the state in r does not belong to r, restore it to its owner + if q != r: + locus = (q, r) + instructions.append(Instruction(name=self.move_gate, qubits=locus, args={})) + self.apply_move(*locus) return instructions - def available_resonators_to_move(self, qubits: Iterable[str]) -> dict[str, list[str]]: - """Generates a dictionary with which resonators a qubit can be moved to, for each qubit. + def resonators_holding_qubits(self, qubits: Iterable[str]) -> list[str]: + """Return the resonators that are holding the state of one of the given qubits. Args: - qubits: The qubits to check which MOVE gates are available. + qubits: The qubits to check. Returns: - The dict that maps each qubit to a list of resonators. + Resonators that hold the state of one of ``qubits``. """ - return {q: [r for r in self.resonators if q in self.available_moves[r]] for q in qubits} + # a resonator can only hold the state of a connected qubit, or its own state + # TODO needs to be made more efficient once we have lots of resonators + return [r for r, q in self.res_state_owner.items() if q != r and q in qubits] - def resonators_holding_qubits(self, qubits: Iterable[str]) -> list[str]: - """Returns the resonators that are currently holding one of the given qubit states. + def map_resonators_in_locus(self, locus: Iterable[str]) -> Locus: + """Map any resonators in the given instruction locus into the QPU components whose state is + currently stored in that resonator. + + If the resonator contains no qubit state at the moment, it is not changed. + Non-resonator components in the locus are also unchanged. Args: - qubits: The qubits + locus: Instruction locus to map. Returns: - The resonators + The mapped locus. """ - return [r for r, q in self.res_qb_map.items() if q in qubits and q not in self.resonators] + return tuple(self.res_state_owner.get(q, q) for q in locus) + + def find_best_sequence( + self, locus: Locus, gate_q2r: dict, lookahead: list[Instruction] + ) -> tuple[str, str, str, float]: + """Find the best way to implement a fictional qubit-qubit gate G(g, m) + using the available native qubit-resonator gates. + + G(g, m) is implemented as G(g, r) for some resonator r, with additional MOVE gates + applied first to make sure the state of the qubit m is in r, and g is holding its own state. - def choose_move_pair( - self, qubits: list[str], remaining_instructions: list[list[str]] - ) -> list[tuple[str, str, list[list[str]]]]: - """Chooses which qubit of the given qubits to move into which resonator, given a sequence of instructions to be - executed later for looking ahead. + Does not change the internal state of the tracker. Args: - qubits: The qubits to choose from - remaining_instructions: The instructions to use for the look-ahead. + locus: Two-qubit locus (g, m), where the state of the second qubit will be MOVEd. + gate_q2r: Mapping from qubit q to resonators r between which there are G(q, r) available. + lookahead: upcoming instructions - Raises: - CircuitTranspilationError: When no move pair is available, most likely because the circuit was not routed. + Returns: + Best gate sequence implementing G(g, m) on the given locus, as the tuple + (g, m, r, badness). Badness is the number of gates in the sequence. + Iff no sequence could be found, r is an empty string. + """ + # pylint: disable=unused-argument + # TODO use the lookahead to add fractional badness + # We could sequence n ops of lookahead using recursion and use sequence len as badness, + # but that would scale exponentially in n. + g, m = locus + # Resonators r for which we have MOVE(m, r) and G(g, r) available. + resonators = gate_q2r.get(g, set()) & self.move_q2r.get(m, set()) + options = [] + for r in resonators: + badness = 1.0 # number of instructions we need to implement G via this resonator + g_holder = self.qubit_state_holder.get(g, g) + if g_holder != g: + badness += 1 # need to move the state back to g + + m_holder = self.qubit_state_holder.get(m, m) + if m_holder == r: + pass # already where it needs to be + else: + if self.res_state_owner[r] != r: + badness += 1 # resonator has some other qubit's state in it, and must be reset + if m_holder == m: + badness += 1 # need to move the state to r + else: + badness += 2 # in another resonator, need 2 moves to get it to r + options.append((g, m, r, badness)) + + # return the best option + return min(options, default=(g, m, '', 1000), key=lambda x: x[-1]) + + def get_sequence(self, g: str, m: str, r: str, inst: Instruction) -> list[Instruction]: + """Apply a fictional two-qubit gate G(g, m) using native qubit-resonator gates. + + G(g, m) is implemented as G(g, r), with additional MOVE gates + applied first to make sure the state of the qubit m is in r, and g is holding its own state. + Args: + g: first qubit + m: moved qubit + r: resonator + inst: G as an instruction Returns: - A sorted preference list of resonator and qubit chosen to apply the move on. + sequence of real qubit-resonator gates implementing G(g, m) """ - r_candidates = [ - (r, q, remaining_instructions) for q, rs in self.available_resonators_to_move(qubits).items() for r in rs - ] - if len(r_candidates) == 0: - raise CircuitTranspilationError( - f'Unable to insert MOVE gates because none of the qubits {qubits} share a resonator. ' - + 'This can be resolved by routing the circuit first without resonators.' - ) - resonator_candidates = list(sorted(r_candidates, key=self._score_choice_heuristic, reverse=True)) - return resonator_candidates + seq: list[Instruction] = [] + # does m state need to be moved to the resonator? + m_holder = self.qubit_state_holder.get(m, m) + if m_holder != r: + seq += self.create_move_instructions(m, r) + # does g state need to be moved to g? + g_holder = self.qubit_state_holder.get(g, g) + if g_holder != g: + seq += self.reset_as_move_instructions([g_holder]) + # apply G(g, r) + seq.append(inst.model_copy(update={'qubits': (g, r)})) + return seq + + def insert_moves( + self, + instructions: list[Instruction], + arch: DynamicQuantumArchitecture, + ) -> list[Instruction]: + """Convert a simplified architecture circuit into a equivalent Star architecture circuit with + resonators and MOVE gates. - def _score_choice_heuristic(self, args: tuple[str, str, list[list[str]]]) -> int: - """A simple look ahead heuristic for choosing which qubit to move where. + Inserts MOVE gates into the circuit and changes the existing instruction loci as needed, + while updating the state of the tracker object. - Counts the number of CZ gates until the qubit needs to be moved out. + Can also handle circuits that mix the simplified and real architectures. Args: - args: resonator, qubit, instructions. + instructions: The instructions in the circuit, using physical qubit names. + arch: Real Star quantum architecture we transpile to. + + Raises: + CircuitTranspilationError: Raised when the circuit contains invalid gates that cannot be + transpiled using this method. Returns: - The count/score. + Real Star architecture equivalent of ``circuit`` with MOVEs and resonators added. """ - _, qb, circ = args - score: int = 0 - for instr in circ: - if qb in instr: - if instr[0] != 'cz': - return score - score += 1 - return score - - def update_qubits_in_resonator(self, qubits: Iterable[str]) -> list[str]: - """Applies the resonator to qubit map in the state of the resonator state tracker to the given qubits. + # pylint: disable=too-many-locals + # This method can handle real single- and two-qubit gates, real q-r gates including MOVE, + # and fictional two-qubit gates which it decomposes into real q-r gates. + new_instructions: list[Instruction] = [] - Args: - qubits: The qubits or resonators to apply the state to + for idx, inst in enumerate(instructions): + locus = inst.qubits + try: + IQMClient._validate_instruction(architecture=arch, instruction=inst) + # inst can be applied as is on locus, but we may first need to use MOVEs to make + # sure the locus qubits contain their states + + if inst.name == self.move_gate: + # apply the requested MOVE, closing interfering MOVE sandwiches first + new_instructions += self.create_move_instructions(*locus) + continue + + # are some of the locus qubits' states currently in a resonator? + if res_match := self.resonators_holding_qubits(locus): + # Some locus qubits do not hold their states, which need to be restored before applying the gate. + # NOTE: as a consequence, a barrier closes a MOVE sandwich. + new_instructions += self.reset_as_move_instructions(res_match) + new_instructions.append(inst) - Returns: - The remapped qubits - """ - return [self.res_qb_map.get(q, q) for q in qubits] + except CircuitValidationError as e: + # inst can not be applied to this locus as is + if inst.name not in self.qr_gates_r2q or any(c in self.resonators for c in locus): + raise CircuitTranspilationError(e) from e + + # inst is a fictional qubit-qubit gate G. It can be made valid by decomposing it using MOVEs. + # G is assumed symmetric, hence we may reverse the locus order for more options + gate_q2r = self.qr_gates_q2r[inst.name] + lookahead = instructions[idx:] + g, m, r, badness = min( + self.find_best_sequence(locus, gate_q2r, lookahead), + self.find_best_sequence(locus[::-1], gate_q2r, lookahead), + key=lambda x: x[-1], + ) + if not r: + raise CircuitTranspilationError( + f'Unable to find native gate sequence to enable fictional gate {inst.name} at {locus}.' + ' Try routing the circuit to the simplified architecture first.' + ) from e + + # implement G using the sequence + new_instructions += self.get_sequence(g, m, r, inst) + + return new_instructions + + +def simplify_architecture( + arch: DynamicQuantumArchitecture, + *, + remove_resonators: bool = True, +) -> DynamicQuantumArchitecture: + """Converts the given IQM Star quantum architecture into the equivalent simplified quantum architecture. + + See :mod:`iqm.iqm_client.transpile` for the details. + + Adds fictional gates, abstracts away their gate implementations. + Returns ``arch`` itself if it does not contain computational resonators (in which case nothing will change). + + Args: + arch: quantum architecture to convert + remove_resonators: iff False, return the union of the simplified and real architectures + + Returns: + equivalent quantum architecture with fictional gates + """ + # NOTE: assumes all qubit-resonator gates have the locus order (q, r) + if not arch.computational_resonators: + return arch + + r_set = frozenset(arch.computational_resonators) + q_set = frozenset(arch.qubits) + + moves: dict[str, set[str]] = {} # maps resonator r to qubits q for which we have MOVE(q, r) available + for q, r in arch.gates['move'].loci if 'move' in arch.gates else []: + if q not in q_set or r not in r_set: + raise ValueError(f'MOVE locus {q, r} is not of the form (qubit, resonator)') + moves.setdefault(r, set()).add(q) + + def simplify_gate(gate_name: str, gate_info: GateInfo) -> GateInfo: + """Convert the loci of the given gate""" + # pylint: disable=too-many-nested-blocks + + new_loci: dict[str, tuple[Locus, ...]] = {} # mapping from implementation to its new loci + # loci for fictional gates, a set because multiple resonators can produce the same fictional locus + fictional_loci: set[Locus] = set() + + for impl_name, impl_info in gate_info.implementations.items(): + kept_impl_loci: list[Locus] = [] # these real loci we keep for this implementation + for locus in impl_info.loci: + if len(locus) == 2: + # two-component op + q1, r = locus + if q1 not in q_set: + raise ValueError(f"Unexpected '{gate_name}' locus: {locus}") + + if r in r_set: + if not remove_resonators: + kept_impl_loci.append(locus) + # involves a resonator, for each G(q1, r), MOVE(q2, r) pair add G(q1, q2) to the simplified arch + for q2 in moves.get(r, []): + if q1 != q2: + fictional_loci.add((q1, q2)) + else: + # does not involve a resonator, keep + kept_impl_loci.append(locus) + else: + # other arities: keep as long as it does not involve a resonator + if not set(locus) & r_set: + kept_impl_loci.append(locus) + new_loci[impl_name] = tuple(kept_impl_loci) + + # implementation info is lost in the simplification + if fictional_loci: + new_loci['__fictional'] = tuple(fictional_loci) + + return GateInfo( + implementations={impl_name: GateImplementationInfo(loci=loci) for impl_name, loci in new_loci.items()}, + default_implementation=gate_info.default_implementation, + override_default_implementation={ + locus: impl_name + for locus, impl_name in gate_info.override_default_implementation.items() + if all(c not in r_set for c in locus) + }, + ) + + # create fictional gates, remove real gate loci that involve a resonator + new_gates: dict[str, GateInfo] = {} + for gate_name, gate_info in arch.gates.items(): + if gate_name == 'move': + # MOVE gates do not have fictional versions + if not remove_resonators: + # keep the gate_info as is + new_gates[gate_name] = gate_info + continue + new_gates[gate_name] = simplify_gate(gate_name, gate_info) + + return DynamicQuantumArchitecture( + calibration_set_id=arch.calibration_set_id, + qubits=arch.qubits, + computational_resonators=[] if remove_resonators else arch.computational_resonators, + gates=new_gates, + ) def transpile_insert_moves( circuit: Circuit, arch: DynamicQuantumArchitecture, - existing_moves: Optional[ExistingMoveHandlingOptions] = None, + *, + existing_moves: ExistingMoveHandlingOptions = ExistingMoveHandlingOptions.KEEP, qubit_mapping: Optional[dict[str, str]] = None, + restore_states: bool = True, ) -> Circuit: - """Inserts MOVEs to the circuit according to a given architecture specification. + """Convert a simplified architecture circuit into an equivalent Star architecture circuit with + resonators and MOVE gates, if needed. - The function does nothing if the given architecture specification does not support MOVE gates. - Note that this method assumes that the circuit is already transpiled to a coupling map/architecture where the - resonator has been abstracted away, i.e. the edges of the coupling map that contain resonators are replaced by - edges between the other qubit and all qubits that can be moved to that resonator. + In the typical use case ``circuit`` has been transpiled to a simplified architecture + where the resonators have been abstracted away, and this function converts it into + the corresponding Star architecture circuit. + + It can also handle the case where ``circuit`` already contains MOVE gates and resonators, + which are treated according to ``existing_moves``, followed by the conversion + of the two-qubit gates that are not supported by the Star architecture. + + The function does nothing if ``arch`` does not support MOVE gates. Args: - circuit: The circuit to add MOVE instructions to. - arch: Restrictions of the target device - existing_moves: Specifies how to deal with existing MOVE instruction, - If None, the function will use ExistingMoveHandlingOptions.REMOVE with a user warning if there are move - instructions in the circuit. + circuit: The circuit to convert. + arch: Real Star architecture of the target device. + existing_moves: Specifies how to deal with existing MOVE instructions in ``circuit``, if any. qubit_mapping: Mapping of logical qubit names to physical qubit names. - Can be set to ``None`` if all ``circuits`` already use physical qubit names. + Can be set to ``None`` if ``circuit`` already uses physical qubit names. + restore_states: Iff True, all qubit states held in resonators are returned to their qubits + at the end of the circuit (i.e. all MOVE sandwiches are closed), even when there + is no computational reason to do so. + + Returns: + Equivalent Star architecture circuit with MOVEs and resonators added. """ - res_status = ResonatorStateTracker.from_dynamic_architecture(arch) - if not qubit_mapping: - qubit_mapping = {} - for q in arch.components: - if q not in qubit_mapping.values(): - qubit_mapping[q] = q - existing_moves_in_circuit = [i for i in circuit.instructions if i.name == res_status.move_gate] - - if existing_moves is None and len(existing_moves_in_circuit) > 0: - warnings.warn('Circuit already contains MOVE instructions, removing them before transpiling.') - existing_moves = ExistingMoveHandlingOptions.REMOVE - - if not res_status.supports_move: - if not existing_moves_in_circuit: - return circuit - if existing_moves == ExistingMoveHandlingOptions.REMOVE: - return transpile_remove_moves(circuit) - raise ValueError('Circuit contains MOVE instructions, but device does not support them') - - if existing_moves is None or existing_moves == ExistingMoveHandlingOptions.REMOVE: - circuit = transpile_remove_moves(circuit) - elif existing_moves == ExistingMoveHandlingOptions.KEEP: - try: - IQMClient._validate_circuit_moves(arch, circuit, qubit_mapping=qubit_mapping) - except CircuitValidationError as e: - raise CircuitTranspilationError( - f'Unable to transpile the circuit after validation error: {e.args[0]}' - ) from e + move_gate = _ResonatorStateTracker.move_gate - rev_qubit_mapping = {v: k for k, v in qubit_mapping.items()} - new_instructions = _transpile_insert_moves(list(circuit.instructions), res_status, arch, qubit_mapping) - new_instructions += res_status.reset_as_move_instructions(alt_qubit_names=rev_qubit_mapping) + # see if the circuit already contains some MOVEs/resonators + circuit_has_moves = any(i for i in circuit.instructions if i.name == move_gate) + # can we use MOVEs? + if move_gate not in arch.gates: + if circuit_has_moves: + raise ValueError('Circuit contains MOVE instructions, but the architecture does not support them.') + # nothing to do (do not validate the circuit) + return circuit - return Circuit(name=circuit.name, instructions=new_instructions, metadata=circuit.metadata) + tracker = _ResonatorStateTracker.from_dynamic_architecture(arch) + # add missing QPU components to the mapping (mapped to themselves) + if qubit_mapping is None: + qubit_mapping = {} + for c in set(arch.components) - set(qubit_mapping.values()): + qubit_mapping[c] = c -def _transpile_insert_moves( - instructions: list[Instruction], - res_status: ResonatorStateTracker, - arch: DynamicQuantumArchitecture, - qubit_mapping: dict[str, str], -) -> list[Instruction]: - """Inserts MOVE gates into a list of instructions and changes the existing instructions as needed. + if existing_moves == ExistingMoveHandlingOptions.KEEP: + # convert to physical qubit names + phys_instructions = _map_loci(circuit.instructions, qubit_mapping) + try: + IQMClient._validate_circuit_moves( + arch, + Circuit(name=circuit.name, instructions=phys_instructions, metadata=circuit.metadata), + ) - Helper function for :func:`transpile_insert_moves`. + except CircuitValidationError as e: + raise CircuitTranspilationError(e) from e + else: + if circuit_has_moves and existing_moves == ExistingMoveHandlingOptions.REMOVE: + # convert the circuit into a pure simplified architecture circuit + circuit = transpile_remove_moves(circuit) - Args: - instructions: The instructions in the circuit. - res_status: The location of the resonator states at the start of the instructions. At - the end of this method this tracker is adjusted to reflect the state at the end of the returned instructions. - arch: The target quantum architecture. - qubit_mapping: Mapping from logical qubit names to physical qubit names. + # convert to physical qubit names + phys_instructions = _map_loci(circuit.instructions, qubit_mapping) - Raises: - CircuitTranspilationError: Raised when the circuit contains invalid gates that cannot be transpiled using this - method. + new_instructions = tracker.insert_moves(phys_instructions, arch) - Returns: - The transpiled list of instructions. - """ - new_instructions = [] - rev_qubit_mapping = {v: k for k, v in qubit_mapping.items()} - for idx, i in enumerate(instructions): - qubits = [qubit_mapping[q] for q in i.qubits] - res_match = res_status.resonators_holding_qubits(qubits) - if res_match and i.name not in ['cz', res_status.move_gate]: - # We have a gate on a qubit in the resonator that cannot be executed on the resonator (incl. barriers) - new_instructions += res_status.reset_as_move_instructions(res_match, alt_qubit_names=rev_qubit_mapping) - new_instructions.append(i) - else: - # Check if the instruction is valid, which raises an exception if not. - try: - IQMClient._validate_instruction( - architecture=arch, - instruction=i, - qubit_mapping=qubit_mapping, - ) - new_instructions.append(i) # No adjustment needed - if i.name == res_status.move_gate: # update the tracker if needed - res_status.apply_move(*[qubit_mapping[q] for q in i.qubits]) - except CircuitValidationError as e: - if i.name != 'cz': # We can only fix cz gates at this point - raise CircuitTranspilationError( - f'Unable to transpile the circuit after validation error: {e.args[0]}' - ) from e - # Pick which qubit-resonator pair to apply this cz to - # Pick from qubits already in a resonator or both targets if none off them are in a resonator - resonator_candidates: Optional[list[tuple[str, str, Any]]] = res_status.choose_move_pair( - [res_status.res_qb_map[res] for res in res_match] if res_match else qubits, - [[i.name] + [qubit_mapping[q] for q in i.qubits] for i in instructions[idx:]], - ) - while resonator_candidates: - r, q1, _ = resonator_candidates.pop(0) - q2 = [q for q in qubits if q != q1][0] - try: - IQMClient._validate_instruction( - architecture=arch, - instruction=Instruction( - name='cz', qubits=(rev_qubit_mapping[q2], rev_qubit_mapping[r]), args={} - ), - qubit_mapping=qubit_mapping, - ) - resonator_candidates = None - break - except CircuitValidationError: - pass - - if resonator_candidates is not None: - raise CircuitTranspilationError( - 'Unable to find a valid resonator-qubit pair for a MOVE gate to enable this CZ gate.' - ) from e + if restore_states: + new_instructions += tracker.reset_as_move_instructions() - # remove the other qubit from the resonator if it was in - new_instructions += res_status.reset_as_move_instructions( - [res for res in res_match if res != r], alt_qubit_names=rev_qubit_mapping - ) - # move the qubit into the resonator if it was not yet in. - if not res_match: - new_instructions += res_status.create_move_instructions(q1, r, alt_qubit_names=rev_qubit_mapping) - new_instructions.append( - Instruction(name='cz', qubits=(rev_qubit_mapping[q2], rev_qubit_mapping[r]), args={}) - ) - return new_instructions + # convert back to logical qubit names + new_instructions = _map_loci(new_instructions, qubit_mapping, inverse=True) + return Circuit(name=circuit.name, instructions=new_instructions, metadata=circuit.metadata) def transpile_remove_moves(circuit: Circuit) -> Circuit: - """Removes MOVE gates from a circuit. + """Convert a Star architecture circuit involving resonators and MOVE gates into an equivalent + simplified achitecture circuit without them. - The method assumes that these MOVE gates are moving the resonator state in and out the resonator register to - reconstruct the CZ gates. If this is not the case, the semantic equivalence cannot be guaranteed. + The method assumes that in ``circuit`` a MOVE gate is always used to move a qubit state into a + resonator before any other gates act on the resonator. If this is not the case, this function + will not work as intended. Args: - circuit: The circuit from which the MOVE gates need to be removed. + circuit: Star architecture circuit from which resonators and MOVE gates should be removed. Returns: - The circuit with the MOVE gates removed and the targets for all other gates updated accordingly. + Equivalent simplified architecture circuit without resonators and MOVEs. + """ - res_status = ResonatorStateTracker.from_circuit(circuit) + tracker = _ResonatorStateTracker.from_circuit(circuit) new_instructions = [] - for i in circuit.instructions: - if i.name == res_status.move_gate: - res_status.apply_move(*i.qubits) + for inst in circuit.instructions: + if inst.name == tracker.move_gate: + # update the state tracking, drop the MOVE + tracker.apply_move(*inst.qubits) else: - new_qubits = res_status.update_qubits_in_resonator(i.qubits) - new_instructions.append(Instruction(name=i.name, qubits=new_qubits, args=i.args)) + # map the instruction locus + new_qubits = tracker.map_resonators_in_locus(inst.qubits) + new_instructions.append( + Instruction(name=inst.name, implementation=inst.implementation, qubits=new_qubits, args=inst.args) + ) return Circuit(name=circuit.name, instructions=new_instructions, metadata=circuit.metadata) diff --git a/tests/.pylintrc b/tests/.pylintrc index 1c3067eac..36aa58893 100644 --- a/tests/.pylintrc +++ b/tests/.pylintrc @@ -6,6 +6,7 @@ disable= missing-function-docstring, protected-access, too-few-public-methods, + too-many-positional-arguments, invalid-name, abstract-method, fixme, diff --git a/tests/conftest.py b/tests/conftest.py index 72b478429..5ef4cfb44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -464,7 +464,7 @@ def sample_static_architecture(): @pytest.fixture -def sample_dynamic_architecture(): +def sample_dynamic_architecture() -> DynamicQuantumArchitecture: return DynamicQuantumArchitecture( calibration_set_id=UUID('26c5e70f-bea0-43af-bd37-6212ec7d04cb'), qubits=['QB1', 'QB2', 'QB3'], @@ -496,11 +496,11 @@ def sample_dynamic_architecture(): @pytest.fixture -def sample_move_architecture(): +def sample_move_architecture() -> DynamicQuantumArchitecture: return DynamicQuantumArchitecture( calibration_set_id=UUID('26c5e70f-bea0-43af-bd37-6212ec7d04cb'), qubits=['QB1', 'QB2', 'QB3'], - computational_resonators=['COMP_R', 'COMP_R2'], + computational_resonators=['CR1', 'CR2'], gates={ 'prx': GateInfo( implementations={'drag_gaussian': GateImplementationInfo(loci=(('QB1',), ('QB2',), ('QB3',)))}, @@ -508,12 +508,12 @@ def sample_move_architecture(): override_default_implementation={}, ), 'cz': GateInfo( - implementations={'tgss': GateImplementationInfo(loci=(('QB1', 'COMP_R'), ('QB2', 'COMP_R')))}, + implementations={'tgss': GateImplementationInfo(loci=(('QB1', 'CR1'), ('QB2', 'CR1')))}, default_implementation='tgss', override_default_implementation={}, ), 'move': GateInfo( - implementations={'tgss_crf': GateImplementationInfo(loci=(('QB3', 'COMP_R'),))}, + implementations={'tgss_crf': GateImplementationInfo(loci=(('QB3', 'CR1'),))}, default_implementation='tgss_crf', override_default_implementation={}, ), @@ -526,6 +526,77 @@ def sample_move_architecture(): ) +@pytest.fixture +def hybrid_move_architecture() -> DynamicQuantumArchitecture: + """Contains both q-r and q-q gate loci. + + QB1 + | + CR1 + * |* + QB2 QB3 - QB4 + * |* + CR2 + |* + QB5 + + Here, | signifies a CZ connection and * a MOVE connection. + """ + return DynamicQuantumArchitecture( + calibration_set_id=UUID('26c5e70f-bea0-43af-bd37-6212ec7d04cb'), + qubits=['QB1', 'QB2', 'QB3', 'QB4', 'QB5'], + computational_resonators=['CR1', 'CR2'], + gates={ + 'prx': GateInfo( + implementations={ + 'drag_gaussian': GateImplementationInfo(loci=(('QB1',), ('QB2',), ('QB3',), ('QB4',), ('QB5',))), + }, + default_implementation='drag_gaussian', + override_default_implementation={}, + ), + 'cz': GateInfo( + implementations={ + 'tgss': GateImplementationInfo( + loci=( + ('QB1', 'CR1'), + ('QB3', 'CR1'), + ('QB3', 'QB4'), + ('QB3', 'CR2'), + ('QB5', 'CR2'), + ) + ), + }, + default_implementation='tgss', + override_default_implementation={}, + ), + 'move': GateInfo( + implementations={ + 'tgss_crf': GateImplementationInfo( + loci=( + ('QB2', 'CR1'), + ('QB2', 'CR2'), + ('QB3', 'CR1'), + ('QB3', 'CR2'), + ('QB5', 'CR2'), + ), + ) + }, + default_implementation='tgss_crf', + override_default_implementation={}, + ), + 'measure': GateInfo( + implementations={ + 'constant': GateImplementationInfo( + loci=(('QB1',), ('QB2',), ('QB3',), ('QB4',), ('QB5',)), + ) + }, + default_implementation='constant', + override_default_implementation={}, + ), + }, + ) + + class MockTextResponse: def __init__(self, status_code: int, text: str, history: Optional[list[Response]] = None): self.status_code = status_code diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 729ee1cad..4efc04364 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for user authentication and token management in IQM client. -""" +"""Tests for user authentication and token management in IQM client.""" # pylint: disable=too-many-arguments import json from uuid import UUID, uuid4 diff --git a/tests/test_iqm_client.py b/tests/test_iqm_client.py index 9b1085e50..b002d4787 100644 --- a/tests/test_iqm_client.py +++ b/tests/test_iqm_client.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the IQM client. -""" +"""Tests for the IQM client.""" # pylint: disable=too-many-arguments,too-many-lines from importlib.metadata import version import re @@ -64,26 +63,26 @@ def move_circuit(): ), Instruction( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), Instruction( name='cz', - qubits=('QB1', 'COMP_R'), + qubits=('QB1', 'CR1'), args={}, ), Instruction( name='cz', - qubits=('QB2', 'COMP_R'), + qubits=('QB2', 'CR1'), args={}, ), Instruction( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), ) - return Circuit(name='COMP_R circuit', instructions=instructions) + return Circuit(name='CR1 circuit', instructions=instructions) @pytest.fixture @@ -96,7 +95,7 @@ def move_circuit_with_prx_in_the_sandwich(): ), Instruction( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), Instruction( @@ -106,11 +105,11 @@ def move_circuit_with_prx_in_the_sandwich(): ), Instruction( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), ) - return Circuit(name='COMP_R circuit with PRX in the sandwich', instructions=instructions) + return Circuit(name='CR1 circuit with PRX in the sandwich', instructions=instructions) def test_serialize_qubit_mapping(): @@ -589,15 +588,12 @@ def test_get_quantum_architecture( unstub() -def test_get_feedback_groups( - base_url, channel_properties_url, channel_properties_success, static_architecture_success -): +def test_get_feedback_groups(base_url, channel_properties_url, channel_properties_success, static_architecture_success): """Test retrieving the feedback groups.""" when(requests).get(f'{base_url}/info/client-libraries', headers=ANY, timeout=ANY).thenReturn( mock_supported_client_libraries_response() ) - expect(requests, times=1).get(f'{base_url}/cocos/quantum-architecture', ...).thenReturn( - static_architecture_success) + expect(requests, times=1).get(f'{base_url}/cocos/quantum-architecture', ...).thenReturn(static_architecture_success) iqm_client = IQMClient(base_url, api_variant=APIVariant.V2) expect(requests, times=1).get(channel_properties_url, ...).thenReturn(channel_properties_success) diff --git a/tests/test_transpiler.py b/tests/test_transpiler.py index 9254b0ec9..01f1eb0c1 100644 --- a/tests/test_transpiler.py +++ b/tests/test_transpiler.py @@ -1,4 +1,4 @@ -# Copyright 2024 IQM client developers +# Copyright 2024-2025 IQM client developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,6 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import re +from typing import Optional from uuid import UUID import pytest @@ -24,13 +26,198 @@ GateInfo, Instruction, IQMClient, + simplify_architecture, transpile_insert_moves, transpile_remove_moves, ) -from iqm.iqm_client.transpile import ResonatorStateTracker +from iqm.iqm_client.transpile import _ResonatorStateTracker as ResonatorStateTracker +I = Instruction # shorthand -class TestNaiveMoveTranspiler: + +class MoveTranspilerBase: + """Base class for transpiler tests, containing some utility methods.""" + + arch: DynamicQuantumArchitecture + + def insert( + self, + circuit: Circuit, + existing_moves: Optional[ExistingMoveHandlingOptions] = None, + qb_map: Optional[dict[str, str]] = None, + restore_states: bool = True, + ): + """Call transpile_insert_moves on the given circuit.""" + kwargs = {} + if existing_moves is not None: + kwargs['existing_moves'] = existing_moves + return transpile_insert_moves( + circuit, + arch=self.arch, + qubit_mapping=qb_map, + restore_states=restore_states, + **kwargs, + ) + + def check_equiv_without_moves(self, c1: Circuit, c2: Circuit) -> bool: + """After removing MOVEs, True iff c1 and c2 are equivalent. + Symmetric gates may have been flipped. + """ + c1 = transpile_remove_moves(c1) + c2 = transpile_remove_moves(c2) + for i1, i2 in zip(c1.instructions, c2.instructions): + if i1.name != i2.name or i1.args != i2.args: + return False + if i1.qubits != i2.qubits: + if i1.name != 'cz': + return False + if i1.qubits != i2.qubits[::-1]: + return False + return True + + def assert_valid_circuit(self, circuit: Circuit, qb_map=None) -> None: + """Raises an error if circuit is not valid.""" + if qb_map: + for q in self.arch.qubits: + if q not in qb_map.values(): + qb_map[q] = q + IQMClient._validate_circuit_instructions( + self.arch, [circuit], qubit_mapping=qb_map, must_close_sandwiches=False + ) + + def check_moves_in_circuit(self, circuit: Circuit, moves: tuple[Instruction]) -> bool: + """True iff ``moves`` all appear in ``circuit`` in that order.""" + idx = 0 + for instr in circuit.instructions: + if idx < len(moves) and moves[idx] == instr: + idx += 1 + return idx == len(moves) + + +class TestMoveTranspilerHybrid(MoveTranspilerBase): + """A more complicated hybrid quantum architecture, involving both real q-r and real q-q loci.""" + + @pytest.fixture(autouse=True) + def init_arch(self, hybrid_move_architecture): + # pylint: disable=attribute-defined-outside-init + self.arch: DynamicQuantumArchitecture = hybrid_move_architecture + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_normal_usage_no_moves_added(self, handling_option): + """No MOVE insertion required.""" + + circuit = Circuit( + name='test', + instructions=( + I(name='prx', qubits=('QB1',), args={'phase_t': 0.3, 'angle_t': -0.2}), + I(name='cz', qubits=('QB1', 'CR1'), args={}), + I(name='cz', qubits=('QB3', 'CR1'), args={}), + I(name='cz', qubits=('QB3', 'QB4'), args={}), + ), + ) + c1 = self.insert(circuit, handling_option) + self.assert_valid_circuit(c1) + assert self.check_equiv_without_moves(c1, circuit) + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_normal_usage(self, handling_option): + """Tests basic usage of the transpile method""" + + circuit = Circuit( + name='test', + instructions=( + I(name='prx', qubits=('QB1',), args={'phase_t': 0.3, 'angle_t': -0.2}), + I(name='cz', qubits=('QB1', 'QB2'), args={}), + I(name='cz', qubits=('QB3', 'QB2'), args={}), + I(name='cz', qubits=('QB3', 'QB4'), args={}), + ), + ) + c1 = self.insert(circuit, handling_option) + self.assert_valid_circuit(c1) + assert self.check_equiv_without_moves(c1, circuit) + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_close_sandwich_for_cz(self, handling_option): + """Tests basic usage of the transpile method""" + + # all: cz is invalid because QB3 is not reset, so transpiler must reset it + circuit = Circuit( + name='test', + instructions=( + I(name='move', qubits=('QB3', 'CR1'), args={}), + I(name='cz', qubits=('QB3', 'CR2'), args={}), + ), + ) + if handling_option == ExistingMoveHandlingOptions.KEEP: + with pytest.raises( + CircuitTranspilationError, + match=re.escape("cz acts on ('QB3', 'CR2') while the state(s) of {'QB3'} are"), + ): + self.insert(circuit, handling_option) + else: + c1 = self.insert(circuit, handling_option) + self.assert_valid_circuit(c1) + assert self.check_equiv_without_moves(c1, circuit) + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_close_sandwich_for_move(self, handling_option): + """MOVE sandwiches are automatically closed when a new one needs to start.""" + circuit = Circuit( + name='test', + instructions=( + # without this prx transpiler_remove_moves would leave an empty, invalid circuit + I(name='prx', qubits=('QB2',), args={'phase_t': 0.3, 'angle_t': -0.2}), + I(name='move', qubits=('QB2', 'CR1'), args={}), # opens a sandwich + I(name='move', qubits=('QB2', 'CR2'), args={}), # opens a new sandwich on the same qubit + ), + ) + if handling_option == ExistingMoveHandlingOptions.KEEP: + with pytest.raises( + CircuitTranspilationError, + match=re.escape("MOVE instruction ('QB2', 'CR2'): state of QB2 is in another"), + ): + self.insert(circuit, handling_option) + else: + c1 = self.insert(circuit, handling_option) + self.assert_valid_circuit(c1) + assert self.check_equiv_without_moves(c1, circuit) + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_heuristic_reuse_moved_state(self, handling_option): + """Heuristic chooses the optimal MOVE locus with multiple resonators.""" + circuit = Circuit( + name='test', + instructions=( + I(name='cz', qubits=('QB3', 'QB2'), args={}), # can happen via moving QB2 to either CR + I(name='cz', qubits=('QB5', 'QB2'), args={}), # requires QB2 state in CR2 + ), + ) + c1 = self.insert(circuit, handling_option) + self.assert_valid_circuit(c1) + assert self.check_equiv_without_moves(c1, circuit) + # TODO currently not always optimal + # assert len(c1.instructions) == 4 # prx(QB2), move(QB2, CR2), cz(QB3, CR2), cz(QB5, CR2) + assert 4 <= len(c1.instructions) <= 6 + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_heuristic_move_correct_qubit(self, handling_option): + """Heuristic chooses the optimal MOVE locus with multiple resonators.""" + circuit = Circuit( + name='test', + instructions=( + I(name='cz', qubits=('QB3', 'QB5'), args={}), # both cz(QB3, QB5) and cz(QB5, QB3) are possible via CR2 + I(name='cz', qubits=('QB3', 'QB1'), args={}), # only cz(QB1, QB3) is possible via CR1 + ), + ) + c1 = self.insert(circuit, handling_option, restore_states=False) + self.assert_valid_circuit(c1) + assert self.check_equiv_without_moves(c1, circuit) + # TODO currently not always optimal + # assert len(c1.instructions) == 4 # move(QB5, CR2), cz(QB3, CR2), move(QB3, CR1), cz(QB1, CR1) + assert 4 <= len(c1.instructions) <= 6 + + +class TestMoveTranspiler(MoveTranspilerBase): # pylint: disable=too-many-public-methods @pytest.fixture(autouse=True) @@ -38,63 +225,63 @@ def init_arch(self, sample_move_architecture): # pylint: disable=attribute-defined-outside-init self.arch: DynamicQuantumArchitecture = sample_move_architecture - @property + @pytest.fixture def unsafe_circuit(self): - """A circuit with moves and an unsafe prx""" + """A circuit with a prx in the middle of a MOVE sandwich.""" instructions = ( - Instruction( + I( name='prx', qubits=('QB1',), args={'phase_t': 0.3, 'angle_t': -0.2}, ), - Instruction( + I( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), - Instruction( + I( name='prx', qubits=('QB3',), args={'phase_t': 0.3, 'angle_t': -0.2}, ), - Instruction( + I( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), ) return Circuit(name='unsafe', instructions=instructions) - @property + @pytest.fixture def safe_circuit(self): """A partially transpiled circuit.""" instructions = ( - Instruction( + I( name='prx', qubits=('QB1',), args={'phase_t': 0.3, 'angle_t': -0.2}, ), - Instruction( + I( name='cz', - qubits=('QB1', 'COMP_R'), + qubits=('QB1', 'CR1'), args={}, ), - Instruction( + I( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), - Instruction( + I( name='cz', - qubits=('QB2', 'COMP_R'), + qubits=('QB2', 'CR1'), args={}, ), - Instruction( + I( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), - Instruction( + I( name='cz', qubits=('QB3', 'QB1'), args={}, @@ -102,31 +289,31 @@ def safe_circuit(self): ) return Circuit(name='safe', instructions=instructions) - @property + @pytest.fixture def simple_circuit(self): - """An untranspiled circuit.""" + """An untranspiled circuit in the simplified architecture.""" instructions = ( - Instruction( + I( name='prx', qubits=('QB1',), args={'phase_t': 0.3, 'angle_t': -0.2}, ), - Instruction( + I( name='cz', - qubits=('QB1', 'COMP_R'), + qubits=('QB1', 'CR1'), args={}, ), - Instruction( + I( name='cz', - qubits=('QB2', 'COMP_R'), + qubits=('QB2', 'CR1'), args={}, ), - Instruction( + I( name='cz', qubits=('QB3', 'QB1'), args={}, ), - Instruction( + I( name='prx', qubits=('QB3',), args={'phase_t': 0.3, 'angle_t': -0.2}, @@ -134,48 +321,31 @@ def simple_circuit(self): ) return Circuit(name='safe', instructions=instructions) - @property - def mapped_circuit(self): - """A circuit with different qubit names and a qubit mapping.""" - instructions = ( - Instruction( - name='prx', - qubits=('A',), - args={'phase_t': 0.3, 'angle_t': -0.2}, - ), - Instruction( - name='cz', - qubits=('A', 'B'), - args={}, - ), - ) - return Circuit(name='mapped', instructions=instructions), {'A': 'QB3', 'B': 'QB1'} - - @property + @pytest.fixture def ambiguous_circuit(self): """A circuit that is unclear how to compile it because there is only one move""" instructions = ( - Instruction( + I( name='prx', qubits=('QB1',), args={'phase_t': 0.3, 'angle_t': -0.2}, ), - Instruction( + I( name='cz', - qubits=('QB1', 'COMP_R'), + qubits=('QB1', 'CR1'), args={}, ), - Instruction( + I( name='move', - qubits=('QB3', 'COMP_R'), + qubits=('QB3', 'CR1'), args={}, ), - Instruction( + I( name='cz', - qubits=('QB2', 'COMP_R'), + qubits=('QB2', 'CR1'), args={}, ), - Instruction( + I( name='cz', qubits=('QB3', 'QB1'), args={}, @@ -183,155 +353,144 @@ def ambiguous_circuit(self): ) return Circuit(name='ambiguous', instructions=instructions) - def insert( - self, - circuit: Circuit, - arg=None, - qb_map=None, - ): - return transpile_insert_moves(circuit, arch=self.arch, existing_moves=arg, qubit_mapping=qb_map) - - def remove(self, circuit: Circuit): - return transpile_remove_moves(circuit) - - def check_equiv_without_moves(self, c1: Circuit, c2: Circuit): - c1 = self.remove(c1) - c2 = self.remove(c2) - for i1, i2 in zip(c1.instructions, c2.instructions): - if i1.name != i2.name or i1.args != i2.args: - return False - if i1.qubits != i2.qubits: - if i1.name != 'cz': - return False - if not all(q1 == q2 for q1, q2 in zip(i1.qubits, reversed(i2.qubits))): - return False - return True - - def assert_valid_circuit(self, circuit: Circuit, qb_map=None): - # pylint: disable=no-member - if qb_map: - for q in self.arch.qubits: - if q not in qb_map.values(): - qb_map[q] = q - IQMClient._validate_circuit_instructions(self.arch, [circuit], qubit_mapping=qb_map) - - def check_moves_in_circuit(self, circuit: Circuit, moves: tuple[Instruction]): - idx = 0 - for instr in circuit.instructions: - if idx < len(moves) and moves[idx] == instr: - idx += 1 - return idx == len(moves) - - def test_no_moves_supported(self, sample_dynamic_architecture): + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_simple_architecture_simple_circuit(self, sample_dynamic_architecture, handling_option, simple_circuit): """Tests transpiler for architectures without a resonator""" - for option in ExistingMoveHandlingOptions: - c1 = transpile_insert_moves(self.simple_circuit, sample_dynamic_architecture, existing_moves=option) - assert c1 == self.simple_circuit - if option != ExistingMoveHandlingOptions.REMOVE: - with pytest.raises(ValueError): - _ = transpile_insert_moves(self.safe_circuit, sample_dynamic_architecture, existing_moves=option) - else: - c2 = transpile_insert_moves(self.safe_circuit, sample_dynamic_architecture, existing_moves=option) - assert self.check_equiv_without_moves(self.safe_circuit, c2) - - def test_unspecified(self): + c1 = transpile_insert_moves(simple_circuit, sample_dynamic_architecture, existing_moves=handling_option) + # no changes + assert c1 == simple_circuit + + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_simple_architecture_moves_in_circuit(self, sample_dynamic_architecture, handling_option, safe_circuit): + """MOVEs in the circuit cause an error if architecture does not support them.""" + with pytest.raises( + ValueError, + match='Circuit contains MOVE instructions, but the architecture does not support them', + ): + transpile_insert_moves(safe_circuit, sample_dynamic_architecture, existing_moves=handling_option) + + @pytest.mark.parametrize('circuit', ['simple_circuit', 'safe_circuit']) + def test_no_handling_option(self, circuit, request): """Tests transpiler in case the handling option is not specified.""" - c1 = self.insert(self.simple_circuit) + circuit = request.getfixturevalue(circuit) + c1 = self.insert(circuit) self.assert_valid_circuit(c1) - assert self.check_equiv_without_moves(c1, self.simple_circuit) - with pytest.warns(UserWarning): - c2 = self.insert(self.safe_circuit) - assert self.check_equiv_without_moves(c2, self.safe_circuit) + assert self.check_equiv_without_moves(c1, circuit) - def test_normal_usage(self, sample_circuit: Circuit): + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_normal_usage(self, simple_circuit, handling_option): """Tests basic usage of the transpile method""" - for handling_option in ExistingMoveHandlingOptions: - c1 = self.insert(self.simple_circuit, handling_option) - self.assert_valid_circuit(c1) - assert self.check_equiv_without_moves(c1, self.simple_circuit) - with pytest.raises(CircuitTranspilationError): - self.insert(sample_circuit, handling_option) # untranspiled circuit - - def test_keep(self): - """Tests special cases for the KEEP option""" - moves = tuple(i for i in self.safe_circuit.instructions if i.name == 'move') - c1 = self.insert(self.safe_circuit, ExistingMoveHandlingOptions.KEEP) + c1 = self.insert(simple_circuit, handling_option) self.assert_valid_circuit(c1) - assert self.check_moves_in_circuit(c1, moves) - - with pytest.raises(CircuitTranspilationError): - self.insert(self.unsafe_circuit, ExistingMoveHandlingOptions.KEEP) - - with pytest.raises(CircuitTranspilationError): - self.insert(self.ambiguous_circuit, ExistingMoveHandlingOptions.KEEP) - - def test_remove(self): - """Tests if removing works as intended.""" - for c in [self.safe_circuit, self.unsafe_circuit, self.ambiguous_circuit]: - moves = tuple(i for i in c.instructions if i.name == 'move') - c1 = self.remove(c) - assert not self.check_moves_in_circuit(c1, moves) - c1_with = self.insert(c1, ExistingMoveHandlingOptions.REMOVE) - c1_direct = self.insert(c, ExistingMoveHandlingOptions.REMOVE) - assert c1_with == c1_direct - assert self.check_equiv_without_moves(c1, c1_with) - assert self.check_equiv_without_moves(c1, c1_direct) - - def test_trust(self): - """Tests if trust works as intended""" - moves = tuple(i for i in self.safe_circuit.instructions if i.name == 'move') - c1 = self.insert(self.safe_circuit, ExistingMoveHandlingOptions.TRUST) + assert self.check_equiv_without_moves(c1, simple_circuit) + + @pytest.mark.parametrize( + 'circuit,error', + [ + ('safe_circuit', None), + ('unsafe_circuit', "prx acts on ('QB3',) while the state(s) of {'QB3'} are in a resonator"), + ('ambiguous_circuit', "cz acts on ('QB3', 'QB1') while the state(s) of {'QB3'} are in a resonator"), + ], + ) + def test_keep(self, circuit, error, request): + """Tests special cases for the KEEP option""" + c = request.getfixturevalue(circuit) + moves = tuple(i for i in c.instructions if i.name == 'move') + if error: + with pytest.raises( + CircuitTranspilationError, + match=re.escape(error), + ): + self.insert(c, ExistingMoveHandlingOptions.KEEP) + else: + c1 = self.insert(c, ExistingMoveHandlingOptions.KEEP) + self.assert_valid_circuit(c1) + assert self.check_moves_in_circuit(c1, moves) + + @pytest.mark.parametrize('circuit', ['safe_circuit', 'unsafe_circuit', 'ambiguous_circuit']) + def test_remove_moves(self, circuit, request): + """Tests if removing MOVEs works as intended.""" + c = request.getfixturevalue(circuit) + moves = tuple(i for i in c.instructions if i.name == 'move') + c1 = transpile_remove_moves(c) + assert not self.check_moves_in_circuit(c1, moves) + c1_with = self.insert(c1, ExistingMoveHandlingOptions.REMOVE) + c1_direct = self.insert(c, ExistingMoveHandlingOptions.REMOVE) + assert c1_with == c1_direct + assert self.check_equiv_without_moves(c1, c1_with) + assert self.check_equiv_without_moves(c1, c1_direct) + + @pytest.mark.parametrize('circuit', ['safe_circuit', 'unsafe_circuit', 'ambiguous_circuit']) + def test_trust(self, circuit, request): + """Tests if the TRUST option works as intended.""" + # Unsafe PRX is made safe since insert adds a MOVE that brings the qubit state back + # before the PRX is applied. If you want to use unsafe PRXs, do not use transpile_insert_moves. + # ambiguous MOVE sandwich is automatically closed + c = request.getfixturevalue(circuit) + moves = tuple(i for i in c.instructions if i.name == 'move') + c1 = self.insert(c, ExistingMoveHandlingOptions.TRUST) self.assert_valid_circuit(c1) assert self.check_moves_in_circuit(c1, moves) - moves2 = tuple(i for i in self.unsafe_circuit.instructions if i.name == 'move') - c2 = self.insert(self.unsafe_circuit, ExistingMoveHandlingOptions.TRUST) - self.assert_valid_circuit(c2) - assert self.check_moves_in_circuit(c2, moves2) + @pytest.mark.parametrize('handling_option', ExistingMoveHandlingOptions) + def test_with_qubit_map(self, handling_option): + """Test if qubit mapping works as intended""" - moves3 = tuple(i for i in self.ambiguous_circuit.instructions if i.name == 'move') - c3 = self.insert(self.ambiguous_circuit, ExistingMoveHandlingOptions.TRUST) - self.assert_valid_circuit(c3) - assert self.check_moves_in_circuit(c3, moves3) + circuit = Circuit( + name='mapped', + instructions=( + I( + name='prx', + qubits=('A',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), + I( + name='cz', + qubits=('A', 'B'), + args={}, + ), + ), + ) + qb_map = {'A': 'QB3', 'B': 'QB1'} - def test_with_qubit_map(self): - """Test if qubit mapping works as intended""" - for handling_option in ExistingMoveHandlingOptions: - circuit, qb_map = self.mapped_circuit - c1 = self.insert(circuit, handling_option, qb_map) - self.assert_valid_circuit(c1, qb_map) - assert self.check_equiv_without_moves(c1, circuit) + c1 = self.insert(circuit, handling_option, qb_map) + self.assert_valid_circuit(c1, qb_map) + assert self.check_equiv_without_moves(c1, circuit) def test_multiple_resonators(self, sample_move_architecture): """Test if multiple resonators works.""" + # add MOVE loci to the architecture default_move_impl = sample_move_architecture.gates['move'].default_implementation - sample_move_architecture.gates['move'].implementations[default_move_impl].loci += (('QB1', 'COMP_R2'),) + sample_move_architecture.gates['move'].implementations[default_move_impl].loci += (('QB1', 'CR2'),) + # Test with bad architecture circuit = Circuit( name='multi resonators', instructions=( - Instruction(name='cz', qubits=('QB1', 'QB2'), args={}), - Instruction(name='cz', qubits=('QB2', 'QB3'), args={}), - Instruction(name='cz', qubits=('QB1', 'QB3'), args={}), + I(name='cz', qubits=('QB1', 'QB2'), args={}), + I(name='cz', qubits=('QB2', 'QB3'), args={}), + I(name='cz', qubits=('QB1', 'QB3'), args={}), ), ) - with pytest.raises(CircuitTranspilationError): + bad_architecture = sample_move_architecture.model_copy(deep=True) + with pytest.raises( + CircuitTranspilationError, + match=re.escape("Unable to find native gate sequence to enable fictional gate cz at ('QB1', 'QB2')"), + ): + # CZ(QB1, QB2) is not possible # Create a new copy of the DQA to ensure the cached properties are computed only for this architecture. - bad_architecture = sample_move_architecture.model_copy(deep=True) transpiled_circuit = transpile_insert_moves(circuit, bad_architecture) - # Add the necessary CZ gates to make it a good architecture. + # Add the CZ loci to the architecture make it ok for this circuit. default_cz_impl = sample_move_architecture.gates['cz'].default_implementation sample_move_architecture.gates['cz'].implementations[default_cz_impl].loci += tuple( - (qb, 'COMP_R2') for qb in sample_move_architecture.components + (qb, 'CR2') for qb in sample_move_architecture.qubits ) + # Create a new copy of the DQA to ensure the cached properties are computed only for this architecture. good_architecture = sample_move_architecture.model_copy(deep=True) transpiled_circuit = transpile_insert_moves(circuit, good_architecture) IQMClient._validate_circuit_instructions(good_architecture, [transpiled_circuit]) - - print(transpiled_circuit) - assert self.check_equiv_without_moves(circuit, transpiled_circuit) def test_circuit_on_nonexisting_qubits(self): @@ -339,72 +498,78 @@ def test_circuit_on_nonexisting_qubits(self): c = Circuit( name='QB5 does not exist', instructions=( - Instruction( + I( name='prx', qubits=('QB5',), args={'phase_t': 0.3, 'angle_t': -0.2}, ), ), ) - with pytest.raises(CircuitTranspilationError): + with pytest.raises(CircuitTranspilationError, match=re.escape("('QB5',) is not allowed as locus for 'prx'")): self.insert(c, qb_map={'QB5': 'QB5'}) - def test_unavailable_cz(self): - """Test for unavailable CZ gates. This test reproduces the bug COMP-1485.""" - c = Circuit( - name='bell', - instructions=( # prx uses wrong values for the H gate, but that's not the point of this test - Instruction( - name='prx', - qubits=('QB1',), - args={'phase_t': 0.3, 'angle_t': -0.2}, - ), - Instruction( - name='prx', - qubits=('QB2',), - args={'phase_t': 0.3, 'angle_t': -0.2}, - ), - Instruction( - name='cz', - qubits=('QB1', 'QB2'), - args={}, - ), - Instruction( - name='prx', - qubits=('QB2',), - args={'phase_t': 0.3, 'angle_t': -0.2}, + @pytest.mark.parametrize( + 'circuit', + [ + Circuit( + name='bell', + instructions=( # prx uses wrong values for the H gate, but that's not the point of this test + I( + name='prx', + qubits=('QB1',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), + I( + name='prx', + qubits=('QB2',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), + I( + name='cz', + qubits=('QB1', 'QB2'), + args={}, + ), + I( + name='prx', + qubits=('QB2',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), ), ), - ) - c2 = Circuit( - name='bell', - instructions=( # prx uses wrong values for the H gate, but that's not the point of this test - Instruction( - name='prx', - qubits=('QB1',), - args={'phase_t': 0.3, 'angle_t': -0.2}, - ), - Instruction( - name='prx', - qubits=('QB2',), - args={'phase_t': 0.3, 'angle_t': -0.2}, - ), - Instruction( - name='cz', - qubits=('QB2', 'QB1'), # Swapped qubits - args={}, - ), - Instruction( - name='prx', - qubits=('QB2',), - args={'phase_t': 0.3, 'angle_t': -0.2}, + Circuit( + name='bell', + instructions=( # prx uses wrong values for the H gate, but that's not the point of this test + I( + name='prx', + qubits=('QB1',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), + I( + name='prx', + qubits=('QB2',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), + I( + name='cz', + qubits=('QB2', 'QB1'), # Swapped qubits + args={}, + ), + I( + name='prx', + qubits=('QB2',), + args={'phase_t': 0.3, 'angle_t': -0.2}, + ), ), ), - ) + ], + ) + def test_can_reverse_cz_locus(self, circuit): + """Circuit requires unavailable CZ locus, but the reversed locus is available in the DQA, + and CZ is symmetric. This test reproduces the bug COMP-1485.""" arch = DynamicQuantumArchitecture( calibration_set_id=UUID('0c5a5624-2faf-4885-888c-805af891479c'), qubits=['QB1', 'QB2'], - computational_resonators=['COMP_R'], + computational_resonators=['CR1'], gates={ 'prx': GateInfo( implementations={'drag_gaussian': GateImplementationInfo(loci=(('QB1',), ('QB2',)))}, @@ -412,12 +577,12 @@ def test_unavailable_cz(self): override_default_implementation={}, ), 'cz': GateInfo( - implementations={'tgss': GateImplementationInfo(loci=(('QB2', 'COMP_R'),))}, + implementations={'tgss': GateImplementationInfo(loci=(('QB2', 'CR1'),))}, default_implementation='tgss', override_default_implementation={}, ), 'move': GateInfo( - implementations={'tgss_crf': GateImplementationInfo(loci=(('QB1', 'COMP_R'), ('QB2', 'COMP_R')))}, + implementations={'tgss_crf': GateImplementationInfo(loci=(('QB1', 'CR1'), ('QB2', 'CR1')))}, default_implementation='tgss_crf', override_default_implementation={}, ), @@ -428,116 +593,182 @@ def test_unavailable_cz(self): ), }, ) - circuits = [transpile_insert_moves(c, arch=arch), transpile_insert_moves(c2, arch=arch)] - IQMClient._validate_circuit_instructions(arch, circuits) + c1 = transpile_insert_moves(circuit, arch=arch) + IQMClient._validate_circuit_instructions(arch, [c1]) + @pytest.mark.parametrize( + 'locus', [(qb1, qb2) for qb1 in ['QB1', 'QB2', 'QB3'] for qb2 in ['QB1', 'QB2', 'QB3'] if qb1 != qb2] + ) + def test_pass_always_picks_correct_move_gate(self, locus): + circuit = Circuit( + name='test', + instructions=(I(name='cz', qubits=locus, args={}),), + ) + if set(locus) == {'QB1', 'QB2'}: + # There is no MOVE gate available between this pair of qubits + with pytest.raises( + CircuitTranspilationError, + match=re.escape(f"Unable to find native gate sequence to enable fictional gate cz at {locus}"), + ): + transpile_insert_moves(circuit, self.arch) + else: + transpiled_circuit = transpile_insert_moves(circuit, self.arch) + IQMClient._validate_circuit_instructions(self.arch, [transpiled_circuit]) -class TestResonatorStateTracker: - alt_qubit_names = {'COMP_R': 'A', 'QB1': 'B', 'QB3': 'C'} - def test_apply_move(self, sample_dynamic_architecture, sample_move_architecture): +class TestResonatorStateTracker: + def test_apply_move_no_resonators(self, sample_dynamic_architecture): # Check handling of an architecture without a resonator no_move_status = ResonatorStateTracker.from_dynamic_architecture(sample_dynamic_architecture) - assert not no_move_status.supports_move - with pytest.raises(CircuitTranspilationError): + with pytest.raises(CircuitTranspilationError, match=re.escape("MOVE locus ('QB1', 'QB2') is not allowed")): no_move_status.apply_move('QB1', 'QB2') + + def test_apply_move(self, sample_move_architecture): # Check handling of an architecture with resonator status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) - assert status.supports_move - status.apply_move('QB3', 'COMP_R') - assert status.res_qb_map['COMP_R'] == 'QB3' - status.apply_move('QB3', 'COMP_R') - assert status.res_qb_map['COMP_R'] == 'COMP_R' - with pytest.raises(CircuitTranspilationError): - status.apply_move('QB1', 'COMP_R') - with pytest.raises(CircuitTranspilationError): + status.apply_move('QB3', 'CR1') + assert status.res_state_owner['CR1'] == 'QB3' + status.apply_move('QB3', 'CR1') + assert status.res_state_owner['CR1'] == 'CR1' + with pytest.raises(CircuitTranspilationError, match=re.escape("MOVE locus ('QB1', 'CR1') is not allowed")): + status.apply_move('QB1', 'CR1') + with pytest.raises(CircuitTranspilationError, match=re.escape("MOVE locus ('QB1', 'QB2') is not allowed")): status.apply_move('QB1', 'QB2') - status.res_qb_map['COMP_R'] = 'QB1' - with pytest.raises(CircuitTranspilationError): - status.apply_move('QB3', 'COMP_R') + status.res_state_owner['CR1'] = 'QB1' + with pytest.raises(CircuitTranspilationError, match=re.escape("MOVE locus ('QB3', 'CR1') is not allowed")): + status.apply_move('QB3', 'CR1') def test_create_move_instructions(self, sample_move_architecture): default_move_impl = sample_move_architecture.gates['move'].default_implementation - sample_move_architecture.gates['move'].implementations[default_move_impl].loci += (('QB1', 'COMP_R'),) + sample_move_architecture.gates['move'].implementations[default_move_impl].loci += (('QB1', 'CR1'),) status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) - instr = Instruction(name='move', qubits=('QB3', 'COMP_R'), args={}) - # Check insertion without and with apply_move - gen_instr = tuple(status.create_move_instructions('QB3', 'COMP_R', apply_move=False)) + instr = I(name='move', qubits=('QB3', 'CR1'), args={}) + # Check insertion + gen_instr = tuple(status.create_move_instructions('QB3', 'CR1')) assert len(gen_instr) == 1 assert gen_instr[0] == instr - assert status.res_qb_map['COMP_R'] == 'COMP_R' - gen_instr = tuple(status.create_move_instructions('QB3', 'COMP_R', apply_move=True)) + assert status.res_state_owner['CR1'] == 'QB3' + gen_instr = tuple(status.create_move_instructions('QB3', 'CR1')) assert len(gen_instr) == 1 assert gen_instr[0] == instr - assert status.res_qb_map['COMP_R'] == 'QB3' - status.res_qb_map['COMP_R'] = 'QB1' - # Check removal without and with apply_move - gen_instr = tuple(status.create_move_instructions('QB3', 'COMP_R', apply_move=False)) + assert status.res_state_owner['CR1'] == 'CR1' + status.res_state_owner['CR1'] = 'QB1' + # Check removal + gen_instr = tuple(status.create_move_instructions('QB3', 'CR1')) assert len(gen_instr) == 2 - assert gen_instr[0] == Instruction(name='move', qubits=('QB1', 'COMP_R'), args={}) + assert gen_instr[0] == I(name='move', qubits=('QB1', 'CR1'), args={}) assert gen_instr[1] == instr - # Check with a qubit mapping - gen_instr = tuple( - status.create_move_instructions('QB3', 'COMP_R', apply_move=True, alt_qubit_names=self.alt_qubit_names) - ) - assert len(gen_instr) == 2 - assert gen_instr[0] == Instruction(name='move', qubits=('B', 'A'), args={}) - assert gen_instr[1] == Instruction(name='move', qubits=('C', 'A'), args={}) - assert status.res_qb_map['COMP_R'] == 'QB3' + assert status.res_state_owner['CR1'] == 'QB3' def test_reset_as_move_instructions(self, sample_move_architecture): status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) # No reset needed gen_instr = tuple(status.reset_as_move_instructions()) assert len(gen_instr) == 0 - # Reset with argument and not apply_move - status.apply_move('QB3', 'COMP_R') - gen_instr = tuple(status.reset_as_move_instructions(['COMP_R'], apply_move=False)) + # Reset with argument + status.apply_move('QB3', 'CR1') + gen_instr = tuple(status.reset_as_move_instructions(['CR1'])) assert len(gen_instr) == 1 - assert gen_instr[0] == Instruction(name='move', qubits=('QB3', 'COMP_R'), args={}) - assert status.res_qb_map['COMP_R'] == 'QB3' - # Reset without argument, with qubit mapping, and not apply_move - gen_instr = tuple(status.reset_as_move_instructions(apply_move=False, alt_qubit_names=self.alt_qubit_names)) - assert len(gen_instr) == 1 - assert gen_instr[0] == Instruction(name='move', qubits=('C', 'A'), args={}) - assert status.res_qb_map['COMP_R'] == 'QB3' - # Reset without arguments and with apply_move - gen_instr = tuple(status.reset_as_move_instructions(apply_move=True)) + assert gen_instr[0] == I(name='move', qubits=('QB3', 'CR1'), args={}) + assert status.res_state_owner['CR1'] == 'CR1' + # Reset without arguments + status.apply_move('QB3', 'CR1') + gen_instr = tuple(status.reset_as_move_instructions()) assert len(gen_instr) == 1 - assert gen_instr[0] == Instruction(name='move', qubits=('QB3', 'COMP_R'), args={}) - assert status.res_qb_map['COMP_R'] == 'COMP_R' - - def test_available_resonators_to_move(self, sample_move_architecture): - components = sample_move_architecture.components - status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) - assert status.available_resonators_to_move(components) == { - 'COMP_R': [], - 'COMP_R2': [], - 'QB1': [], - 'QB2': [], - 'QB3': ['COMP_R'], - } + assert gen_instr[0] == I(name='move', qubits=('QB3', 'CR1'), args={}) + assert status.res_state_owner['CR1'] == 'CR1' def test_qubits_in_resonator(self, sample_move_architecture): components = sample_move_architecture.components status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) assert status.resonators_holding_qubits(components) == [] - status.apply_move('QB3', 'COMP_R') - assert status.resonators_holding_qubits(components) == ['COMP_R'] + status.apply_move('QB3', 'CR1') + assert status.resonators_holding_qubits(components) == ['CR1'] - def test_choose_move_pair(self, sample_move_architecture): - status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) - with pytest.raises(CircuitTranspilationError): - status.choose_move_pair(['QB1', 'QB2'], []) - resonator_candidates = status.choose_move_pair( - ['QB1', 'QB2', 'QB3'], [['cz', 'QB2', 'QB3'], ['prx', 'QB2'], ['prx', 'QB3']] - ) - r, q, _ = resonator_candidates[0] - assert r == 'COMP_R' - assert q == 'QB3' - - def test_update_state_in_resonator(self, sample_move_architecture): + def test_map_resonators_in_locus(self, sample_move_architecture): components = sample_move_architecture.components status = ResonatorStateTracker.from_dynamic_architecture(sample_move_architecture) - status.apply_move('QB3', 'COMP_R') - assert status.update_qubits_in_resonator(components) == ['QB3', 'COMP_R2', 'QB1', 'QB2', 'QB3'] + status.apply_move('QB3', 'CR1') + assert status.map_resonators_in_locus(components) == ('QB3', 'CR2', 'QB1', 'QB2', 'QB3') + + +def test_simplify_architecture(sample_move_architecture): + """Resonators and MOVE gates are eliminated, q-r gates are replaced with q-q gates.""" + simple = simplify_architecture(sample_move_architecture) + + assert simple.qubits == sample_move_architecture.qubits + assert not simple.computational_resonators + + assert len(simple.gates) == 3 + assert 'move' not in simple.gates + assert simple.gates['measure'].loci == (('QB1',), ('QB2',), ('QB3',)) + assert simple.gates['prx'].loci == (('QB1',), ('QB2',), ('QB3',)) + assert simple.gates['cz'].loci == ( + ('QB1', 'QB3'), + ('QB2', 'QB3'), + ) + + +def test_simplify_architecture_hybrid(hybrid_move_architecture): + """Resonators and MOVE gates are eliminated, q-r gates are replaced with q-q gates.""" + simple = simplify_architecture(hybrid_move_architecture) + + qubit_loci = (('QB1',), ('QB2',), ('QB3',), ('QB4',), ('QB5',)) + assert simple.qubits == hybrid_move_architecture.qubits + assert not simple.computational_resonators + assert len(simple.gates) == len(hybrid_move_architecture.gates) - 1 + assert 'move' not in simple.gates + + # default implementations have not changed + for name, info in simple.gates.items(): + orig_info = hybrid_move_architecture.gates[name] + assert orig_info.default_implementation == info.default_implementation + assert orig_info.override_default_implementation == info.override_default_implementation + + # non-ficitional gates retain their implementations + impls = simple.gates['prx'].implementations + assert len(impls) == 1 + assert impls['drag_gaussian'].loci == qubit_loci + + impls = simple.gates['measure'].implementations + assert len(impls) == 1 + assert impls['constant'].loci == qubit_loci + + impls = simple.gates['cz'].implementations + assert len(impls) == 2 + assert impls['tgss'].loci == (('QB3', 'QB4'),) + # fictional gates lose their implementation info + assert set(impls['__fictional'].loci) == { + ('QB1', 'QB2'), + ('QB1', 'QB3'), + ('QB3', 'QB2'), + ('QB5', 'QB2'), + ('QB5', 'QB2'), + ('QB5', 'QB3'), + ('QB3', 'QB5'), + } + + +@pytest.mark.parametrize('locus', [('QB1', 'QB2'), ('CR1', 'CR2'), ('CR1', 'QB1')]) +def test_simplify_architecture_bad_move_locus(locus): + """MOVE gate with a locus that isn't (qubit, resonator).""" + dqa = DynamicQuantumArchitecture( + calibration_set_id=UUID('26c5e70f-bea0-43af-bd37-6212ec7d04cb'), + qubits=['QB1', 'QB2'], + computational_resonators=['CR1', 'CR2'], + gates={ + 'move': GateInfo( + implementations={'tgss_crf': GateImplementationInfo(loci=(locus,))}, + default_implementation='tgss_crf', + override_default_implementation={}, + ), + }, + ) + with pytest.raises(ValueError, match=re.escape(f'MOVE locus {locus} is not of the form')): + simplify_architecture(dqa) + + +def test_simplify_architecture_no_resonators(sample_dynamic_architecture): + """Architectures with no resonators are not changed.""" + simple = simplify_architecture(sample_dynamic_architecture) + assert simple == sample_dynamic_architecture diff --git a/tests/test_util.py b/tests/test_util.py index 3f12a9dfc..f32007dac 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for the IQM client utilities. -""" +"""Tests for the IQM client utilities.""" import numpy as np import pytest diff --git a/tests/test_validation.py b/tests/test_validation.py index 95a7e08d0..be738a742 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -11,8 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for circuit validation. -""" +"""Tests for circuit validation.""" import pytest from iqm.iqm_client import ( @@ -24,7 +23,7 @@ MoveGateValidationMode, ) -sample_qb_mapping = {'0': 'COMP_R', '1': 'QB1', '2': 'QB2', '3': 'QB3', '100': 'COMP_R2'} +sample_qb_mapping = {'0': 'CR1', '1': 'QB1', '2': 'QB2', '3': 'QB3', '100': 'CR2'} reverse_qb_mapping = {value: key for key, value in sample_qb_mapping.items()} @@ -34,6 +33,9 @@ Instruction(name='barrier', qubits=['QB1'], args={}), Instruction(name='barrier', qubits=['QB1', 'QB2'], args={}), Instruction(name='barrier', qubits=['QB2', 'QB1'], args={}), # barrier can use any loci + Instruction(name='delay', qubits=['QB1'], args={'duration': 80e-9}), + Instruction(name='delay', qubits=['QB1', 'QB2'], args={'duration': 40e-9}), + Instruction(name='delay', qubits=['QB2', 'QB1'], args={'duration': 100e-9}), # delay can use any loci Instruction(name='prx', qubits=['QB1'], args={'phase_t': 0.3, 'angle_t': -0.2}), Instruction(name='cz', qubits=['QB1', 'QB2'], args={}), Instruction(name='cz', qubits=['QB2', 'QB1'], args={}), # CZ is symmetric @@ -51,6 +53,7 @@ def test_valid_instruction(sample_dynamic_architecture, instruction): 'instruction,match', [ [Instruction(name='barrier', qubits=['QB1', 'QB2', 'XXX'], args={}), 'does not exist'], + [Instruction(name='delay', qubits=['YYY'], args={'duration': 40e-9}), 'does not exist'], [ Instruction(name='prx', qubits=['QB4'], args={'phase_t': 0.3, 'angle_t': -0.2}), "not allowed as locus for 'prx'", @@ -75,7 +78,7 @@ def test_invalid_instruction(sample_dynamic_architecture, instruction, match): @pytest.mark.parametrize('qubit_mapping', [None, sample_qb_mapping]) -@pytest.mark.parametrize('qubits', [['QB1', 'COMP_R'], ['COMP_R', 'QB1'], ['COMP_R', 'QB2']]) +@pytest.mark.parametrize('qubits', [['QB1', 'CR1'], ['CR1', 'QB1'], ['CR1', 'QB2']]) def test_allowed_cz_qubits(sample_move_architecture, qubits, qubit_mapping): """ Tests that instruction validation passes for allowed CZ loci @@ -91,7 +94,7 @@ def test_allowed_cz_qubits(sample_move_architecture, qubits, qubit_mapping): @pytest.mark.parametrize('qubit_mapping', [None, sample_qb_mapping]) @pytest.mark.parametrize( - 'qubits', [['QB1', 'QB2'], ['QB2', 'QB1'], ['QB1', 'QB1'], ['QB3', 'QB1'], ['COMP_R', 'COMP_R'], ['COMP_R', 'QB3']] + 'qubits', [['QB1', 'QB2'], ['QB2', 'QB1'], ['QB1', 'QB1'], ['QB3', 'QB1'], ['CR1', 'CR1'], ['CR1', 'QB3']] ) def test_disallowed_cz_qubits(sample_move_architecture, qubits, qubit_mapping): """ @@ -108,7 +111,7 @@ def test_disallowed_cz_qubits(sample_move_architecture, qubits, qubit_mapping): @pytest.mark.parametrize('qubit_mapping', [None, sample_qb_mapping]) -@pytest.mark.parametrize('qubits', [['QB3', 'COMP_R']]) +@pytest.mark.parametrize('qubits', [['QB3', 'CR1']]) def test_allowed_move_qubits(sample_move_architecture, qubits, qubit_mapping): """ Tests that instruction validation passes for allowed MOVE loci @@ -126,7 +129,7 @@ def test_allowed_move_qubits(sample_move_architecture, qubits, qubit_mapping): @pytest.mark.parametrize('qubit_mapping', [None, sample_qb_mapping]) @pytest.mark.parametrize( 'qubits', - [['QB1', 'QB2'], ['QB2', 'QB1'], ['QB1', 'QB1'], ['QB1', 'COMP_R'], ['COMP_R', 'COMP_R'], ['COMP_R', 'QB3']], + [['QB1', 'QB2'], ['QB2', 'QB1'], ['QB1', 'QB1'], ['QB1', 'CR1'], ['CR1', 'CR1'], ['CR1', 'QB3']], ) def test_disallowed_move_qubits(sample_move_architecture, qubits, qubit_mapping): """ @@ -155,7 +158,7 @@ def test_allowed_measure_qubits(sample_move_architecture, qubits): ) -@pytest.mark.parametrize('qubits', [['QB1', 'COMP_R'], ['COMP_R'], ['QB1', 'QB2', 'QB4'], ['QB4']]) +@pytest.mark.parametrize('qubits', [['QB1', 'CR1'], ['CR1'], ['QB1', 'QB2', 'QB4'], ['QB4']]) def test_disallowed_measure_qubits(sample_move_architecture, qubits): """ Tests that instruction validation fails for loci containing any qubits that are not valid measure qubits @@ -213,9 +216,9 @@ def test_same_measurement_key_in_different_circuits(sample_move_architecture): @pytest.mark.parametrize( 'qubits', [ - ['COMP_R', 'QB1', 'QB2', 'QB3'], - ['QB1', 'COMP_R', 'QB2', 'QB3'], - ['QB1', 'COMP_R', 'QB2'], + ['CR1', 'QB1', 'QB2', 'QB3'], + ['QB1', 'CR1', 'QB2', 'QB3'], + ['QB1', 'CR1', 'QB2'], ], ) def test_barrier(sample_move_architecture, qubits): @@ -252,8 +255,8 @@ def make_circuit_and_check( @pytest.mark.parametrize( 'instructions', [ - (Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}),), - (Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}),) * 3, + (Instruction(name='move', qubits=['QB3', 'CR1'], args={}),), + (Instruction(name='move', qubits=['QB3', 'CR1'], args={}),) * 3, ], ) def test_non_sandwich_move(self, sample_move_architecture, validate_moves, instructions): @@ -267,18 +270,18 @@ def test_non_sandwich_move(self, sample_move_architecture, validate_moves, instr @pytest.mark.parametrize('validate_moves', list(MoveGateValidationMode)) def test_move_sandwich(self, sample_move_architecture, validate_moves): """Valid pair of MOVEs.""" - move = Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}) + move = Instruction(name='move', qubits=['QB3', 'CR1'], args={}) TestMoveValidation.make_circuit_and_check((move, move), sample_move_architecture, validate_moves) @pytest.mark.parametrize('validate_moves', list(MoveGateValidationMode)) def test_bad_move_occupied_resonator(self, sample_move_architecture, validate_moves): """Moving a qubit state into an occupied resonator.""" - move = Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}) + move = Instruction(name='move', qubits=['QB3', 'CR1'], args={}) invalid_sandwich_circuit = Circuit( name='Move validation circuit', instructions=( move, - Instruction(name='move', qubits=['QB2', 'COMP_R'], args={}), + Instruction(name='move', qubits=['QB2', 'CR1'], args={}), ), # this MOVE locus is not in the architecture, but only checking MOVE validation ) if validate_moves != MoveGateValidationMode.NONE: @@ -298,12 +301,12 @@ def test_bad_move_occupied_resonator(self, sample_move_architecture, validate_mo @pytest.mark.parametrize('validate_moves', list(MoveGateValidationMode)) def test_bad_move_qubit_already_moved(self, sample_move_architecture, validate_moves): """Moving the state of a qubit which is already moved to another resonator.""" - move = Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}) + move = Instruction(name='move', qubits=['QB3', 'CR1'], args={}) invalid_sandwich_circuit = Circuit( name='Move validation circuit', instructions=( move, - Instruction(name='move', qubits=['QB3', 'COMP_R2'], args={}), + Instruction(name='move', qubits=['QB3', 'CR2'], args={}), ), # this MOVE locus is not in the architecture, but only checking MOVE validation ) if validate_moves != MoveGateValidationMode.NONE: @@ -330,7 +333,7 @@ def test_bad_move_qubit_already_moved(self, sample_move_architecture, validate_m (MoveGateValidationMode.STRICT,), ), ( - Instruction(name='cz', qubits=['QB2', 'COMP_R'], args={}), + Instruction(name='cz', qubits=['QB2', 'CR1'], args={}), (MoveGateValidationMode.STRICT, MoveGateValidationMode.ALLOW_PRX, MoveGateValidationMode.NONE), (), ), @@ -341,7 +344,7 @@ def test_gates_in_move_sandwich( ): """Only some gates can be applied on the qubit or resonator inside a MOVE sandwich.""" # pylint: disable=too-many-arguments - move = Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}) + move = Instruction(name='move', qubits=['QB3', 'CR1'], args={}) instructions = (move, gate, move) if validation_mode in disallowed_modes: with pytest.raises(CircuitValidationError, match=r'while the state\(s\) of (.+) are in a resonator'): @@ -362,7 +365,7 @@ def test_gates_in_move_sandwich( @pytest.mark.parametrize('validation_mode', list(MoveGateValidationMode)) def test_device_without_resonator(self, sample_dynamic_architecture, sample_circuit, validation_mode): """MOVEs cannot be used on a device that does not support them.""" - move = Instruction(name='move', qubits=['QB3', 'COMP_R'], args={}) + move = Instruction(name='move', qubits=['QB3', 'CR1'], args={}) with pytest.raises(CircuitValidationError, match="'move' is not supported"): TestMoveValidation.make_circuit_and_check((move,), sample_dynamic_architecture, validation_mode) # But validation passes if there are no MOVE gates @@ -373,11 +376,11 @@ def test_device_without_resonator(self, sample_dynamic_architecture, sample_circ @pytest.mark.parametrize('validation_mode', list(MoveGateValidationMode)) def test_qubit_mapping(self, sample_move_architecture, validation_mode): """Test that MOVE circuit validation works with an explicit qubit mapping given.""" - move = Instruction(name='move', qubits=[reverse_qb_mapping[qb] for qb in ['QB3', 'COMP_R']], args={}) + move = Instruction(name='move', qubits=[reverse_qb_mapping[qb] for qb in ['QB3', 'CR1']], args={}) prx = Instruction( name='prx', qubits=[reverse_qb_mapping[qb] for qb in ['QB3']], args={'phase_t': 0.3, 'angle_t': -0.2} ) - cz = Instruction(name='cz', qubits=[reverse_qb_mapping[qb] for qb in ['QB2', 'COMP_R']], args={}) + cz = Instruction(name='cz', qubits=[reverse_qb_mapping[qb] for qb in ['QB2', 'CR1']], args={}) TestMoveValidation.make_circuit_and_check( (move, move), sample_move_architecture, validation_mode, sample_qb_mapping ) @@ -385,7 +388,7 @@ def test_qubit_mapping(self, sample_move_architecture, validation_mode): (prx, move, cz, move), sample_move_architecture, validation_mode, sample_qb_mapping ) # qubit mapping without all qubits/resonators in the architecture - partial_qb_mapping = {k: v for k, v in sample_qb_mapping.items() if v in ['QB2', 'QB3', 'COMP_R']} + partial_qb_mapping = {k: v for k, v in sample_qb_mapping.items() if v in ['QB2', 'QB3', 'CR1']} TestMoveValidation.make_circuit_and_check( (move, move), sample_move_architecture, validation_mode, partial_qb_mapping )