From 3a860118327d3ce4a9e615d0d963db3a23ed42ba Mon Sep 17 00:00:00 2001 From: naezzell Date: Thu, 7 Jul 2022 18:23:12 -0700 Subject: [PATCH] First public commit just cloning development code over We needed to remove committ history as we hard coded various things whic we would prefer to keep private for now. To do this, we needed to make a fresh commit with just public code in a new public repo. --- CITATION.cff | 16 + README.md | 244 ++ requirements.txt | 4 + setup.py | 45 + setup_commands.txt | 59 + src/edd/__init__.py | 18 + src/edd/_version.py | 14 + src/edd/backend/__init__.py | 13 + src/edd/backend/_backend.py | 264 ++ src/edd/backend/_backend_test.py | 140 + src/edd/circuit/__init__.py | 13 + src/edd/circuit/_ibmq_circ.py | 1882 +++++++++++ src/edd/circuit/_ibmq_circ_test.py | 1510 +++++++++ src/edd/data/__init__.py | 13 + src/edd/data/_ibmq_data.py | 1145 +++++++ src/edd/data/_ibmq_data_test.py | 152 + src/edd/experiments/__init__.py | 20 + src/edd/experiments/_circuit_experiments.py | 585 ++++ .../experiments/_circuit_experiments_test.py | 314 ++ src/edd/experiments/_pulse_experiments.py | 931 ++++++ .../experiments/_pulse_experiments_test.py | 235 ++ src/edd/pulse/__init__.py | 13 + src/edd/pulse/_ibmq_pulse.py | 2840 +++++++++++++++++ src/edd/pulse/_ibmq_pulse_test.py | 746 +++++ src/edd/states/2q_gate_list.npy | Bin 0 -> 451690 bytes src/edd/states/u3_list.npy | Bin 0 -> 4928 bytes src/edd/workflow/__init__.py | 13 + src/edd/workflow/_workflow_utils.py | 2299 +++++++++++++ 28 files changed, 13528 insertions(+) create mode 100644 CITATION.cff create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 setup_commands.txt create mode 100644 src/edd/__init__.py create mode 100644 src/edd/_version.py create mode 100644 src/edd/backend/__init__.py create mode 100644 src/edd/backend/_backend.py create mode 100644 src/edd/backend/_backend_test.py create mode 100644 src/edd/circuit/__init__.py create mode 100644 src/edd/circuit/_ibmq_circ.py create mode 100644 src/edd/circuit/_ibmq_circ_test.py create mode 100644 src/edd/data/__init__.py create mode 100644 src/edd/data/_ibmq_data.py create mode 100644 src/edd/data/_ibmq_data_test.py create mode 100644 src/edd/experiments/__init__.py create mode 100644 src/edd/experiments/_circuit_experiments.py create mode 100644 src/edd/experiments/_circuit_experiments_test.py create mode 100644 src/edd/experiments/_pulse_experiments.py create mode 100644 src/edd/experiments/_pulse_experiments_test.py create mode 100644 src/edd/pulse/__init__.py create mode 100644 src/edd/pulse/_ibmq_pulse.py create mode 100644 src/edd/pulse/_ibmq_pulse_test.py create mode 100644 src/edd/states/2q_gate_list.npy create mode 100644 src/edd/states/u3_list.npy create mode 100644 src/edd/workflow/__init__.py create mode 100644 src/edd/workflow/_workflow_utils.py diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..08db148 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,16 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: "edd" +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Nic Ezzell + email: naezzell@proton.me + affiliation: University of Southern California +version: 1.0.0 +date-released: 2022-06-09 +url: "https://github.com/USCqserver/edd-dev" diff --git a/README.md b/README.md new file mode 100644 index 0000000..d988d94 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +# edd +Experimental dynamical decoupling (EDD) is a repo dedicated to testing out various DD sequences on NISQ-era quantum computers. Further details of what this means are contained in the associated paper: "Dynamical decoupling for superconducting qubits: a performance survey" by Nic Ezzell, Bibek Pokharel, Lina Tewala, Gregory Quiroz, and Daniel A. Lidar. + +In this README, we shall discuss (i) how to download and install edd (ii) the package structure and useage (iii) a dicussion of which parts of the repo are relevant to our paper and how to obtain/ analyze our data and (iv) some special cavetas/ considerations when using edd with IBM devices. + +## (i) How to download and install edd +Simply clone the repo in your preferred directory and run `pip install -e .` within your desired python virtual environment/ conda environment while located in the edd directory with the setup.py file. If this doesn't make sense to you, please refer to the more detailed instructions in "setup_commands.txt." + +## (ii) Package structure and usage +We modeled our package structure after `numpy`, but by no means did we try to make things "production" quality. In other words, don't be surprised to find defunct functions or weird conventions. + +### Summary of structure + +Anyway, all the source code is located in `src/edd`. This main directory has several sub-directories +- `backend` -- contains the `IBMQBackend` class for sending/receiving jobs from IBM +- `pulse` -- contains the `IBMQDdSchedule` class which inherits from `qiskit.pulse.Schedule` class. Contains the DD sequence definitions in terms of pulse instructions. Allows one to add DD sequences to it as methods. +- `circuit` -- contains `IBMQDdCircuit` class which inherits from qiskit.QuantumCircuit but otherwise should behave roughly the same as `pulse`. However, we ended up not using this class for the paper, so no promises that it's on par with `pulse` +- `experiments` -- contains scripts to run various experiments we were interested in at various points. For example, in `_pulse_experiments.py` we have a function called `pauli_pode_fid_decay_dd` which is the quantum memory experiment on the Pauli eigenstates. Also, `haar_fid_decay_dd` contains script to run the same for Haar random states. +- `data` -- contains `IBMQData` class which is initialized with job data from IBM devices and can re-format and summarize it with useful statistics +- `workflow` -- contains scripts useful for automating the job-submission, data extraction, and analysis for experiments used in the paper. For example, `gen_fds_on_fly_and_submit` was used to submit many circuits for this purpose. +- `states` -- contains "u3_list.npy" which encodes the u3 parameters which define the static set of Haar random states we test over + +### Import patterns +Basic usage follows a `numpy` like convention. For example, a typical set of imports looks like +``` +from edd.backend import IBMQBackend +from edd.pulse import IBMQDdSchedule +from edd.data import IBMQData +import edd.experiments as edde +import edd.workflow as wf +``` +### Loading in IBM Backend +When using the `IBMQDdSchedule` class, note that all pulses are defined relative to a backend, so you'll want to first load in a pulse-compatible backend. To do this, you can just run something like +``` +backend_name = "ibmq_example" +hub = "ibm-q-research-or-whatever" +group = "perhaps-your-uni" +project = "the-funds" +token = "your-token" +backend = IBMQBackend(backend, hub, group, project, token) +``` +where the hub, group, project, and token info can be obtained by going to `IBM quantum > account settings > providers`. Then click the three vertical dots and select `copy Qiskit provider code.` If typing this is annoys you, you can hardcode your account as an `__init__` option. An important functon to be aware of when running experiments is +``` +print(backend.get_readable_props_str()) +``` +which gives a human readable summary of the backend properties which can be saved to a txt file easily. For `jakarta`, for example, the result as of this tutorial are +``` +Backend Properties +--- +Experiment Date: 2022-07-07 12:06:44 +Backend Name: ibmq_jakarta +Version: 1.0.34 +Last Update Date: 2022-07-07 08:21:07-07:00 + +Gate Info +--- +name, gate_error(), gate_length(ns) +id, 0.0003199501648000584, 35.55555555555556 +id, 0.00020741584099435536, 35.55555555555556 +id, 0.00020747484772500605, 35.55555555555556 +id, 0.0002035022703507296, 35.55555555555556 +id, 0.0010525762631233982, 35.55555555555556 +id, 0.0002491638513245879, 35.55555555555556 +id, 0.00020354933384976543, 35.55555555555556 +rz, 0, 0 +rz, 0, 0 +rz, 0, 0 +rz, 0, 0 +rz, 0, 0 +rz, 0, 0 +rz, 0, 0 +sx, 0.0003199501648000584, 35.55555555555556 +sx, 0.00020741584099435536, 35.55555555555556 +sx, 0.00020747484772500605, 35.55555555555556 +sx, 0.0002035022703507296, 35.55555555555556 +sx, 0.0010525762631233982, 35.55555555555556 +sx, 0.0002491638513245879, 35.55555555555556 +sx, 0.00020354933384976543, 35.55555555555556 +x, 0.0003199501648000584, 35.55555555555556 +x, 0.00020741584099435536, 35.55555555555556 +x, 0.00020747484772500605, 35.55555555555556 +x, 0.0002035022703507296, 35.55555555555556 +x, 0.0010525762631233982, 35.55555555555556 +x, 0.0002491638513245879, 35.55555555555556 +x, 0.00020354933384976543, 35.55555555555556 +cx, 0.017376502926994886, 504.88888888888886 +cx, 0.017376502926994886, 540.4444444444445 +cx, 0.007410021627299507, 384 +cx, 0.007410021627299507, 419.55555555555554 +cx, 0.006437586645274163, 277.3333333333333 +cx, 0.006437586645274163, 312.88888888888886 +cx, 0.008834249450477588, 291.55555555555554 +cx, 0.008834249450477588, 327.1111111111111 +cx, 0.00980800779616306, 248.88888888888889 +cx, 0.00980800779616306, 284.44444444444446 +cx, 0.007000849427907713, 234.66666666666666 +cx, 0.007000849427907713, 270.22222222222223 +reset, 7342.222222222222 +reset, 7342.222222222222 +reset, 7342.222222222222 +reset, 7342.222222222222 +reset, 7342.222222222222 +reset, 7342.222222222222 +reset, 7342.222222222222 + +Qubit Info +--- +qubit, T1(us), T2(us), frequency(GHz), anharmonicity(GHz), readout_error(), prob_meas0_prep1(), prob_meas1_prep0(), readout_length(ns) +0, 179.27108623920228, 46.48443007226778, 5.236537333392189, -0.339883615358574, 0.03959999999999997, 0.05479999999999996, 0.0244, 5351.11111111111 +1, 136.6107664533625, 28.38193214382445, 5.014431945688961, -0.3432005583724651, 0.035599999999999965, 0.03739999999999999, 0.0338, 5351.11111111111 +2, 115.41307756584426, 26.005733303914802, 5.108468919342932, -0.3416150041672664, 0.024499999999999966, 0.0388, 0.010199999999999987, 5351.11111111111 +3, 130.6752912614701, 43.29522100023257, 5.178135251335165, -0.3411171247904715, 0.017800000000000038, 0.0268, 0.00880000000000003, 5351.11111111111 +4, 43.33845082911979, 50.386887681694404, 5.213062099531775, -0.3392533874360392, 0.1964999999999999, 0.29479999999999995, 0.0982, 5351.11111111111 +5, 69.68705534461373, 49.38499263027378, 5.063262326256089, -0.3412893561600795, 0.040100000000000025, 0.050799999999999956, 0.0294, 5351.11111111111 +6, 99.3803716167542, 23.117838725006226, 5.300667969846487, -0.3383638923290693, 0.049900000000000055, 0.0364, 0.06340000000000001, 5351.11111111111 +``` +which as you can see is fairly comprehensive. + +### Created a DD pulse Schedule and visualizing it + Now you can initialize a `IBMQDdSchedule` object, add DD pulses, and print the pulse sequence. For example, +``` +# basis is an advanced key option to change the way 'x' and 'y' pulses are defined. Default is 'g_basis' +# name is just a tag that shows up when printing the schedule and whatnot +dd_sched = IBMQDdSchedule(backend, basis_version = "g_basis", name = "github_example") +dd_sched.add_xy4(qubit = 1, num_reps = 2, d = 0, sym = False) +dd_sched.add_measurement(1, 0) +print(dd_sched.sched) +``` +which produces a result like +``` +Schedule((0, Play(Drag(duration=160, amp=(-0.004039903559079659+0.20185383960793365j), sigma=40, beta=-1.017901529566831, name='Y'), DriveChannel(1), name='Y')), (160, Play(Drag(duration=160, amp=(0.20187820062948367+0j), sigma=40, beta=-1.0153527787687813, name='X'), DriveChannel(1), name='X')), (320, Play(Drag(duration=160, amp=(-0.004039903559079659+0.20185383960793365j), sigma=40, beta=-1.017901529566831, name='Y'), DriveChannel(1), name='Y')), (480, Play(Drag(duration=160, amp=(0.20187820062948367+0j), sigma=40, beta=-1.0153527787687813, name='X'), DriveChannel(1), name='X')), (640, Play(Drag(duration=160, amp=(-0.004039903559079659+0.20185383960793365j), sigma=40, beta=-1.017901529566831, name='Y'), DriveChannel(1), name='Y')), (800, Play(Drag(duration=160, amp=(0.20187820062948367+0j), sigma=40, beta=-1.0153527787687813, name='X'), DriveChannel(1), name='X')), (960, Play(Drag(duration=160, amp=(-0.004039903559079659+0.20185383960793365j), sigma=40, beta=-1.017901529566831, name='Y'), DriveChannel(1), name='Y')), (1120, Play(Drag(duration=160, amp=(0.20187820062948367+0j), sigma=40, beta=-1.0153527787687813, name='X'), DriveChannel(1), name='X')), (1280, Acquire(22400, AcquireChannel(1), MemorySlot(0))), (1280, Play(GaussianSquare(duration=22400, amp=(0.135263552870952+0.14115247523414948j), sigma=64, width=22144, name='M_m1'), MeasureChannel(1), name='M_m1')), (23680, Delay(1680, MeasureChannel(1))), name="github_example") +``` +Pay special attention to the above notation of `dd_sched.sched` and the comment above it. To actually get the schedule object, we need to ask for it. In a way, the `IBMQDdSchedule` object is more of a wrapper than proper inheritance of `Schedule`, but again, this is just a technical detail to get changes to stay. In other words, you can continually add more pulses without previous additions getting thrown away. Anyway, the text is hard to understand, but luckily, you can invoke all the `Schedule` methods such as `draw`. So, +``` +dd_sched.draw() +``` +Screen Shot 2022-07-07 at 11 59 53 AM + +### Submitting the schedule as a job +At this point, you can submit the pulse schedule as a job using the `backend.submit_job` function like +``` +job = backend.submit_job(dd_sched, "qiskit_test", num_shots = 1000) +``` +When the job is finished (you can check on it with `job.status()` or by looing at the online queue), you can pass the result object to the data class to perform data analysis. For example, +``` +result = job.result() +data = IBMQData(result) +``` +However, the specific analysis we choose is only relevant to our partiuclar experiment. We will get into more details when we discuss how we used our package for our paper. But just for a glimpe into the ultimate simplicity (most of the complication is just keep track of which sequence/ paramters/ etc. were used), consider that +``` +counts = result.get_counts() +``` +gives the answer `counts = {'0': 981, '1': 19}` on this day which encodes an empirial Uhlmann fidelity of `981 / 1000 = 98.1%` which is consistent with expectations after merely applying 2 repetitions of XY4. + + + +## (iii) Relevant parts to paper +The job submission scripts, data analysis scripts, raw data, and machine calibration information is contained in a Zenodo project. If you are accessing the Github repo on Zenodo, it's the directory called "EDD_Paper_Data," otherwise, visit this link. Within the EDD_Paper_Data top level directory, there are data wrangling scripts, +- pauliData_wrangle.ipynb -- wrangle Pauli experiment data +- haarData_wrangle.ipynb -- wrangles Haar convergence experiment data +- haarDelayData_wrangle.ipynb -- wrangles Haar delay experiment data +- average_T1_and_T2.ipynb -- wrangles all machine calibration text dumps into an average T1 and T2 across all experiments for each device +as well as a directory for each device +- Armonk/ +- Bogota/ +- Jakarta/ +These device specific directories contain the experiment submission sripts, raw data, machine calibration files, and processed data. For example purpses, we will discuss the Armonk folder structure, but the other folders follow the same patterns. + +### Exploring Armonk submission scripts and data +Within the Armonk directory, there are three sub-directories +- PauliData -- contains Pauli experiment submission script `job_submit.ipynb`, data, analysis, and plots put in paper +- HaarData -- contains Haar convergence experiment submission script, data, analysis, and plots put in paper +- HaarDelayData -- contains Haar delay experiment submission script, data, analysis, and plots put in paper + +### Bottom line summary +We will discuss what each important file/ directory in the above PauliData, HaarData, and HaarDelayData is. But the bottom line is +- EDD_Paper_Data/Armonk/PauliData/csvPauliData contains the Pauli experiment fidelity decay data in CSV format: `T, empirical Uhlmann fidelity, 2 sigma bootstrapped error on fidelity` +- EDD_Paper/Data/Armonk/PauliData/armonkPauliAnalysis.nb is the Mathematica script we used to generate publication plots +- EDD_Paper_Data/Armonk/HaarDelayData/csvHaarFreeData contains the Haar delay experiment data in CSV format: `d, empirical Uhlmann fidelity, 2 sigma bootstrapped error on fidelity` +- EDD_Paper_Data/Armonk/HaarDelayData/ArmonkHaarDelayDataAnalysis.nb is the Mathematica script we used to generate publication plots + + +#### The PauliData folder +The PauliData folder consists of +- job_submit.ipynb -- job submission script +- rawPauliData/ -- folder containing raw Pauli experiment data along with machine calibration files +- csvPauliData/ -- folder containing summary statistics of Pauli data as CSV files with three columns: T, fid, 2 \sigma bootstrapped err. Files are descriptively named like `fidDecay_backend_armonk_qubit_0_basis_g_goodc_False_T1_142.83_T2_236.68_dtype_min_offset_0.0_seq_qdd-2-7_sym_False_pode_3_date_2021-09-23 23:59:24-07:00.csv` +- armonkPauliAnalysis.nb -- Mathematica script to visualze data/ make plots +- armonkPauliAnalysis_figures/ -- folder containing plots we made several of which appear in the paper +- armonkPauliDataDict -- folder containing a compressed, re-loadable (think Python pickle of a dictionary or JSON file) Association containing Armonk Pauli data for loading into armonkPauliAnalysis.nb +- armonkPauliIntDict -- same as DataDict just above but with Hermite polynomial interpolations of each fidelity decay + +### The HaarDelayData and HaarDelay folders +The structure of these two folders is very similar to the PauliData folder, so we do not re-iterate things here. + +## (iv) Details for experts/ gotchas for those who want to reproduce results +If you are an IBM expert or someone interested in reproducing our results down to a T, then you will want to be aware of a few specific details regarding our code which we discuss now. + +#### The choice of "pulse basis" +The "traditional way" (funny to say this) to think about using NISQ devices is in terms of unitary logic gates in a quantum circuit. Note, however, that one will be unable to reproduce all our results by using the `QuantumCircuit` class alone. The empirical difference was discussed in "Appendix B: Circuit vs OpenPulse APIs" in our paper, so please check this out first. Another way to see the difference is to define a common sequence like XY4 with our method and then compare it to the standard circuit derived method: +Screen Shot 2022-07-07 at 3 23 57 PM + +The "g_basis" is a custom basis choice whereas "c_basis" corresponds to the process of defining XY4 with the QuantumCircuit API, transpiling it into native gates, and using the qiskit `build_schedule` function to construct the corresponding `Schedule` object. In other words, the "c_basis" is the following: +Screen Shot 2022-07-07 at 3 32 21 PM +which again yields Y in terms of X and virtual Z. + +The code to generate the "g_basis" is contained in `src/edd/pulse/_ibmq_pulse.py` in the function `create_from_greg_basis`. First, we define `x` pulses in exactly the same way as they are defined as an X gate. But rather than define Y in terms of X and virtual Y, we define it from the Y used in the now deprecated `u2` pulse, +``` +y90 = defaults.get('u2', qubit, P0=0, P1=0).instructions[index][1].pulse +dur = y90.parameters['duration'] +amp = y90.parameters['amp'] +sigma = y90.parameters['sigma'] +beta = y90.parameters['beta'] +y180 = Drag(dur, 2*amp, sigma, beta, name='Y') +``` +where Drag is a type of pulse specification defined in . In addition, we can define rotations about axis between X and Y by simply rotating the amplitude fo the Drag pulse, i.e. +``` +def rotate(complex_signal: np.ndarray, delta_phi: float) -> np.ndarray: + '''Adds a phase to a complex signal.''' + phi = np.angle(complex_signal) + delta_phi + return np.abs(complex_signal) * np.exp(1j*phi) + +x30 = Drag(dur, rotate(amp, np.pi/6), sigma, beta, name='X30') +x120 = Drag(dur, rotate(amp, (2*np.pi)/3), sigma, beta, name='X120') +``` +where `x30` is a `Pi` pulse rotating about the axis 30 degrees from the X axis counter-clockwise. These rotated pulses are important in defining the `UR`, for example. + +This may seem like an unusual choice, but we justify it in the aforementioned appendix. + + +#### A delay instruction gotchI +If you are an NISQ experimentalist, you will likely be familiar with a so-called "acquire constraint." To explain this in IBM terms, let's first introduce the notion of IBM's `dt`. Basically, `dt` is the smallest unit of time for which a pulse wave-form can be defined. In other worse, an X pulse--in an digital to analog kind of way--consists of an ampltiude value at `t = 0, dt, 2 dt, ..., 160dt`, as it's width is `160dt`. In the above pulse plots, for example, the total evolution time was listed in this `dt` and was `160 * 4 = 640 dt`. One can acquire `dt` in nano-seconds for their given backend using +``` +# evaluates to 0.222222 currently +dt = backend.get_dt("ns") +``` +Now, even though pulses are defined in increments in `dt`, the total sum of all wave-forms must actually be a multiple of `16dt` which is known as the acquire constraint. To query what the acquire constraint while using the device, use +``` +### evaluates to 16 right now +backend.get_acquire_alignment() +``` + +As far as I can tell, this is not a well documented constraint of devices, as using `OpenPulse` is still fairly new. This has important implications for how to run DD experiments when varying the pulse interval. In short, the total interval delays (the free evolution periods between pulses) must also be a mutltiple of 16. Not respecting this constraint will lead to vary biare results as we described in a note in an early draft (but redacted for being too int he weeds) which may help clarify using equations/ empirical results: +Screen Shot 2022-07-07 at 3 55 40 PM diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3c90497 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +qiskit +jupyterlab +matplotlib +pyyaml diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fc39c7d --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import os + +from setuptools import setup, find_packages + +# read the __version__ variable from nisqai/_version.py +exec(open("src/edd/_version.py").read()) + +# readme file as long description +long_description = ("======\n" + + "EDD\n" + + "======\n") +stream = io.open("README.md", encoding="utf-8") +stream.readline() +long_description += stream.read() + +# read in requirements.txt +requirements = open("requirements.txt").readlines() +requirements = [r.strip() for r in requirements] + +setup( + name="edd", + version=__version__, + author="Nic Ezzell, Bruno Avritzer, Bibek Pokharel, Vinay Tripathi", + author_email="nezzell@usc.edu", + url="https://github.com/naezzell/edd-dev", + description="Library for implementing dynamical decoupling schemes on NISQ-era quantum computers.", + long_description=long_description, + install_requires=requirements, + license="Apache 2", + packages=find_packages(where="src"), + package_dir={"": "src"} + ) diff --git a/setup_commands.txt b/setup_commands.txt new file mode 100644 index 0000000..7dfa991 --- /dev/null +++ b/setup_commands.txt @@ -0,0 +1,59 @@ +******** basic repo set up/ work flow guide ******** + +1. git clone git@github.com:naezzell/gadd.git +--> if you don't have ssh keys set up with github: https://help.github.com/en/enterprise/2.15/user/articles/adding-a-new-ssh-key-to-your-github-account + +** This will create a directory called gadd. I recommend renaming it to your desired feature name, i.e. gadd-ibmsim if working on ibm simulator + +2. git checkout dev +** this makes it so you are on dev branch instead of master + +3. $ git checkout -b myfeature dev +** this creates a local branch called myfeature which forks off of dev + +4. conda create -n gadd +** creates a conda environment called gadd + +5. conda activate gadd +** activates gadd environment + +--> now make sure you are in gadd directory with setup.py file + +6. pip install -e . +** this builds the gadd package to your conda env while installing necessary dependencies like qiskit along the way +** building the package let's commands like: + from gadd import ibmsim +work from any directory so as long as your conda env is active + +7. make code changes +** DO NOT make changes to same files at once. This causes merge conflicts. That's why we have feature branches... only work on feature that branch is related to. + +8. git commit +** as you add features to myfeature, commit them and add comments regularly + +-->Once you're satisfied with your feature, you can merge it to dev + +9. git checkout dev +** puts you back on local copy of dev branch which is hosted remotely + +10. git fetch origin dev +** this fetches any changes pushed to dev by other users (i.e. updates your local dev with remote changes) + +11. git merge --no-ff myfeature +** merges changes on 'myfeature' branch to 'dev' branch locally +** btw, no-ff flag causes merge to always make new commit object which prevents loss of historical info about feature branch + +--> after you are done with features, you want to delete local branch before pushing merged features to remote +--> this makes cleaner commit log history +12. git branch -d myfeature + +13. git push origin dev +** this pushes altered features to remote + +************ links to useful guides/ stackoverflow posts ************ +1. https://nvie.com/posts/a-successful-git-branching-model/ +2. https://stackoverflow.com/questions/15838192/best-way-to-manage-local-feature-branches-with-git +3. https://alex.dzyoba.com/blog/python-import/ +4. https://stackoverflow.com/questions/49474575/how-to-install-my-own-python-module-package-via-conda-and-watch-its-changes +5. https://help.github.com/en/enterprise/2.15/user/articles/adding-a-new-ssh-key-to-your-github-account +6. https://help.github.com/en/github/using-git/getting-changes-from-a-remote-repository diff --git a/src/edd/__init__.py b/src/edd/__init__.py new file mode 100644 index 0000000..9883044 --- /dev/null +++ b/src/edd/__init__.py @@ -0,0 +1,18 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import edd.backend +import edd.circuit +import edd.experiments +import edd.data + +from ._version import __version__ diff --git a/src/edd/_version.py b/src/edd/_version.py new file mode 100644 index 0000000..4339a14 --- /dev/null +++ b/src/edd/_version.py @@ -0,0 +1,14 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Define version number here and read it from setup.py automatically.""" +__version__ = "0.0.1" diff --git a/src/edd/backend/__init__.py b/src/edd/backend/__init__.py new file mode 100644 index 0000000..c85a6ae --- /dev/null +++ b/src/edd/backend/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.backend._backend import IBMQBackend diff --git a/src/edd/backend/_backend.py b/src/edd/backend/_backend.py new file mode 100644 index 0000000..e29527d --- /dev/null +++ b/src/edd/backend/_backend.py @@ -0,0 +1,264 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import random +import numpy as np +from qiskit import IBMQ, execute, Aer, assemble +from qiskit.providers.ibmq.managed import IBMQJobManager +from qiskit.providers.aer.noise import NoiseModel + +# type check from IBMQDdSchedule +from edd.pulse import IBMQDdSchedule + +class IBMQBackend(): + """ + This is backend. + """ + def __init__(self, strname, hub, group, project, token): + + """ + Loads backend with [strname]. + """ + IBMQ.save_account(hub=hub, group=group, project=project, token=token, overwrite=True) + self.provider = IBMQ.load_account() + self.backend = self.provider.get_backend(strname) + now = datetime.datetime.now() + print("IBM Backend {} loaded at date/time: {}".format(strname, now.strftime("%Y-%m-%d %H:%M:%S"))) + # now get properties of device and save as member data + self.config = self.backend.configuration().to_dict() + if strname != 'ibmq_qasm_simulator': + self.props = self.backend.properties().to_dict() + self.gate_props = self.backend.properties()._gates + else: + self.props = "N/A" + self.gate_props = "N/A" + + return + + def change_backend(self, strname): + """ + Loads [strname] backend. Replaces current backend in doing so. + """ + self.provider = IBMQ.load_account() + oldname = self.config['backend_name'] + self.backend = self.provider.get_backend(strname) + now = datetime.datetime.now() + print("Old backend {} switched to {} at date/time {}".format( + oldname, strname, now.strftime("%Y-%m-%d %H:%M:%S"))) + + # now get properties of device and save as member data + self.config = self.backend.configuration().to_dict() + self.props = self.backend.properties().to_dict() + self.gate_props = self.backend.properties()._gates + + return + + def get_remaining_jobs_count(self): + """ + Get number of remaining jobs to run. + """ + return self.backend.remaining_jobs_count() + + def get_backend_config_dict(self): + return self.config + + def get_backend_props_dict(self): + return self.props + + def get_dt(self, unit): + """ + Get inverse sampling rate of backend in [unit]. + """ + dt = self.backend.configuration().dt + + if unit == 's': + dt = dt + elif unit == 'ns': + dt = dt * 1e9 + else: + e = f"Unit {unit} not supported." + raise ValueError(e) + + return dt + + def get_acquire_alignment(self): + """ + Instructions must be a multiple of acquire_alignment + for a measurement to make sense. + """ + return self.config['timing_constraints']['acquire_alignment'] + + def get_readable_props_str(self): + p = self.get_backend_props_dict() + now = datetime.datetime.now() + now = now.strftime("%Y-%m-%d %H:%M:%S") + """Returns backend props info in human-readable string.""" + prop_str = "Backend Properties\n" + prop_str += "---\n" + prop_str += f"Experiment Date: {now}\n" + prop_str += f"Backend Name: {p['backend_name']}\n" + prop_str += f"Version: {p['backend_version']}\n" + prop_str += f"Last Update Date: {p['last_update_date']}\n" + + prop_str += "\nGate Info\n" + prop_str += "---\n" + # add gate header information + gate_header = "name" + for gate_params in p['gates'][0]['parameters']: + gate_header += f", {gate_params['name']}({gate_params['unit']})" + gate_header += "\n" + prop_str += gate_header + # add info corresponding to header info for each gate + for gate in p['gates']: + gate_info = f"{gate['gate']}" + for param in gate['parameters']: + gate_info += ", " + gate_info += f"{param['value']}" + prop_str += (gate_info + "\n") + + prop_str += "\nQubit Info\n" + prop_str += "---\n" + # add qubit header information + qubit_header = "qubit" + for qubit_params in p['qubits'][0]: + qubit_header += f", {qubit_params['name']}({qubit_params['unit']})" + qubit_header += "\n" + prop_str += qubit_header + # add info corresponding to header info for each gate + for qubit, qubit_params in enumerate(p['qubits']): + qubit_info = str(qubit) + for param in qubit_params: + qubit_info += ", " + qubit_info += f"{param['value']}" + prop_str += (qubit_info + "\n") + + return prop_str + + def save_backend_props(self, fname): + prop_str = self.get_readable_props_str() + with open(fname, 'w+') as f: + f.write(prop_str) + return + + def get_number_qubits(self): + """Outputs the number of qubits on this backend.""" + n_qubits = self.config['n_qubits'] + return n_qubits + + def get_native_gates(self): + """Outputs gates native to this backend.""" + nat_gates = self.config['basis_gates'] + return nat_gates + + def get_gate_times(self, avg=True): + """ Acquires gate application times in ns. If avg is True, returns + the average time it takes gate to apply across all qubits. If False, + returns times of gate for each qubit. """ + + # first get the native gate set + nat_gates = self.config['basis_gates'] + gate_to_time = {} + for gate in nat_gates: + gate_info = self.gate_props[gate] + # iterate over qubits the gate is defined over and check if + # gate times are all the same + times = [] + for qubit in gate_info: + times.append(gate_info[qubit]['gate_length'][0] * 1e9) + if avg is True: + gate_to_time[gate] = np.mean(times) + else: + gate_to_time[gate] = times + + return gate_to_time + + + def get_max_runs(self): + """ + Given the backend, returns the max # experiments and max # shots + that can be queued in a single job. + """ + + max_experiments = self.config['max_experiments'] + max_shots = self.config['max_shots'] + max_runs = {'max_experiments': max_experiments, 'max_shots': max_shots} + return max_runs + + + def submit_job(self, experiments, qobj_id, num_shots='max', + shuffle=False): + """ + Submit [experiments] to [backend] and run [num_shots] for each of + the [experiments]. Results get a qobj_id tag labelled with [qobj_id]. + If [shuffle] is True, randomly shuffles input data before sending to + prevent biasing data with time. + """ + # set runs to max runs allowed by hardware if set to do so + max_runs = self.get_max_runs() + if str(num_shots).lower() == 'max': + num_shots = max_runs['max_shots'] + + if not isinstance(experiments, list): + experiments = [experiments] + + # parse experiments a bit before passing to assemble + parsed_experiments = [] + for exp in experiments: + if isinstance(exp, IBMQDdSchedule): + parsed_experiments.append(exp.sched) + else: + parsed_experiments.append(exp) + + # shuffle data if desired + if shuffle is True: + random.shuffle(parsed_experiments) + + # submit the job in the background and output status information + program = assemble(parsed_experiments, backend=self.backend, + shots=num_shots, qobj_id=qobj_id) + job = self.backend.run(program) + return job + + def get_noisemodel(self): + """ + Given a backend, loads in the necessary information to run a noisy + simulation emulating noise on actual device. + """ + # set-up noise model for simulator + noise_model = NoiseModel.from_backend(self.backend) + # Get coupling map from backend + coupling_map = self.config['coupling_map'] + # Get basis gates from noise model + basis_gates = noise_model.basis_gates + noise_info = {'noise_model': noise_model, 'coupling_map': coupling_map, 'basis_gates': basis_gates} + + return noise_info + + def submit_test(self, experiments, qobj_id, num_shots = 1000): + """ + Submits [experiments] to simulator with noisemodel of [backend] and + runs [num_shots] for each + """ + # set up noise model of backend + noise_info = self.get_noisemodel() + coupling_map = noise_info['coupling_map'] + basis_gates = noise_info['basis_gates'] + noise_model = noise_info['noise_model'] + + job = execute(experiments, Aer.get_backend('qasm_simulator'), + coupling_map=coupling_map, + basis_gates=basis_gates, + noise_model=noise_model, + shots=num_shots, qobj_id=qobj_id) + + return job.result() diff --git a/src/edd/backend/_backend_test.py b/src/edd/backend/_backend_test.py new file mode 100644 index 0000000..0a675c5 --- /dev/null +++ b/src/edd/backend/_backend_test.py @@ -0,0 +1,140 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from edd.backend import IBMQBackend + +import unittest + +# there may be need for unit testing in future, so this is here for that +class BackendTest(unittest.TestCase): + """ Units tests for IBMQBackend. """ + + ################################################## + # Armonk Tests + ################################################## + def test_init_armonk(self): + armonk = IBMQBackend("ibmq_armonk") + self.assertIsInstance(armonk, IBMQBackend) + + def test_get_number_qubits_armonk(self): + armonk = IBMQBackend("ibmq_armonk") + n_qubits = armonk.get_number_qubits() + self.assertEqual(n_qubits, 1) + + def test_get_native_gates_armonk(self): + armonk = IBMQBackend("ibmq_armonk") + native_gates = armonk.get_native_gates() + manual_natives = ['id', 'u1', 'u2', 'u3'] + self.assertCountEqual(native_gates, manual_natives) + + def test_get_gate_times_armonk(self): + armonk = IBMQBackend("ibmq_armonk") + native_gates = armonk.get_native_gates() + # get the first gate since all single qubit anyway + f_g = native_gates[0] + # get time it takes to run gate on average over the qubits + avg_times = armonk.get_gate_times() + f_g_time = avg_times[f_g] + self.assertIsInstance(f_g_time, float) + + # get times for gate on each qubit + n_qubits = armonk.get_number_qubits() + all_times = armonk.get_gate_times(avg=False) + f_g_times = all_times[f_g] + self.assertEqual(len(f_g_times), n_qubits) + + def test_get_readable_props_str_armonk(self): + armonk = IBMQBackend("ibmq_armonk") + prop_str = armonk.get_readable_props_str() + print(prop_str) + self.assertIsInstance(prop_str, str) + + def test_get_max_runs_armonk(self): + armonk = IBMQBackend("ibmq_armonk") + max_runs = armonk.get_max_runs() + max_experiments = max_runs['max_experiments'] + max_shots = max_runs['max_shots'] + self.assertIsInstance(max_experiments, int) + self.assertIsInstance(max_shots, int) + + def test_get_noisemodel(self): + armonk = IBMQBackend("ibmq_armonk") + noise_info = armonk.get_noisemodel() + self.assertIsInstance(noise_info, dict) + + ################################################## + # TODO: Write Tests of Basic Circuits and Actually Run them on hardware + ################################################## + + + ################################################## + # Ourense Tests + ################################################## + def test_init_ourense(self): + ourense = IBMQBackend("ibmq_ourense") + self.assertIsInstance(ourense, IBMQBackend) + + def test_get_number_qubits_ourense(self): + ourense = IBMQBackend("ibmq_ourense") + n_qubits = ourense.get_number_qubits() + self.assertEqual(n_qubits, 5) + + def test_get_native_gates_ourense(self): + ourense = IBMQBackend("ibmq_ourense") + native_gates = ourense.get_native_gates() + manual_natives = ['id', 'u1', 'u2', 'u3', 'cx'] + self.assertCountEqual(native_gates, manual_natives) + + def test_get_gate_times_ourense(self): + ourense = IBMQBackend("ibmq_ourense") + native_gates = ourense.get_native_gates() + # get the first gate since all single qubit anyway + f_g = native_gates[0] + # get time it takes to run gate on average over the qubits + avg_times = ourense.get_gate_times() + f_g_time = avg_times[f_g] + self.assertIsInstance(f_g_time, float) + + # get times for gate on each qubit + n_qubits = ourense.get_number_qubits() + all_times = ourense.get_gate_times(avg=False) + f_g_times = all_times[f_g] + self.assertEqual(len(f_g_times), n_qubits) + + def test_get_readable_props_str_ourense(self): + armonk = IBMQBackend("ibmq_ourense") + prop_str = armonk.get_readable_props_str() + print(prop_str) + self.assertIsInstance(prop_str, str) + + def test_get_max_runs_ourense(self): + armonk = IBMQBackend("ibmq_ourense") + max_runs = armonk.get_max_runs() + max_experiments = max_runs['max_experiments'] + max_shots = max_runs['max_shots'] + self.assertIsInstance(max_experiments, int) + self.assertIsInstance(max_shots, int) + + def test_get_noisemodel(self): + ourense = IBMQBackend("ibmq_ourense") + noise_info = ourense.get_noisemodel() + self.assertIsInstance(noise_info, dict) + + ################################################## + # TODO: Write Tests of Basic Circuits and Actually Run them on hardware + ################################################## + + +if __name__ == "__main__": + unittest.main() + diff --git a/src/edd/circuit/__init__.py b/src/edd/circuit/__init__.py new file mode 100644 index 0000000..0c2af22 --- /dev/null +++ b/src/edd/circuit/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.circuit._ibmq_circ import IBMQDdCircuit diff --git a/src/edd/circuit/_ibmq_circ.py b/src/edd/circuit/_ibmq_circ.py new file mode 100644 index 0000000..43a343b --- /dev/null +++ b/src/edd/circuit/_ibmq_circ.py @@ -0,0 +1,1882 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from qiskit import (QuantumCircuit, IBMQ, execute, transpile, + schedule as build_schedule, Aer) +from qiskit.converters import circuit_to_dag +from qiskit.quantum_info import random_unitary +from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer + + +class IBMQDdCircuit(QuantumCircuit): + """ + DdCircuit is class for Dynamical Decoupling Circuits. + Inherits all mehods/ valid data from qiskit's QuantumCircuit + """ + + def __init__(self, *args, name=None, ibmq_backend=None, mode='circ'): + super(IBMQDdCircuit, self).__init__(*args, name=name) + self.ibmq_backend = ibmq_backend + self.gate_dict = None + + ################################################## + # General Useful Utility Methods + ################################################## + def link_with_backend(self, ibmq_backend): + """ + Links self with [ibmq_backend] which changes the default + behavior of adding gates by converting all gates to + native gates for [backend] before adding from then on. + Also allows for get_transpiled_circ method to work. + """ + self.ibmq_backend = ibmq_backend + return + + def predefine_common_gates(self, mode='circ'): + """ + Given that we're linked to ibmq_backend, it's useful + to pre-define the common gates (i.e. X, Y, etc...) + a single time and not have to transpile each time. + This predefines them. + """ + gates = ['X', 'Y', 'Z', 'Xb', 'Yb'] + gate_dict = {} + for g in gates: + # init circ + if self.num_clbits == 0: + circ = IBMQDdCircuit(self.num_qubits) + else: + circ = IBMQDdCircuit(self.num_qubits, self.num_clbits) + # link with backend + circ.link_with_backend(self.ibmq_backend) + # make gate g for each qubit q + for q in range(self.num_qubits): + if g == 'X': + circ.add_x(q) + t_circ = circ.get_transpiled_circ() + re_circ = replace_sx_sx_with_x(t_circ) + key = f"{g}{q}" + if mode == 'circ': + gate_dict[key] = re_circ + else: + gate_dict[key] = re_circ.qasm() + if g == 'Y': + circ.add_y(q) + t_circ = circ.get_transpiled_circ() + re_circ = replace_sx_sx_with_x(t_circ) + key = f"{g}{q}" + if mode == 'circ': + gate_dict[key] = re_circ + else: + gate_key[key] = re_circ.qasm() + if g == 'Z': + circ.add_z(q) + t_circ = circ.get_transpiled_circ() + re_circ = replace_sx_sx_with_x(t_circ) + key = f"{g}{q}" + if mode == 'circ': + gate_dict[key] = re_circ + else: + gate_key[key] = re_circ.qasm() + if g == 'Xb': + circ.add_xb(q) + t_circ = circ.get_transpiled_circ() + re_circ = replace_sx_sx_with_x(t_circ) + key = f"{g}{q}" + if mode == 'circ': + gate_dict[key] = re_circ + else: + gate_key[key] = re_circ.qasm() + if g == 'Yb': + circ.add_yb(q) + t_circ = circ.get_transpiled_circ() + re_circ = replace_sx_sx_with_x(t_circ) + key = f"{g}{q}" + if mode == 'circ': + gate_dict[key] = re_circ + else: + gate_key[key] = re_circ.qasm() + + self.gate_dict = gate_dict + return + + + + def get_transpiled_circ(self, scheduling_method=None): + """ Returns transpiled circ provided linked with backend """ + try: + return transpile(self, self.ibmq_backend.backend, scheduling_method=scheduling_method) + except AttributeError: + e = "ibmq_backend not defined. Try self.link_with_backend(backend)." + raise AttributeError(e) + + def get_gate_count(self, trans=False, ibmq_backend=None): + """Returns dictionary with which gates are in circuit and + how many of them there are.""" + circ = self.copy() + if trans: + circ = transpile(circ, ibmq_backend.backend) + circ_dag = circuit_to_dag(circ) + gate_count = circ_dag.count_ops() + + return gate_count + + + def get_phys_time(self, ibmq_backend='linked'): + """Given an IBMQBackend called [backend], returns the total + run_time of this circuit in ns. Substracts any gates in [sub] + along with their count, i.e. if state prep is 2 u3s, then + sub = {'u3': 2} and this would reduce phys_time by the time of + 2 u3 gates. + + NOTE: This assumes circuit is written in terms of native gates, but + the methods we've developed do this automatically. + """ + if ibmq_backend == 'linked': + ibmq_backend = self.ibmq_backend + + # cast as schedule + dt = ibmq_backend.backend.configuration().dt + sched = build_schedule(self, ibmq_backend.backend) + dt_duration = sched.duration + # convert time to nano-seconds + phys_time = dt_duration * dt * 10**9 + + return phys_time + + + def get_statevector(self): + """ + Returns statevector obtained by running the circuit assuming no + noise. + """ + job = execute(self, Aer.get_backend('statevector_simulator')) + state_results = job.result() + return state_results.results[0].data.statevector + + def get_unitary(self): + """ + Gets unitary equivalent of running the circuit with no noise. + """ + job = execute(self, Aer.get_backend('unitary_simulator')) + unitary_results = job.result() + return unitary_results.results[0].data.unitary + + + ################################################## + # Methods that add gates of interest + ################################################## + def add_rn(self, theta, phi, alpha, qubits): + """ + Appends R_n(\alpha) gate to [qubits]. + n = (theta, phi) is unit vector and \alpha is + rotation angle about n-axis. + """ + if self.ibmq_backend is None: + self.rz(-phi, qubits) + self.ry(-theta, qubits) + self.rz(alpha, qubits) + self.ry(theta, qubits) + self.rz(phi, qubits) + else: + if self.num_clbits == 0: + circ = IBMQDdCircuit(self.num_qubits) + circ.add_rn(theta, phi, alpha, qubits) + circ.link_with_backend(self.ibmq_backend) + t_circ = circ.get_transpiled_circ() + #replace_circ = replace_sx_sx_with_x(t_circ) + #self.extend(replace_circ) + self.extend(t_circ) + else: + circ = IBMQDdCircuit(self.num_qubits, self.num_clbits) + circ.add_rn(theta, phi, alpha, qubits) + circ.link_with_backend(self.ibmq_backend) + t_circ = circ.get_transpiled_circ() + replace_circ = replace_sx_sx_with_x(t_circ) + #self.extend(replace_circ) + self.extend(t_circ) + return + + def add_rx(self, alpha, qubits): + """ + Appends R_X(\alpha) gate to [qubits]. + """ + self.add_rn(np.pi/2, 0, alpha, qubits) + + def add_ry(self, alpha, qubits): + """ + Appends R_Y(\alpha) gate to [qubits]. + """ + self.add_rn(np.pi/2, np.pi/2, alpha, qubits) + + def add_rz(self, alpha, qubits): + """ + Appends R_Z(\alpha) gate to [qubits]. + """ + self.add_rn(0, 0, alpha, qubits) + + def add_pi_eta(self, eta, qubits): + """ + Appends (\pi)_{\eta} gate to [qubits]. + \eta is angle from pos x axis, so pi rotation about \eta. + """ + self.add_rn(np.pi/2, eta, np.pi, qubits) + return + + def add_x(self, qubits): + """ + Appends X gate to circuit on [qubits]. + """ + if self.gate_dict is not None: + key = f"X{qubits}" + self.extend(self.gate_dict[key]) + else: + self.add_pi_eta(0, qubits) + return + + def add_y(self, qubits): + """ + Appends Y gate to circuit on [qubits]. + """ + if self.gate_dict is not None: + key = f"Y{qubits}" + self.extend(self.gate_dict[key]) + else: + self.add_pi_eta(np.pi / 2, qubits) + return + + def add_xb(self, qubits): + """ + Appends Xb gate to circuit on [qubits]. + """ + if self.gate_dict is not None: + key = f"Xb{qubits}" + self.extend(self.gate_dict[key]) + else: + self.add_pi_eta(np.pi, qubits) + return + + def add_yb(self, qubits): + """ + Appends Yb gate to circuit on [qubits]. + """ + if self.gate_dict is not None: + key = f"Yb{qubits}" + self.extend(self.gate_dict[key]) + else: + self.add_pi_eta(3*np.pi / 2, qubits) + return + + def add_z(self, qubits): + """ + Appends (virutal) Z gate to circuit on [qubits]. + """ + if self.gate_dict is not None: + key = f"Z{qubits}" + self.extend(self.gate_dict[key]) + else: + self.add_rn(0, 0, np.pi, qubits) + return + + def add_zb(self, qubits): + """ + Appends (virutal) Zb gate to circuit on [qubits]. + """ + if self.gate_dict is not None: + key = f"Z{qubits}" + self.extend(self.gate_dict[key]) + else: + self.add_rn(0, 0, -np.pi, qubits) + return + + def add_zii(self, qubits): + """ + Appends (virtual Z gate to ciruit on [qubits] followed by + two identities. + """ + self.add_z(qubits) + self.barrier(qubits) + self.id(qubits) + self.barrier(qubits) + self.id(qubits) + + def add_error_mitigation_0(self, qubits): + """ + Adds measurement error mitigation schedule for |0>, + Id-Measure. + """ + self.id(qubits) + self.barrier(qubits) + self.id(qubits) + self.barrier(qubits) + self.measure(qubits, qubits) + return + + def add_error_mitigation_1(self, qubits): + """ + Adds measurement error mitigation schedule for |1>, + X-Measure. + """ + self.add_x(qubits) + self.barrier(qubits) + self.measure(qubits, qubits) + return + + def add_measurement(self, qubits): + """ + Adds measurement from [qubits] to [qubits]. + """ + if mode == 'circ': + self.measure(qubits, qubits) + else: + if self.num_clbits == 0: + circ = IBMQDdCircuit(self.num_qubits) + circ.measure(qubits, qubits) + circ.link_with_backend(self.ibmq_backend) + t_circ = circ.get_transpiled_circ() + replace_circ = replace_sx_sx_with_x(t_circ) + self.extend(replace_circ) + #self.extend(t_circ) + else: + circ = IBMQDdCircuit(self.num_qubits, self.num_clbits) + circ.add_rn(theta, phi, alpha, qubits) + circ.link_with_backend(self.ibmq_backend) + t_circ = circ.get_transpiled_circ() + replace_circ = replace_sx_sx_with_x(t_circ) + self.extend(replace_circ) + #self.extend(t_circ) + + gate_key[key] = re_circ.qasm() + + ################################################## + # State Prep and Decoding Methods + ################################################## + def encode_podal_state(self, qubits, pole=2, offset=0): + """ + Assuming you are start in the |0> state, prepares a podal + state--essentially a variation of Pauli-states. If offset=0, + this method prepares the following states based on [pole] + pole = 0 --> |0> + pole = 1 --> |1> + pole = 2 --> |+> + pole = 3 --> |-> + pole = 4 --> |+i> + pole = 5 --> |-i> + [offset] sets a systematic tilt to all states in same direction. + In particular, when [offset] = pi / 2, we get + |0> --> |+> + |1> --> |-> + |+> --> |+i> + |-> --> |-i> + |+i> --> |0> + |-i> --> |1> + """ + if pole == 0: + self.add_ry(0 + offset, qubits) + elif pole == 1: + self.add_ry(np.pi + offset, qubits) + elif pole == 2: + self.add_ry(np.pi/2, qubits) + self.add_rz(offset, qubits) + elif pole == 3: + self.add_ry(-np.pi/2 , qubits) + self.add_rz(offset, qubits) + elif pole == 4: + self.add_rx(-np.pi/2 + offset, qubits) + elif pole == 5: + self.add_rx(np.pi/2 + offset, qubits) + self.barrier(qubits) + return + + def decode_podal_state(self, qubits, pole=2, offset=0): + """ + Decodes podal state of same parameters to return back to |0>. + """ + if pole == 0: + self.add_ry(0 - offset, qubits) + elif pole == 1: + self.add_ry(-np.pi - offset, qubits) + elif pole == 2: + self.add_rz(-offset, qubits) + self.add_ry(-np.pi/2, qubits) + elif pole == 3: + self.add_rz(-offset, qubits) + self.add_ry(np.pi/2 , qubits) + elif pole == 4: + self.add_rx(np.pi/2 - offset, qubits) + elif pole == 5: + self.add_rx(-np.pi/2 - offset, qubits) + self.barrier(qubits) + return + + def encode_theta_state(self, qubits, theta): + """ + Appends u3(theta, 0, 0) gate to [qubits] to prep each state + into the 1/sqrt(2) * (cos(theta/2)|0> + sin(theta/2)|1>) state. + """ + self.u3(theta, 0, 0, qubits) + self.barrier(qubits) + return + + def decode_theta_state(self, qubits, theta): + """ + Appends u3(-theta, 0, 0) gate to [qubits] in order to decode the + 1/sqrt(2)*(cos(theta/2)|0> + sin(theta/2)|1>) back to |0> state. + """ + self.barrier(qubits) + self.u3(-1*theta, 0, 0, qubits) + self.barrier(qubits) + return + + def encode_bell_phi_plus(self, qubit_pairs): + """ + Encodes the |phi^+> = |00> + |11> bell state on all (q1, q2) + pairs in qubit_pairs starting with |00> state. + + Input: qubit_pairs -- list of tuples: [(q1, q2), (q3, q4), ... + """ + for q_pair in qubit_pairs: + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + + return + + def decode_bell_phi_plus(self, qubit_pairs, measure=True): + """ + Decodes the |phi^+> = |00> + |11> bell state on all (q1, q2) + pairs in [qubit_pairs] back into |00> state. + If measure is True, also adds measurements on relevant qubits. + """ + for q_pair in qubit_pairs: + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + + if measure is True: + # qubit to register may not be 1-1 since not all qubits + # can be used (i.e. odd qubit number or whatever) + count = 0 + for q_pair in qubit_pairs: + self.measure(q_pair[0], count) + count += 1 + self.measure(q_pair[1], count) + count += 1 + + return + + def encode_bell_phi_minus(self, qubit_pairs): + """ + Encodes the |phi^-> = |00> - |11> bell state on all (q1, q2) + pairs in qubit_pairs starting with the |00> state. + + Input: qubit_pairs -- list of tuples: [(q1, q2), (q3, q4)...] + """ + for q_pair in qubit_pairs: + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_z(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + + return + + def decode_bell_phi_minus(self, qubit_pairs, measure=True): + """ + Decodes the |phi^-> = |00> - |11> bell state on all (q1, q2) + pairs in [qubit_pairs] back into |00> state. + If measure is True, also adds measurements on relevant qubits. + """ + for q_pair in qubit_pairs: + self.add_z(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + + if measure is True: + # qubit to register may not be 1-1 since not all qubits + # can be used (i.e. odd qubit number or whatever) + count = 0 + for q_pair in qubit_pairs: + self.measure(q_pair[0], count) + count += 1 + self.measure(q_pair[1], count) + count += 1 + + return + + def encode_bell_psi_plus(self, qubit_pairs): + """ + Encodes the |psi^+> = |01> + |10> bell state on all (q1, q2) + pairs in qubit_pairs starting with |00> state. + + Input: qubit_pairs -- list of tuples: [(q1, q2), (q3, q4), ... + """ + for q_pair in qubit_pairs: + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_x(q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + + return + + def decode_bell_psi_plus(self, qubit_pairs, measure=True): + """ + Decodes the |psi^+> = |01> + |10> bell state on all (q1, q2) + pairs in [qubit_pairs] back into |00> state. + If measure is True, also adds measurements on relevant qubits. + """ + for q_pair in qubit_pairs: + self.add_x(q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + + if measure is True: + # qubit to register may not be 1-1 since not all qubits + # can be used (i.e. odd qubit number or whatever) + count = 0 + for q_pair in qubit_pairs: + self.measure(q_pair[0], count) + count += 1 + self.measure(q_pair[1], count) + count += 1 + return + + def encode_bell_psi_minus(self, qubit_pairs): + """ + Encodes the |psi^-> = |01> - |10> bell state on all (q1, q2) + pairs in qubit_pairs starting with |00> state. + + Input: qubit_pairs -- list of tuples: [(q1, q2), (q3, q4), ... + """ + for q_pair in qubit_pairs: + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_x(q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_z(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + + return + + def decode_bell_psi_minus(self, qubit_pairs, measure=True): + """ + Decodes the |psi^-> = |01> - |10> bell state on all (q1, q2) + pairs in [qubit_pairs] back into |00> state. + If measure is True, also adds measurements on relevant qubits. + """ + for q_pair in qubit_pairs: + self.add_z(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + self.add_x(q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.cx(q_pair[0], q_pair[1]) + self.barrier(q_pair[0], q_pair[1]) + self.add_h(q_pair[0]) + self.barrier(q_pair[0], q_pair[1]) + + if measure is True: + # qubit to register may not be 1-1 since not all qubits + # can be used (i.e. odd qubit number or whatever) + count = 0 + for q_pair in qubit_pairs: + self.measure(q_pair[0], count) + count += 1 + self.measure(q_pair[1], count) + count += 1 + + return + + def encode_ghz_n(self, qubit_tuples): + """ + Encodes the |0...0> + |1...1> GHZ state on all (q1, ..., qn) + n-tuples in qubit_tuples starting with |0...0> state. + NOTE: The assumption is that CX should be applied on pairs + such as (q1, q2), (q2, q3), etc..., so obviously this doesn't + apply to all hardware natively. In general, must be more manual. + + Input: qubit_tuples -- list of tuples of the form + [(q1, ..., qn), (qn+1, ..., qn+n), ...] + """ + for q_tup in qubit_tuples: + self.add_h(q_tup[0]) + n = len(q_tup) + for j in range(n-1): + self.barrier([q for q in q_tup]) + self.cx(q_tup[j], q_tup[j+1]) + self.barrier([q for q in q_tup]) + + return + + def decode_ghz_n(self, qubit_tuples, measure=True): + """ + Decodes the |0...0> + |1...1> GHZ state on all (q1, ..., qn) + n-tuples in qubit_tuples starting with |0...0> state. + NOTE: The assumption is that CX should be applied on pairs + such as (q1, q2), (q2, q3), etc..., so obviously this doesn't + apply to all hardware natively. In general, must be more manual. + + Input: qubit_tuples -- list of tuples of the form + [(q1, ..., qn), (qn+1, ..., qn+n), ...] + """ + for q_tup in qubit_tuples: + n = len(q_tup) + for j in range(n-1): + self.barrier([q for q in q_tup]) + # add cx in opposite order of encoding + # which means running list backwards + self.cx(q_tup[n-j-2], q_tup[n-j-1]) + self.barrier([q for q in q_tup]) + # add the hadarmard which used to be at the beginning + self.add_h(q_tup[0]) + self.barrier([q for q in q_tup]) + + if measure is True: + # qubit to register may not be 1-1 since not all qubits + # can be used (i.e. odd qubit number or whatever) + count = 0 + for q_tup in qubit_tuples: + for q in q_tup: + self.measure(q, count) + count += 1 + + return + + ################################################## + # DD Circuit Methods + ################################################## + def add_free(self, qubits, num_ids): + """ + Appends free evolution to [qubits] over [n] identity gates. + """ + for _ in range(num_ids): + self.id(qubits) + self.barrier(qubits) + return + + + def add_pause(self, qubits, time, unit='dt'): + '''adds pause (id gate) for [time]dt on [qubits]''' + if time != 0: + self.delay(time, qubits, unit=unit) + self.barrier(qubits) + + return + + ###################################################################### + # Basic DD sequences (XY4) and slight augmentations + ###################################################################### + + def add_hahn(self, qubits, num_reps=1, tau=0): + """ + Appends Hahn echo sequence to [qubits] with [idpad] ids between pulses. + -X- where - is [idpad] free evo. + + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_super_hahn(self, qubits, num_reps=1, tau=0): + """ + Appends symmetric eulerian super-cycle Hahn sequence to + [qubits] with [idpad] ids between pulses, i.e. + -X--Xb- where - is [idpad] freee evo. + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # Xb (aka -X) + self.add_xb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_cpmg(self, qubits, num_reps=1, tau=0): + """ + Appends cpmg sequence to [qubits] with [idpad] ids between pulses + -X--X- where - is [idpad] free evo. + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_super_cpmg(self, qubits, num_reps=1, tau=0): + """ + Appends symmetric eulerian super-cycle cpmg sequence to [qubits] + with [idpad] ids between pulses, i.e. + -X--X--Xb--Xb- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_super_euler(self, qubits, num_reps=1, tau=0): + """ + Appends symmetric eulerian super-cycle sequence to [qubits] + with [idpad] ids between pulses, i.e. + -X--Y--X--Y--Y--X--Y--X--Xb--Yb--Xb--Yb--Yb--Xb--Yb--Xb- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_xy4(self, qubits, num_reps=1, tau=0): + """ + Appends xy4 sequence to [qubits] for [num_reps] times with [tau] + id gates between each DD pulse gate. + -Y--X--Y--X- where - is [idpad] free evo. + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2 * idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + + return + + def add_cdd_n(self, qubits, n, tau=0): + if n == 1: + self.add_xy4(qubits, 1, tau) + else: + # add free.Y.free motif + self.add_pause(qubits, tau) + self.add_y(qubits) + self.barrier(qubits) + self.add_pause(qubits, tau) + # add U^(n-1) + self.add_cdd_n(qubits, n-1, tau) + # add free.X.free motif + self.add_pause(qubits, tau) + self.add_x(qubits) + self.barrier(qubits) + self.add_pause(qubits, tau) + # add U^(n-1) + self.add_cdd_n(qubits, n-1, tau) + # add free.Y.free motif + self.add_pause(qubits, tau) + self.add_y(qubits) + self.barrier(qubits) + self.add_pause(qubits, tau) + # add U^(n-1) + self.add_cdd_n(qubits, n-1, tau) + # add free.X.free motif + self.add_pause(qubits, tau) + self.add_x(qubits) + self.barrier(qubits) + self.add_pause(qubits, tau) + # add U^(n-1) + self.add_cdd_n(qubits, n-1, tau) + return + + def add_cdd2(self, qubits, num_reps=1, tau=0): + """ + Appends CDD_2 sequence to [qubits] for [num_reps] times with [tau] + in between each DD pulse gate. + """ + for _ in range(num_reps): + self.add_cdd_n(qubits, 2, tau) + + return + + def add_cdd3(self, qubits, num_reps=1, tau=0): + """ + Appends CDD_3 sequence to [qubits] for [num_reps] times with [tau] + in between each DD pulse sequence. + """ + for _ in range(num_reps): + self.add_cdd_n(qubits, 3, tau) + + return + + def add_cdd4(self, qubits, num_reps=1, tau=0): + """ + Appends CDD_4 sequence to [qubits] for [num_reps] times with [tau] + in between each DD pulse sequence. + """ + for _ in range(num_reps): + self.add_cdd_n(qubits, 4, tau) + + return + + def add_cdd5(self, qubits, num_reps=1, tau=0): + """ + Appends CDD_5 sequence to [qubits] for [num_reps] times with [tau] + in between each DD pulse sequence. + """ + for _ in range(num_reps): + self.add_cdd_n(qubits, 5, tau) + + return + + ###################################################################### + # RGA DD Sequences + ###################################################################### + + def add_rga2x(self, qubits, num_reps=1, tau=0): + """ + Appends RGA2 sequence to [qubits] for [num_reps] times with [tau] + in between each DD pulse gate. + Sequence: Xb X + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + We implement Xb = Z as vZ.I.I. + -Xb--X- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + + return + + def add_rga2y(self, qubits, num_reps=1, tau=0): + """ + Appends RGA2 sequence to [qubits] for [num_reps] times with [tau] + id_gates beteen each DD pulse gate. + Sequence: Yb Y + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + -Yb--Y- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + + return + + def add_rga2z(self, qubits, num_reps=1, tau=0): + """ + Appends RGA2z sequence to [qubits] for [num_reps] times with [tau] + id gates between each DD pulse gate. + Sequence: Zb Z + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + -Zb--Z- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Zb + self.add_zb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Z + self.add_zii(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_rga4(self, qubits, num_reps=1, tau=0): + """ + Appends RGA4 sequence to [qubits] for [num_reps] times with [tau] + identity gates between each DD pulse gate. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + -Yb--X--Yb--X- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_rga4p(self, qubits, num_reps=1, tau=0): + """ + Appends RGA4' to [qubits] for [num_reps] times with [tau] id + gates each DD pulse gate. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + -Yb--Xb--Yb--X- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + + return + + def add_rga8a(self, qubits, num_reps=1, tau=0): + """ + appends RGA8a to [qubits] with [tau] pause between pulses + -X--Yb--X--Yb--Y--Xb--Y--Xb- + Note: This is a more symmetrized version suggested by Greg that + is not in the original paper. + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_rga8c(self, qubits, num_reps=1, tau=0): + """ + Appends RGA8c to [qubits] for [num_reps] times with [tau] id_gates + between each DD pulse gate. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + -X--Y--X--Y--Y--X--Y--X- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + return + + def add_rga16a(self, qubits, num_reps=1, tau=0): + """ + Appends RGA16a to [qubits] for [num_reps] times with [tau] id + gates each DD pulse gate. + NOTE: We choose P2 = X and P1 = Y for no particular reason. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + Sequence: -Zb-(RGA8a)-Z-(RGA8a) + """ + for _ in range(num_reps): + # free evo (tau) + self.add_pause(qubits, tau) + # Zb + self.add_zb(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # RGA8a + self.add_rga8a(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Z + self.add_zii(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # RGA8a + self.add_rga8a(qubits, 1, tau) + return + + def add_rga16b(self, qubits, num_reps=1, tau=0): + """ + Appends RGA16b'' to [qubits] for [num_reps] times with [tau] id + gates each DD pulse gate. + NOTE: We choose P2 = X and P1 = Y for no particular reason. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + RGA4'[RGA4'], where RGA4' is + -Yb--Xb--Yb--X- + """ + for _ in range(num_reps): + # free evo (tau) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4' + self.add_rga4p(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4' + self.add_rga4p(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4' + self.add_rga4p(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4' + self.add_rga4p(qubits, 1, tau) + + return + + + def add_rga32a(self, qubits, num_reps=1, tau=0): + """ + Appends RGA32a to [qubits] for [num_reps] times with [tau] id + gates each DD pulse gate. + NOTE: We choose P2 = X and P1 = Y for no particular reason. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + RGA4[RGA8a] where RGA4 is given by + -Yb--X--Yb--X- + """ + for _ in range(num_reps): + # free evo (tau) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + + return + + def add_rga32c(self, qubits, num_reps=1, tau=0): + """ + Appends RGA32c to [qubits] for [num_reps] times with [tau] id + gates each DD pulse gate. + NOTE: We choose P2 = X and P1 = Y for no particular reason. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + RGA8c[RGA4], where RGA8c sequence is given by + -X--Y--X--Y--Y--X--Y--X- + """ + for _ in range(num_reps): + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + # free evo (tau) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (tau) + self.add_pause(qubits, tau) + # rga4 + self.add_rga4(qubits, 1, tau) + return + + def add_rga64a(self, qubits, num_reps=1, tau=0): + """ + Appends RGA64a to [qubits] for [num_reps] times with [tau] id_gates + between each DD pulse gate. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + RGA8a[RGA8a] where the RGA8a sequence is + -X--Yb--X--Yb--Y--Xb--Y--Xb- + """ + for _ in range(num_reps): + # add free (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rg8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + # add free (idpad) + self.add_pause(qubits, tau) + # Xb + self.add_xb(qubits) + self.barrier(qubits) + # add free (idpad) + self.add_pause(qubits, tau) + # rga8a + self.add_rga8a(qubits, 1, tau) + return + + + def add_rga64c(self, qubits, num_reps=1, tau=0): + """ + Appends RGA64c to [qubits] for [num_reps] times with [tau] id_gates + between each DD pulse gate. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + Sequence: RGA8c[RGA8c] where the RGA8c sequence is + -X--Y--X--Y--Y--X--Y--X- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # Y + self.add_y(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga8c + self.add_rga8c(qubits, 1, tau) + return + + def add_rga256a(self, qubits, num_reps=1, tau=0): + """ + Appends RGA256a sequence to [qubits] for [num_reps] times with [tau] + identity gates between each DD pulse gate. + NOTE: Xb means X-bar which is pi phase flip on all axes of Y gate. + RGA4[RGA64a] where the RGA4 sequence is given by + -Yb--X--Yb--X- + """ + for _ in range(num_reps): + # free evo (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga64a + self.add_rga64a(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga64a + self.add_rga64a(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # Yb + self.add_yb(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga64a + self.add_rga64a(qubits, 1, tau) + # free evo (idpad) + self.add_pause(qubits, tau) + # X + self.add_x(qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + # rga64a + self.add_rga64a(qubits, 1, tau) + return + + ################################################## + # KDD Sequence + ################################################## + # define the KDD_\phi composite pulse + def add_comp_kdd(self, qubits, phi, tau=0): + """ + Appends the KDD_\phi composite pulse which consists of the following: + - (\pi)_{\pi/6 + \phi} -- (\pi)_{\phi} -- (\pi)_{\pi/2 + \phi} + -- (\pi)_{\phi} -- (\pi)_{\pi/6 + \phi} - + where - is free evo for [tau] id gates and (\pi)_{\phi} is + a Pi rotation pulse about the \phi axis where \phi is angle between + positive x-axis and point in x-y axis, counter-clockwise. + """ + # free evo (idpad) + self.add_pause(qubits, tau) + # p1 pulse + p1 = np.pi/6 + phi + self.add_pi_eta(p1, qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # p2 pulse + p2 = phi + self.add_pi_eta(p2, qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # p3 + \phi pulse + p3 = np.pi/2 + phi + self.add_pi_eta(p3, qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # p2 pulse + self.add_pi_eta(p2, qubits) + self.barrier(qubits) + # free evo (2*idpad) + self.add_pause(qubits, 2*tau) + # p1 pulse + self.add_pi_eta(p1, qubits) + self.barrier(qubits) + # free evo (idpad) + self.add_pause(qubits, tau) + + return + + def add_kdd(self, qubits, num_reps=1, tau=0): + """ + Appends KDD sequence to [qubits] for [num_reps] times with [tau] + pause between each DD pulse. + + (KDD_pi/2)(KDD_0)(KDD_pi/2)(KDD_0) = "Y.X.Y.X" with composite pulses + """ + for _ in range(num_reps): + self.add_comp_kdd(qubits, np.pi/2, tau) + self.add_comp_kdd(qubits, 0, tau) + self.add_comp_kdd(qubits, np.pi/2 , tau) + self.add_comp_kdd(qubits, 0, tau) + + ################################################## + # Universal Robust (UR) DD Sequence + ################################################## + def add_ur(self, qubits, n, num_reps=1, tau=0): + """ + Appends the UR_n sequence to [qubits] for [num_reps] times with + [idpad] pause between each DD pulse. + The sequence consists of [pi]_\phi_k rotations where phi_k is + rotation axis (standard phi in x-y plane polar coords) in: + "Arbitrarily Accurate Pulse Sequences for Robust" DD by + Genov, Schraft, Vitanov, and Halfmann in PRL. The + get_urdd_phis() function below should also make it clear. + """ + # get list of pulses from unique phi information + phi_list, _, _ = get_urdd_phis(n) + for _ in range(num_reps): + for phi in phi_list: + # free evo + self.add_pause(qubits, tau) + # pulse + self.add_pi_eta(phi, qubits) + self.barrier(qubits) + # free evo + self.add_pause(qubits, tau) + return + + def add_ur6(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 6, num_reps, tau) + return + + def add_ur10(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 10, num_reps, tau) + return + + def add_ur20(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 20, num_reps, tau) + return + + def add_ur50(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 50, num_reps, tau) + return + + def add_ur100(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 100, num_reps, tau) + return + + def add_ur200(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 200, num_reps, tau) + return + + def add_ur300(self, qubits, num_reps=1, tau=0): + """ + Just a wrapper to run experiments more easily. + """ + self.add_ur(qubits, 300, num_reps, tau) + return + +################################################## +# Utilities for this class (put in this file for now because there are +# so few. If I start collecting more--I'll add seperate file. +################################################## +def replace_sx_sx_with_x(circ): + """ + Given a [circ], replace sx-sx motif with + a single x gate. + """ + qasm_string = circ.qasm() + new_qasm_string = "" + prev_was_sx = False + replace_sx_idxs = [] + + # find and keep track of sx-sx motifs + for idx, inst in enumerate(qasm_string.split('\n')): + if 'sx' in inst: + if prev_was_sx == True: + replace_sx_idxs.append(idx - 1) + else: + prev_was_sx = True + new_qasm_string += inst + "\n" + else: + new_qasm_string += inst + "\n" + prev_was_sx == False + + # replace sx values with x if applicable + if replace_sx_idxs != []: + qasm_split = new_qasm_string.split('\n') + for idx in replace_sx_idxs: + qasm_split[idx] = "x" + qasm_split[idx][2:] + new_qasm_string = "\n".join(qasm_split) + + # get new circ with old global phase + new_circ = QuantumCircuit.from_qasm_str(new_qasm_string) + new_circ.global_phase = circ.global_phase + + return new_circ + + +def get_harr_random_u3_params(): + """ + Generates the theta, phi, and lambda parameters for a harr-random + unitary in u3(theta, phi, lambda) form + """ + decomp = OneQubitEulerDecomposer(basis='U3') + harr_random = random_unitary(2).data + + return decomp.angles(harr_random) + +######################### +# URDD Functions +######################### +def get_urdd_phis(n): + ''' Gets \phi_k values for n pulse UR sequence''' + if n % 2 == 1: + raise ValueError("n must be even") + elif n < 4: + raise ValueError("n must be >= 4") + # phi1 = 0 by convention + phis = [0] + + # get capital Phi value + if n % 4 == 0: + m = int(n / 4) + big_phi = np.pi / m + else: + m = int((n - 2) / 4) + big_phi = (2 * m * np.pi) / (2 * m + 1) + + # keep track of unique phi added; we choose phi2 = big_phi by convention-- + # only real requirement is (n * big_phi = 2pi * j for j int) + unique_phi = [0, big_phi] + # map each phi in [phis] to location (by index) of corresponding [unique_phi] + phi_indices = [0, 1] + # populate remaining phi values + for k in range(3, n+1): + phi_k = (k * (k - 1) * big_phi) / 2 + # values only matter modulo 2 pi + phi_k = (phi_k) % (2 * np.pi) + if np.isclose(phi_k, 0): + phi_k = 0 + elif np.isclose(phi_k, 2 * np.pi): + phi_k = 0 + + added_new = False + for idx, u_phi in enumerate(unique_phi): + if np.isclose(u_phi, phi_k, atol=0.001): + added_new = True + phi_indices.append(idx) + + if added_new == False: + unique_phi.append(phi_k) + phi_indices.append(len(unique_phi)-1) + + # construct phi list + phis = [] + for idx in phi_indices: + phis.append(unique_phi[idx]) + + return (phis, unique_phi, phi_indices) diff --git a/src/edd/circuit/_ibmq_circ_test.py b/src/edd/circuit/_ibmq_circ_test.py new file mode 100644 index 0000000..c3587ae --- /dev/null +++ b/src/edd/circuit/_ibmq_circ_test.py @@ -0,0 +1,1510 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +import numpy as np + +from qiskit.converters import circuit_to_dag +from qiskit import QuantumCircuit + +from edd.backend import IBMQBackend +from edd.circuit import IBMQDdCircuit + +# there may be need for unit testing in future, so this is here for that +class DdCircuitTest(unittest.TestCase): + """ Units tests for IBMQBackend. """ + + ################################################## + # General IBMQDdCircuit method tests + ################################################## + def test_get_gate_count_no_trans_armonk(self): + armonk = IBMQBackend('ibmq_armonk') + circ = IBMQDdCircuit(1) + circ.x(0) + circ.z(0) + circ.h(0) + answer = {'x': 1, 'z': 1, 'h': 1} + gate_count = circ.get_gate_count(False, armonk) + self.assertDictEqual(answer, gate_count) + + def test_get_gate_count_with_trans_armonk(self): + armonk = IBMQBackend('ibmq_armonk') + circ = IBMQDdCircuit(1) + circ.x(0) + circ.z(0) + circ.h(0) + answer = {'u2': 1} + gate_count = circ.get_gate_count(True, armonk) + self.assertDictEqual(answer, gate_count) + + def test_get_gate_count_with_trans_and_barriers_armonk(self): + armonk = IBMQBackend('ibmq_armonk') + circ = IBMQDdCircuit(1) + circ.x(0) + circ.barrier(0) + circ.z(0) + circ.barrier(0) + circ.h(0) + circ.barrier(0) + answer = {'u1': 1, 'u2': 1, 'u3': 1, 'barrier': 3} + gate_count = circ.get_gate_count(True, armonk) + self.assertDictEqual(answer, gate_count) + + def test_get_transpiled_circ_armonk(self): + armonk = IBMQBackend('ibmq_armonk') + circ = IBMQDdCircuit(1) + circ.x(0) + circ.barrier(0) + circ.z(0) + circ.barrier(0) + circ.h(0) + t_circ = circ.get_transpiled_circ(armonk) + circ_dag = circuit_to_dag(t_circ) + gate_count = circ_dag.count_ops() + answer = {'u1': 1, 'u2': 1, 'u3': 1, 'barrier': 2} + self.assertDictEqual(answer, gate_count) + + def test_get_phys_time_armonk(self): + armonk = IBMQBackend('ibmq_armonk') + gate_times = armonk.get_gate_times() + circ = IBMQDdCircuit(1) + circ.z(0) + curr_time = circ.get_phys_time(armonk) + # at this point, only added virtual z + self.assertEquals(0, curr_time) + + # now add identity + circ.barrier(0) + circ.id(0) + answer = gate_times['id'] + curr_time = circ.get_phys_time(armonk) + self.assertEquals(answer, curr_time) + + # now ensure that adding gates increases the time + circ.barrier(0) + # this is x in terms of u3 + circ.add_x(0) + answer += gate_times['u3'] + curr_time = circ.get_phys_time(armonk) + self.assertEquals(answer, curr_time) + + def test_get_statevector(self): + circ1 = IBMQDdCircuit(1) + answer1 = np.array([1.+0.j, 0.+0.j]) + s = circ1.get_statevector() + self.assertEquals(np.allclose(s, answer1), True) + + circ2 = IBMQDdCircuit(5) + answer2 = [] + for j in range(2**5): + if j == 0: + answer2.append(1.+0.j) + else: + answer2.append(0.+0.j) + answer2 = np.array(answer2) + s = circ2.get_statevector() + self.assertEquals(np.allclose(s, answer2), True) + + + def test_get_unitary(self): + circ1 = IBMQDdCircuit(1) + id2 = np.identity(2) + u = circ1.get_unitary() + self.assertEquals(np.allclose(u, id2), True) + + circ2 = IBMQDdCircuit(5) + id5 = np.identity(2**5) + u = circ2.get_unitary() + self.assertEquals(np.allclose(u, id5), True) + + + ################################################## + # Basic Gate Tests + ################################################## + def test_add_x(self): + circ = IBMQDdCircuit(1) + circ.add_x(0) + x = np.array([[0, 1], [1, 0]]) + u = circ.get_unitary() + self.assertEquals(np.allclose(u, x), True) + + def test_add_y(self): + circ = IBMQDdCircuit(1) + circ.add_y(0) + y = np.array([[0, -1j], [1j, 0]]) + u = circ.get_unitary() + self.assertEquals(np.allclose(u, y), True) + + def test_add_z(self): + circ = IBMQDdCircuit(1) + circ.add_z(0) + z = np.array([[1, 0], [0, -1]]) + u = circ.get_unitary() + self.assertEquals(np.allclose(u, z), True) + + def test_add_zii(self): + circ = IBMQDdCircuit(1) + circ.add_zii(0) + z = np.array([[1, 0], [0, -1]]) + u = circ.get_unitary() + # check that you get correct unitary + self.assertEquals(np.allclose(u, z), True) + # now check that it really has added z_i_i + gate_counts = {"u1": 1, "barrier": 2, "id": 2} + self.assertDictEqual(circ.get_gate_count(), gate_counts) + + def test_add_xb(self): + circ = IBMQDdCircuit(1) + circ.add_xb(0) + xb = np.array([[1, 0], [0, -1]]) + u = circ.get_unitary() + # check that you get correct unitary + self.assertEquals(np.allclose(u, xb), True) + # now check that it really has added z_i_i + gate_counts = {"u1": 1, "barrier": 2, "id": 2} + self.assertDictEqual(circ.get_gate_count(), gate_counts) + + def test_add_yb(self): + circ = IBMQDdCircuit(1) + circ.add_yb(0) + yb = np.array([[1, 0], [0, -1]]) + u = circ.get_unitary() + # check that you get correct unitary + self.assertEquals(np.allclose(u, yb), True) + # now check that it really has added z_i_i + gate_counts = {"u1": 1, "barrier": 2, "id": 2} + self.assertDictEqual(circ.get_gate_count(), gate_counts) + + def test_add_zb(self): + circ = IBMQDdCircuit(1) + circ.add_zb(0) + zb = np.array([[0, 1], [1, 0]]) + u = circ.get_unitary() + self.assertEquals(np.allclose(u, zb), True) + + def test_add_h(self): + circ = IBMQDdCircuit(1) + circ.add_h(0) + norm = 1 / np.sqrt(2) + h = norm * np.array([[1, 1], [1, -1]]) + u = circ.get_unitary() + self.assertEquals(np.allclose(u, h), True) + + + ################################################## + # Encoding/ Decoding Tests + ################################################## + def test_encode_theta_state(self): + circ = IBMQDdCircuit(1) + circ.encode_theta_state(0, np.pi/2) + circ_state = circ.get_statevector() + state = np.array([1/np.sqrt(2), 1/np.sqrt(2)]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_decode_theta_state(self): + circ = IBMQDdCircuit(1) + circ.encode_theta_state(0, np.pi/2) + circ.decode_theta_state(0, np.pi/2) + circ_state = circ.get_statevector() + state = np.array([1, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_encode_bell_phi_plus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_phi_plus(qubit_pairs) + circ_state = circ.get_statevector() + norm = 1 / np.sqrt(2) + state = norm * np.array([1, 0, 0, 1]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_decode_bell_phi_plus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_phi_plus(qubit_pairs) + circ.decode_bell_phi_plus(qubit_pairs, False) + circ_state = circ.get_statevector() + state = np.array([1, 0, 0, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_encode_bell_phi_minus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_phi_minus(qubit_pairs) + circ_state = circ.get_statevector() + norm = 1 / np.sqrt(2) + state = norm * np.array([1, 0, 0, -1]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_decode_bell_phi_minus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_phi_minus(qubit_pairs) + circ.decode_bell_phi_minus(qubit_pairs, False) + circ_state = circ.get_statevector() + state = np.array([1, 0, 0, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_encode_bell_psi_plus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_psi_plus(qubit_pairs) + circ_state = circ.get_statevector() + norm = 1 / np.sqrt(2) + state = norm * np.array([0, 1, 1, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_decode_bell_psi_plus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_psi_plus(qubit_pairs) + circ.decode_bell_psi_plus(qubit_pairs, False) + circ_state = circ.get_statevector() + state = np.array([1, 0, 0, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_encode_bell_psi_minus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_psi_minus(qubit_pairs) + circ_state = circ.get_statevector() + norm = 1 / np.sqrt(2) + #state = norm * np.array([0, 1, -1, 0]) + #NOTE: I got tricked by qiskit qubit indexing (last qubit first) + state = norm * np.array([0, -1, 1, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_decode_bell_psi_minus(self): + circ = IBMQDdCircuit(2) + qubit_pairs = [(0, 1)] + circ.encode_bell_psi_minus(qubit_pairs) + circ.decode_bell_psi_minus(qubit_pairs, False) + circ_state = circ.get_statevector() + state = np.array([1, 0, 0, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + + def test_encode_ghz_n(self): + circ = IBMQDdCircuit(3) + qubit_tuples = [(0, 1, 2)] + circ.encode_ghz_n(qubit_tuples) + circ_state = circ.get_statevector() + norm = 1 / np.sqrt(2) + state = norm * np.array([1, 0, 0, 0, 0, 0, 0, 1]) + self.assertEquals(np.allclose(circ_state, state), True) + + def test_decode_ghz_n(self): + circ = IBMQDdCircuit(3) + qubit_tuples = [(0, 1, 2)] + circ.encode_ghz_n(qubit_tuples) + circ.decode_ghz_n(qubit_tuples, False) + circ_state = circ.get_statevector() + state = np.array([1, 0, 0, 0, 0, 0, 0, 0]) + self.assertEquals(np.allclose(circ_state, state), True) + + + ################################################## + # Test DD Circuits + ################################################## + + def test_add_free(self): + qubits = [0, 2] + num_ids = 7 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(3) + for _ in range(num_ids): + man_circ.id(qubits) + man_circ.barrier(qubits) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + edd_circ = IBMQDdCircuit(3) + edd_circ.add_free(qubits, num_ids) + edd_qasm = edd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(edd_qasm, man_qasm) + + def test_add_hahn(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_hahn(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_super_hahn(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # free + man_circ.add_free(qubits, id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_super_hahn(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_cpmg(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + man_circ.id(qubits) + man_circ.barrier(qubits) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_cpmg(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_super_cpmg(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_super_cpmg(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_super_euler(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_super_euler(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_xy4(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # free + man_circ.add_free(qubits, id_pad) + # y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, 2*id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, 2*id_pad) + # y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, 2*id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_xy4(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_cdd_n(self): + qubits = [0, 2, 3] + n = 1 + id_pad = 1 + # check that n = 1 works + xy4_circ = IBMQDdCircuit(4) + xy4_circ.add_xy4(qubits, 1, id_pad) + xy4_qasm = xy4_circ.qasm() + cdd_1_circ = IBMQDdCircuit(4) + cdd_1_circ.add_cdd_n(qubits, n, id_pad) + cdd_1_qasm = cdd_1_circ.qasm() + self.assertEquals(xy4_qasm, cdd_1_qasm) + + # check that n = 2 works + man_circ = IBMQDdCircuit(4) + man_circ.add_free(qubits, id_pad) + man_circ.add_y(qubits) + man_circ.barrier(qubits) + man_circ.add_free(qubits, id_pad) + + man_circ.add_xy4(qubits, 1, id_pad) + + man_circ.add_free(qubits, id_pad) + man_circ.add_x(qubits) + man_circ.barrier(qubits) + man_circ.add_free(qubits, id_pad) + + man_circ.add_xy4(qubits, 1, id_pad) + + man_circ.add_free(qubits, id_pad) + man_circ.add_y(qubits) + man_circ.barrier(qubits) + man_circ.add_free(qubits, id_pad) + + man_circ.add_xy4(qubits, 1, id_pad) + + man_circ.add_free(qubits, id_pad) + man_circ.add_x(qubits) + man_circ.barrier(qubits) + man_circ.add_free(qubits, id_pad) + + man_circ.add_xy4(qubits, 1, id_pad) + man_circ_qasm = man_circ.qasm() + + cdd_2_circ = IBMQDdCircuit(4) + n = 2 + cdd_2_circ.add_cdd_n(qubits, n, id_pad) + cdd_2_qasm = cdd_2_circ.qasm() + + self.assertEquals(cdd_2_qasm, man_circ_qasm) + + + def test_add_rga2x(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # free + man_circ.add_free(qubits, id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga2x(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga2y(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # free + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # free + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga2y(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga2z(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Zb + man_circ.add_zb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # z_i_i + man_circ.add_zii(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga2z(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga4(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga4(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga4p(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga4p(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga8a(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga8a(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga8c(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga8c(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga16a(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Zb + man_circ.add_zb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RGA8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # z + man_circ.add_zii(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RGA8a + man_circ.add_rga8a(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga16a(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga16b(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RGA4' + man_circ.add_rga4p(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # RGA4' + man_circ.add_rga4p(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # rga4' + man_circ.add_rga4p(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # rga4' + man_circ.add_rga4p(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga16b(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + + def test_add_rga32a(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RGA8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RG8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga32a(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + + def test_add_rga32c(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RGA4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # RGA4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga4 + man_circ.add_rga4(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga4 + man_circ.add_rga4(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga32c(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga64a(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Xb + man_circ.add_xb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8a + man_circ.add_rga8a(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga64a(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_rga64c(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # x + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Y + man_circ.add_y(qubits) + man_circ.barrier(qubits) + # id + man_circ.id(qubits) + man_circ.barrier(qubits) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga8c + man_circ.add_rga8c(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga64c(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + + def test_add_rga256a(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga64a + man_circ.add_rga64a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga64a + man_circ.add_rga64a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # Yb + man_circ.add_yb(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga64a + man_circ.add_rga64a(qubits, 1, id_pad) + # id + man_circ.add_free(qubits, id_pad) + # X + man_circ.add_x(qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # rga64a + man_circ.add_rga64a(qubits, 1, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_rga256a(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_comp_kdd(self): + qubits = [0, 2, 3] + id_pad = 1 + phi = 1.2 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + x_params = np.array([np.pi, 0, np.pi]) + # id + man_circ.add_free(qubits, id_pad) + # p1 + p1_params = x_params + np.pi/6 + phi + man_circ.u3(*p1_params, qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # p2 + p2_params = x_params + phi + man_circ.u3(*p2_params, qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # p3 + p3_params = x_params + np.pi/2 + phi + man_circ.u3(*p3_params, qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # p2 + man_circ.u3(*p2_params, qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, 2*id_pad) + # p1 + man_circ.u3(*p1_params, qubits) + man_circ.barrier(qubits) + # id + man_circ.add_free(qubits, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_comp_kdd(qubits, phi, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_kdd(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # create a manual circuit with intended sequence + man_circ = IBMQDdCircuit(4) + for _ in range(num_reps): + man_circ.add_comp_kdd(qubits, np.pi/2, id_pad) + man_circ.add_comp_kdd(qubits, 0, id_pad) + man_circ.add_comp_kdd(qubits, np.pi/2, id_pad) + man_circ.add_comp_kdd(qubits, 0, id_pad) + # get qasm from manual circuit + man_qasm = man_circ.qasm() + + # create our IBMQDdCircuit version + dd_circ = IBMQDdCircuit(4) + dd_circ.add_kdd(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + + # assert that qasm strings are the same + self.assertEquals(dd_qasm, man_qasm) + + def test_add_ur(self): + qubits = [0, 2, 3] + num_reps = 4 + id_pad = 1 + # just test that it runs properly + dd_circ = IBMQDdCircuit(4) + dd_circ.add_ur50(qubits, num_reps, id_pad) + dd_qasm = dd_circ.qasm() + boolval = isinstance(dd_qasm, str) + self.assertEquals(boolval, True) + +if __name__ == "__main__": + unittest.main() diff --git a/src/edd/data/__init__.py b/src/edd/data/__init__.py new file mode 100644 index 0000000..e96764c --- /dev/null +++ b/src/edd/data/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.data._ibmq_data import IBMQData diff --git a/src/edd/data/_ibmq_data.py b/src/edd/data/_ibmq_data.py new file mode 100644 index 0000000..e39e41c --- /dev/null +++ b/src/edd/data/_ibmq_data.py @@ -0,0 +1,1145 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import rc +rc('text', usetex=True) +import scipy +import yaml +import itertools +import copy + +# type check import +from qiskit.result import Result + + +class IBMQData(): + """ + This class holds data from IBMQ experiments and allows easy manipulation + such as bootstrapping, plotting, and saving data. + """ + + def __init__(self, raw_data=None, name='test', working_label=None): + """ [raw_data] is Result object from submitting IBMQ job or + this object cast as a dict and [name] just names the raw_data set.""" + self.name = name + # cast as dict to utilize common methods (not data class ones) + self.raw_data = raw_data + if self.raw_data is not None: + if isinstance(raw_data, Result): + self.raw_data = [raw_data.to_dict()] + elif isinstance(raw_data, dict): + self.raw_data = [raw_data] + elif isinstance(raw_data, list): + self.raw_data = raw_data + + # convert data to more workable form + if self.raw_data is not None: + self.collate_to_results() + self.results_to_tuple_data() + if working_label is not None: + self.change_working_data_by_label(working_label) + else: + self.results = None + self.tuple_data = None + self.working_data = None + self.working_label = working_label + + # last member data is "plottable data" + self.plot_data = None + self.grover_data = None + + return + + def add_raw_data(self, more_raw_data): + """ + Appends [more_raw_data] to self.raw_data. If not a list, makes + self.raw_data into one. + This method is useful for concatenating similar raw_data sets. + """ + # convert more_raw_data to right type + if not isinstance(more_raw_data, list) and not isinstance(more_raw_data, dict): + if isinstance(more_raw_data, Result): + more_raw_data = more_raw_data.to_dict() + else: + raise ValueError("added raw_data must be list, dict, or Result object") + + if self.raw_data is None: + if isinstance(more_raw_data, list): + self.raw_data = more_raw_data + else: + self.raw_data = [more_raw_data] + else: + if isinstance(more_raw_data, list): + self.raw_data.extend(more_raw_data) + else: + self.raw_data.append(more_raw_data) + return + + def save_raw_data(self, fname): + """ + Saves raw_data in self.raw_data to '[fname].yml' + """ + if fname[-4::] != '.yml': + fname += '.yml' + try: + with open(f'{fname}', 'w') as outfile: + yaml.dump(self.raw_data, outfile) + except ValueError: + e = "Cannot save self.raw_data to file when it is None type." + raise ValueError(e) + return + + def load_raw_data(self, fname): + """ + Loads raw_datas (as dict) from [fname] into [self.raw_data] + """ + # check if fname has .yml extension already and add if not + if fname[-4::] != '.yml': + fname += '.yml' + + # load in raw_data + with open(fname, 'r') as infile: + new_raw_data = yaml.safe_load(infile) + # add new loaded raw_datas in with current raw_datas + self.add_raw_data(new_raw_data) + + ################################################## + # "Pre-wrangle" wrangle methods + ################################################## + def collate_to_results(self): + self.results = [] + for result_dict in self.raw_data: + r_dict_list = result_dict['results'] + for r_dict in r_dict_list: + r_dict['backend_name'] = result_dict['backend_name'] + r_dict['date'] = result_dict['date'].__str__() + self.results.extend(result_dict['results']) + + def results_to_tuple_data(self): + self.tuple_data = [] + for result in self.results: + back_name = result['backend_name'].split("_")[1] + back_str = f"backName_{back_name}_" + date_str = f"_date_{result['date']}_" + tup = (back_str + result['header']['name'] + date_str, + result['header']['memory_slots'], result['data']['counts']) + self.tuple_data.append(tup) + return + + def format_data(self): + self.collate_to_results() + self.results_to_tuple_data() + return + + def change_working_data_by_label(self, *labels): + self.working_label = labels + self.working_data = [] + for result in self.tuple_data: + if np.array([x in result[0] for x in labels]).all(): + self.working_data.append(result) + return + + ################################################## + # Wrangle and Plotting Methods + ################################################## + def apply_error_mitigation(self): + """ + If this dataset contains error-mitigation circuits, then we want + to offset obtained results appropriately. + """ + self.change_working_data_by_label('error_mitigate_0') + err0_data = self.working_data[0][2] + tot = err0_data['0x0'] + err0_data['0x1'] + m00 = err0_data['0x0'] / tot + m01 = err0_data['0x1'] / tot + self.change_working_data_by_label('error_mitigate_1') + err1_data = self.working_data[0][2] + tot = err1_data['0x0'] + err1_data['0x1'] + m10 = err1_data['0x0'] / tot + m11 = err1_data['0x1'] / tot + calib_mat = np.array([[m00, m01], [m10, m11]]) + # adjust raw counts for other data + for idx in range(len(self.tuple_data)): + tup = self.tuple_data[idx] + count_vec = np.array([tup[2]['0x0'], tup[2]['0x1']]) + new_vec = np.matmul(calib_mat, count_vec) + new_counts = {'0x0': int(new_vec[0]), '0x1': int(new_vec[1])} + self.tuple_data[idx] = (tup[0], tup[1], new_counts) + + def save_plot_data(self, fname): + """ + Given plot data, saves to file in format readable by Mathematica. + """ + plot_data = self.plot_data + with open(fname + '.csv', 'w') as f: + for idx in range(len(plot_data[0])): + f.write(f"{plot_data[0][idx]}, {plot_data[1][idx]}, {plot_data[2][idx]}\n") + return fname + + def wrangle_theta_sweep(self): + """ + Wrangles [self.working_data] assuming it was produced + from a theta sweep experiment. + """ + # iterate over the list of runs and get global information + theta_vals = [] + theta_fids = [] + theta_2stds = [] + + for result in self.working_data: + # extract tau value from experiment label + split_label = result[0].split('_')[2::] + theta_idx = split_label.index('theta') + theta_val = split_label[theta_idx + 1] + theta_vals.append(float(theta_val)) + + num_qubits_measured, count_list = result + # iterate over counts for each of the haar states + succ_counts = [] + fail_counts = [] + for state_data in count_list: + succ, fail = get_success_failure_count(state_data, num_qubits_measured) + succ_counts.append(succ) + fail_counts.append(fail) + + # bootstrap over haar states + boot_sample = beta_bayesian_bootstrap(succ_counts, fail_counts, succ_counts[0] + fail_counts[0]) + theta_fids.append(np.mean(boot_sample)) + theta_2stds.append(2 * np.std(boot_sample)) + + # cast data lists to arrays for convenience + theta_vals = np.array(theta_vals) + theta_fids = np.array(theta_fids) + theta_2stds = np.array(theta_2stds) + fid_data = (theta_vals, theta_fids, theta_2stds) + self.plot_data = fid_data + + return fid_data + + def plot_theta_sweep(self, title=''): + """ + Plots [self.plot_data] assuming it was produced + from a theta sweep experiment. + """ + # unpack data + thetas, fids, l_ci, u_ci, p_errs = self.plot_data + # divide thetas by np to make easier to display + thetas = thetas / np.pi + # get diff between upper ci and mean value for errbar formatting + fid_up = u_ci - fids + fid_low = fids - l_ci + + # make thetas into more readable format + # finally plot it and add labels + plt.errorbar(thetas, fids, yerr=(fid_low, fid_up), fmt='.') + plt.xlabel('theta / pi') + plt.ylabel('fidelity') + plt.title(title) + + return plt + + def wrangle_fid_decay(self): + """ + Wrangles [self.working_data] assuming it was produced + from a fidelity decay experiment. + Outputs: + T_vals: array of DD sequence times in microseconds + haar_fids: array of fidelities associated to each time + haar_2stds: array of 2 standard deviation values associated with fids + """ + T_vals = [] + haar_fids = [] + haar_2stds = [] + + contracted_data = contract_by_T(self.working_data) + + for Tval, count_data in contracted_data.items(): + T_vals.append(float(Tval)) + num_qubits_measured, count_list = count_data + # iterate over counts for each of the haar states + succ_counts = [] + fail_counts = [] + for state_data in count_list: + succ, fail = get_success_failure_count(state_data, num_qubits_measured) + succ_counts.append(succ) + fail_counts.append(fail) + + # bootstrap over haar states + boot_sample = beta_bayesian_bootstrap(succ_counts, fail_counts, succ_counts[0] + fail_counts[0]) + haar_fids.append(np.mean(boot_sample)) + haar_2stds.append(2 * np.std(boot_sample)) + + # cast data lists to arrays for convenience + T_vals = np.array(T_vals) / 1000 # convert to \mu s + haar_fids = np.array(haar_fids) + haar_2stds = np.array(haar_2stds) + + self.plot_data = (T_vals, haar_fids, haar_2stds) + + return self.plot_data + + def wrangle_mistake_fid_decay(self): + """ + Wrangles [self.working_data] assuming it was produced + from a fidelity decay experiment. + Outputs: + T_vals: array of DD sequence times in microseconds + haar_fids: array of fidelities associated to each time + haar_2stds: array of 2 standard deviation values associated with fids + """ + T_vals = [] + haar_fids = [] + haar_2stds = [] + + contracted_data = contract_by_fixed_data_T(self.working_data) + + for Tval, count_data in contracted_data.items(): + T_vals.append(float(Tval)) + num_qubits_measured, count_list = count_data + count_list = fix_measurement_mistake(count_list) + # iterate over counts for each of the haar states + succ_counts = [] + fail_counts = [] + for state_data in count_list: + succ, fail = get_success_failure_count(state_data, num_qubits_measured) + succ_counts.append(succ) + fail_counts.append(fail) + + # bootstrap over haar states + boot_sample = beta_bayesian_bootstrap(succ_counts, fail_counts, succ_counts[0] + fail_counts[0]) + haar_fids.append(np.mean(boot_sample)) + haar_2stds.append(2 * np.std(boot_sample)) + + # cast data lists to arrays for convenience + T_vals = np.array(T_vals) / 1000 # convert to \mu s + haar_fids = np.array(haar_fids) + haar_2stds = np.array(haar_2stds) + + self.plot_data = (T_vals, haar_fids, haar_2stds) + + return self.plot_data + + def plot_fid_decay(self, with_fit=False, title='', legend=None): + """ + Plots [self.plot_data] assuming it was produced + from fidelity decay experiment. + """ + fig, ax = plt.subplots() + times, fids, errs = self.plot_data + prop = ax._get_lines.prop_cycler + color = next(prop)['color'] + if with_fit is True and legend is None: + legend = [] + # simple plot + if with_fit is False: + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ax.set_xlabel('time ($\\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + # plot with simple_exp fitting + else: + # get fit and plot it + fit_params, par_params, sum_res = self.bootstrap_fit_aug_exp() + tau = fit_params[0] + tau_err = fit_params[1] + a0 = par_params[0] + a0_err = par_params[1] + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ord_t = np.array(sorted(times)) + ax.plot(ord_t, aug_exp_func([a0, tau], ord_t), color=color) + # format the legend with the decay constant and 2 std value + st_tau = f'{tau:.2f}' + st_tau_err = f'{tau_err:.2f}' + legend.append(f'$\\tau$ = {st_tau} $\pm$ {st_tau_err}') + # add the 95% confidence intervals + ax.fill_between(ord_t, aug_exp_func([a0+a0_err, tau+tau_err], ord_t), aug_exp_func([a0-a0_err, tau-tau_err], ord_t), + color=color, alpha=.2) + # add labels + ax.set_xlabel('time ($\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + if legend is not None: + ax.legend(legend) + + return (fig, ax) + + def plot_cosfid_decay(self, with_fit=False, title='', seqs=None): + """ + Plots [self.plot_data] assuming it was produced + from fidelity decay experiment. + """ + times, fids, errs = self.plot_data + fig, ax = plt.subplots() + prop = ax._get_lines.prop_cycler + if seqs is None: + seqs = ['' for _ in range(len(self.plot_data))] + s_idx = 0 + legend = [] + color = next(prop)['color'] + # simple plot + if with_fit is False: + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ax.set_xlabel('time ($\\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + legend.append(seqs[s_idx]) + # plot with simple_exp fitting + else: + # get fit and plot it + fit_params, par_params, gam_params, sum_res = self.bootstrap_fit_aug_cosexp() + tau = fit_params[0] + tau_err = fit_params[1] + a0 = par_params[0] + a0_err = par_params[1] + gam = gam_params[0] + gam_err = gam_params[1] + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ord_t = np.array(sorted(times)) + ax.plot(ord_t, aug_cosexp_func([a0, tau, gam], ord_t), color=color) + # format the legend with the decay constant and 2 std value + st_tau = f'{tau:.2f}' + st_tau_err = f'{tau_err:.2f}' + if np.abs(gam) > 1000: + st_gam = "$\\infty$" + st_gam_err = "0" + else: + st_gam = f'{gam:.2f}' + st_gam_err = f'{gam_err:.2f}' + legend.append(f'{seqs[s_idx]}, $\\tau$ = {st_tau} $\pm$ {st_tau_err}, $\\gamma$ = {st_gam} $\pm$ {st_gam_err}') + # add the 95% confidence intervals + ax.fill_between(ord_t, aug_cosexp_func([a0+a0_err, tau+tau_err, gam+gam_err], ord_t), aug_cosexp_func([a0-a0_err, tau-tau_err, gam-gam_err], ord_t), + color=color, alpha=.2) + # add labels + ax.set_xlabel('time ($\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + s_idx += 1 + ax.legend(legend) + + return (fig, ax) + + def wrangle_tau_test(self): + """ + Wrangles [self.working_data] assuming it was produced + from a tau test. + Outputs: + tau_vals: array of DD sequence times in microseconds + haar_fids: array of fidelities associated to each tau + haar_2stds: array of 2 standard deviation values associated with fids + """ + tau_vals = [] + haar_fids = [] + haar_2stds = [] + + contracted_data = contract_by_tau(self.working_data) + + for tau, count_data in contracted_data.items(): + tau_vals.append(float(tau)) + num_qubits_measured, count_list = count_data + # iterate over counts for each of the haar states + succ_counts = [] + fail_counts = [] + for state_data in count_list: + succ, fail = get_success_failure_count(state_data, num_qubits_measured) + succ_counts.append(succ) + fail_counts.append(fail) + + # bootstrap over haar states + boot_sample = beta_bayesian_bootstrap(succ_counts, fail_counts, succ_counts[0] + fail_counts[0]) + haar_fids.append(np.mean(boot_sample)) + haar_2stds.append(2 * np.std(boot_sample)) + + # cast data lists to arrays for convenience + tau_vals = np.array(tau_vals) + haar_fids = np.array(haar_fids) + haar_2stds = np.array(haar_2stds) + + self.plot_data = (tau_vals, haar_fids, haar_2stds) + + return self.plot_data + + def plot_tau_test(self, with_fit=False, title='', seqs=None): + """ + Plots [self.plot_data] assuming it was produced + from tau test experiment. + """ + times, fids, errs = self.plot_data + fig, ax = plt.subplots() + prop = ax._get_lines.prop_cycler + if seqs is None: + seqs = ['' for _ in range(len(self.plot_data))] + s_idx = 0 + legend = [] + color = next(prop)['color'] + # simple plot + if with_fit is False: + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ax.set_xlabel('delay time (dt)') + ax.set_ylabel('fidelity') + ax.set_title(title) + legend.append(seqs[s_idx]) + # plot with simple_exp fitting + else: + # get fit and plot it + fit_params, par_params, gam_params, sum_res = self.bootstrap_fit_aug_cosexp() + tau = fit_params[0] + tau_err = fit_params[1] + a0 = par_params[0] + a0_err = par_params[1] + gam = gam_params[0] + gam_err = gam_params[1] + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ord_t = np.array(sorted(times)) + ax.plot(ord_t, aug_cosexp_func([a0, tau, gam], ord_t), color=color) + # format the legend with the decay constant and 2 std value + st_tau = f'{tau:.2f}' + st_tau_err = f'{tau_err:.2f}' + if np.abs(gam) > 1000: + st_gam = "$\\infty$" + st_gam_err = "0" + else: + st_gam = f'{gam:.2f}' + st_gam_err = f'{gam_err:.2f}' + legend.append(f'{seqs[s_idx]}, $\\tau$ = {st_tau} $\pm$ {st_tau_err}, $\\gamma$ = {st_gam} $\pm$ {st_gam_err}') + # add the 95% confidence intervals + ax.fill_between(ord_t, aug_cosexp_func([a0+a0_err, tau+tau_err, gam+gam_err], ord_t), aug_cosexp_func([a0-a0_err, tau-tau_err, gam-gam_err], ord_t), + color=color, alpha=.2) + # add labels + ax.set_xlabel('delay time (dt)') + ax.set_ylabel('fidelity') + ax.set_title(title) + s_idx += 1 + ax.legend(legend) + + return (fig, ax) + + def wrangle_idpad_test(self): + """ + Wrangles [self.working_data] assuming it was produced + from a tau test. + """ + idpad_vals = [] + haar_fids = [] + haar_2stds = [] + + contracted_data = contract_by_idpad(working_data) + + for id_pad, count_data in contracted_data.items(): + idpad_vals.append(id_pad) + num_qubits_measured, count_list = count_data + # iterate over counts for each of the haar states + succ_counts = [] + fail_counts = [] + for state_data in count_list: + succ, fail = get_success_failure_count(state_data, num_qubits_measured) + succ_counts.append(succ) + fail_counts.append(fail) + + # bootstrap over haar states + boot_sample = beta_bayesian_bootstrap(succ_counts, fail_counts, succ_counts[0] + fail_counts[0]) + haar_fids.append(np.mean(boot_sample)) + haar_2stds.append(2 * np.std(boot_sample)) + + # cast data lists to arrays for convenience + idpad_vals = np.array(idpad_vals) + haar_fids = np.array(haar_fids) + haar_2stds = np.array(haar_2stds) + + self.plot_data = (tau_vals, haar_fids, haar_2stds) + + return self.plot_data + + def wrangle_grover(self): + """ + Wrangles [self.working_data] assuming it was produced + from a grover experiment. + """ + marker_states = [] + haar_fids = [] + haar_2stds = [] + prob_amp_list = [] + + contracted_data = contract_by_marker(self.working_data) + + for markers, count_data in contracted_data.items(): + # turn markers into list with no whitespace + markers = markers.replace(" ", "").split(',') + marker_states.append(markers) + num_qubits_measured, count_list = count_data + # count_list is list of only one element in this case (comes from + # haar experiments where list of length # haar states) + count_dict = hex_dict_to_bin_dict(count_list[0], num_qubits_measured) + # flip the keys to match normal physics notation + phys_count_dict = {} + for key, value in count_dict.items(): + phys_count_dict[key[::-1]] = value + succ, fail = get_grover_succ_and_fail_num(phys_count_dict, num_qubits_measured, markers) + total_trials = succ + fail + prob_amps = {} + for key, value in phys_count_dict.items(): + prob_amps[key] = (value / total_trials) + + prob_amp_list.append(prob_amps) + + boot_sample = beta_bayesian_bootstrap([succ], [fail], total_trials) + haar_fids.append(np.mean(boot_sample)) + haar_2stds.append(2 * np.std(boot_sample)) + + # cast data lists to arrays for convenience + haar_fids = np.array(haar_fids) + haar_2stds = np.array(haar_2stds) + + self.grover_data = (marker_states, haar_fids, haar_2stds, prob_amp_list) + + return self.grover_data + + def plot_grover_heatmap(self): + """ + Plots a heatmap of grover data akin to that in + Figgatt et. al. Nature paper of 2017. + """ + markers, fids, errs, amp_dicts = self.grover_data + # get number of qubits in a measurement + num_q = len(list(amp_dicts[0].keys())[0]) + # form list of (ordered) basis states + basis_states = [''.join(i) for i in itertools.product('01', repeat=num_q)] + + # format data for heatmap + y_labels = [] + prob_amps = np.zeros((len(markers), len(basis_states))) + # format side success probabilities + side_fids = [] + for idx in range(len(fids)): + fid = f"{fids[idx] * 100:.1f}" + err = f"{errs[idx] * 100:.1f}" + side_fids.append(f"{fid}({err})") + side_fids = np.asarray([side_fids]) + for (i, m) in enumerate(markers): + y_lab = str(m).strip('[]').replace("'", "") + y_labels.append(y_lab) + for (j, state) in enumerate(basis_states): + prob = amp_dicts[i].get(state, 0) + prob_amps[(i, j)] = prob + + + # format the plot data + fig = plt.figure(figsize=(10,5)) + ax1 = plt.subplot2grid((20,20), (0,0), colspan=16, rowspan=19) + ax2 = plt.subplot2grid((20,20), (0,16), colspan=2, rowspan=19) + ax3 = plt.subplot2grid((20,20), (0,18), colspan=1, rowspan=19) + # set up main plot + sns.heatmap(prob_amps, ax=ax1, linewidth=0.5, xticklabels=basis_states, cbar=False) + ax1.set_yticklabels(y_labels, rotation=0, horizontalalignment='right') + ax1.set(xlabel='Detected State', ylabel='Marked State') + # set up the side bar with marked state success probability + sns.heatmap(np.array([fids]), ax=ax2, cmap = "YlGnBu", annot=side_fids, fmt="", cbar=False, xticklabels=False, yticklabels=False) + + fig.colorbar(ax1.get_children()[0], cax=ax3, orientation="vertical") + + return plt + +############################################################ +# Fitting Utilities +############################################################ + def perform_aug_exp_fit(self, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = aug_exp_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + return perform_aug_exp_fit(self.plot_data, seed) + + def perform_aug_cosexp_fit(self, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = aug_cosexp_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + return perform_aug_cosexp_fit(self.plot_data, seed) + + def bootstrap_fit_aug_exp(self, boot_samps=1000, seed=None): + """ + Bootstrap fit of [plots_data] to the aug_exp function. + """ + plot_data = self.plot_data + a0_list = [] + tau_list = [] + for i in range(boot_samps): + # generate sample of plot_data + samp_plot_data = gen_time_series_sample(plot_data) + # fit this data and add to lists + tau_fit, a0_fit, _ = perform_aug_exp_fit(samp_plot_data, seed) + a0_list.append(a0_fit[0]) + tau_list.append(tau_fit[0]) + + avg_a0 = np.mean(a0_list) + a0_std = 2*np.std(a0_list) + avg_tau = np.mean(tau_list) + tau_std = 2*np.std(tau_list) + + # obtain sum of residuals squared + t, fid, _ = plot_data + rsq_sum = np.sum((aug_exp_func([avg_a0, avg_tau], t) - fid)**2) + + return [avg_tau, tau_std], [avg_a0, a0_std], rsq_sum + + def bootstrap_fit_aug_cosexp(self, boot_samps=1000, seed=None): + """ + Bootstrap fit of [plots_data] to the aug_exp function. + """ + plot_data = self.plot_data + a0_list = [] + tau_list = [] + gam_list = [] + + # do fit on original data + tau_fit, a0_fit, gam_fit, _ = perform_aug_cosexp_fit(plot_data, seed) + a0 = a0_fit[0] + a0_list.append(a0) + tau = tau_fit[0] + tau_list.append(tau) + gam = gam_fit[0] + gam_list.append(gam) + seed = [a0, tau, gam] + + for i in range(boot_samps): + # generate sample of plot_data + samp_plot_data = gen_time_series_sample(plot_data) + # fit this data and add to lists + tau_fit, a0_fit, gam_fit, _ = perform_aug_cosexp_fit(samp_plot_data, seed) + a0_list.append(a0_fit[0]) + tau_list.append(tau_fit[0]) + gam_list.append(gam_fit[0]) + + avg_a0 = np.mean(a0_list) + a0_std = 2*np.std(a0_list) + avg_tau = np.mean(tau_list) + tau_std = 2*np.std(tau_list) + avg_gam = np.mean(gam_list) + gam_std = 2*np.std(gam_list) + + # obtain sum of residuals squared + t, fid, _ = plot_data + rsq_sum = np.sum((aug_cosexp_func([avg_a0, avg_tau, avg_gam], t) - fid)**2) + + return [avg_tau, tau_std], [avg_a0, a0_std], [avg_gam, gam_std], rsq_sum + + +# a better version than Bibek fit for now +def aug_exp_func(p, t): + """ + F(t) = 1/2(p[0]*e^{-t / p[1]} + 1) + """ + # define f(t) + return (1/2)*(p[0]*np.exp(-t/p[1]) + 1) + +def aug_cosexp_func(p, t): + """ + F(t) = 1/2(p[0]*e^{-t / p[1]} + 1)*Cos(p[2]*t) + """ + mid_val = p[0]*np.exp(-t/p[1])*np.cos(t/p[2]) + return (1/2)*(mid_val + 1) + + +def perform_aug_exp_fit(plot_data, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = aug_exp_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + t, fid, yerr = plot_data + ysig = (yerr / 2) + + def err_func(p, t, fid, err): + return ((fid - aug_exp_func(p, t)) / err)**2 + + if seed is None: + p_init = [1.0, 50.0] + else: + p_init = seed + out = scipy.optimize.leastsq(err_func, p_init, + args=(t, fid, ysig), full_output=1) + # collect fitting parameters + a0, tau = out[0] + covar = out[1] + # get sum of squared residuals + res_sum = np.sum((aug_exp_func([a0, tau], t) - fid)**2) + red_chi_sq = res_sum / (len(fid)- 2) + # get fitting errors provided covar is not None + if covar is not None: + if covar[0][0] is not None: + a0_err = np.sqrt(covar[0][0] * red_chi_sq) + else: + a0_err = 'undetermined' + if covar[1][1] is not None: + tau_err = np.sqrt(covar[1][1] * red_chi_sq) + else: + tau_err = 'undetermined' + else: + a0_err = 'undetermined' + tau_err = 'undetermined' + + return [tau, tau_err], [a0, a0_err], res_sum + +def perform_aug_cosexp_fit(plot_data, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = aug_cosexp_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + t, fid, yerr = plot_data + ysig = (yerr / 2) + + def err_func(p, t, fid, err): + return ((fid - aug_cosexp_func(p, t)) / err)**2 + + if seed is None: + p_init = [1.0, 50.0, 150.0] + else: + p_init = seed + out = scipy.optimize.leastsq(err_func, p_init, + args=(t, fid, ysig), full_output=1) + + # collect fitting parameters + a0, tau, gam = out[0] + covar = out[1] + # get sum of squared residuals + res_sum = np.sum((aug_cosexp_func([a0, tau, gam], t) - fid)**2) + red_chi_sq = res_sum / (len(fid) - 2) + # get errors if not None + if covar is not None: + if covar[0][0] is not None: + a0_err = np.sqrt(covar[0][0] * red_chi_sq) + else: + a0_err = 'undetermined' + if covar[1][1] is not None: + tau_err = np.sqrt(covar[1][1] * red_chi_sq) + else: + tau_err = 'undetermined' + if covar[2][2] is not None: + gam_err = np.sqrt(covar[2][2] * red_chi_sq) + else: + gam_err = 'undetermined' + else: + a0_err = 'undetermined' + tau_err = 'undetermined' + gam_err = 'undetermined' + + return [tau, tau_err], [a0, a0_err], [gam, gam_err], res_sum + +def gen_time_series_sample(plot_data): + """ + Given [plot_data] of the form [xdata, ydata, yerrs], + generates a new sample data set of the form + [xsamp, ysamp, yerrs] useful for bootstrapping. + + Assumes yerr is 2 \sigma. + """ + xs, ys, errs = plot_data + samp_ys = [] + # get 1 sigma errs instead for sampling + errs = errs / 2 + for i in range(len(ys)): + # generate sample y from normal dist + s_y = np.random.normal(ys[i], errs[i]) + samp_ys.append(s_y) + + return [xs, samp_ys, errs] +########################################################################### +# HELPER FUNCTIONS +########################################################################### +def hex_to_bin(hexstr, num_bits): + """ + Converts [hexstr] which is hexadecimal as string + into binary with [num_bits] padded 0s. For example, + 0x00 --> 0000 if num_bits is 4. + """ + base = 16 # hexadecimal is base 16 + return bin(int(hexstr, base))[2:].zfill(num_bits) + + +def hex_dict_to_bin_dict(hexdict, num_bits): + """ + Converts a dictionary with hexadecimal (str) keys into binary (str) keys + """ + return {hex_to_bin(key, num_bits): hexdict[key] for key in hexdict} + +def count_0s(istr): + """ + Counts the number of 0s in [istr] and returns that integer. + """ + num_0s = 0 + for s in istr: + if s == '0': + num_0s += 1 + return num_0s + +def dict_data_to_n0_array(data_dict): + """ + Converts a dictionary of the form {'101': 2, '000': 1}-->[101, 101, 000] + -->[1, 1, 3] where last list specifies how many zeros each bitstring + has. (We don't actually compute second list, but helps to see it.) + """ + # populate data_list with relative frequencies from dict + data_list = [] + for key, value in data_dict.items(): + for i in range(value): + # get number of zeros in this string and append to list + data_list.append(count_0s(key)) + + return np.array(data_list) + +def contract_by_T(working_data): + """ + Given that self.working_data is populated by tuple data, + contracts data into [contracted_data] dict by combining all + data with shared T into single data point, i.e. + contracted_data[T] = [{counts_dict1}, {counts_dict2}, ...] + where each counts_dict is over same T. + """ + contracted_data = {} + for tup_data in working_data: + split_label = tup_data[0].split('_') + T_idx = split_label.index('T') + # the [0:-2] removes the trailing units of ns + T_val = f"{float(split_label[T_idx + 1][0:-2]):.4f}" + + if T_val in contracted_data: + # check that number of qubits measured is the same + assert(contracted_data[T_val][0] == tup_data[1]) + # append additional data to T_val + contracted_data[T_val][1].append(copy.deepcopy(tup_data[2])) + else: + contracted_data[T_val] = (tup_data[1], [copy.deepcopy(tup_data[2])]) + + return contracted_data + +def contract_by_fixed_data_T(working_data): + """ + Given that self.working_data is populated by tuple data, + contracts data into [contracted_data] dict by combining all + data with shared T into single data point, i.e. + contracted_data[T] = [{counts_dict1}, {counts_dict2}, ...] + where each counts_dict is over same T. + """ + contracted_data = {} + for tup_data in working_data: + split_label = tup_data[0].split('_') + T_idx = split_label.index('T') + # the [0:-2] removes the trailing units of ns + T_val = f"{float(split_label[T_idx + 1][0:-2]):.4f}" + + if T_val in contracted_data: + # append additional data to T_val + contracted_data[T_val][1].append(copy.deepcopy(tup_data[2])) + else: + contracted_data[T_val] = (tup_data[1], [copy.deepcopy(tup_data[2])]) + + return contracted_data + +def contract_by_tau(working_data): + """ + Given that self.working_data is populated by tuple data, + contracts data into [contracted_data] dict by combining all + data with shared tau into single data point, i.e. + contracted_data[tau] = [{counts_dict1}, {counts_dict2}, ...] + where each counts_dict is over same tau. + """ + contracted_data = {} + for tup_data in working_data: + split_label = tup_data[0].split('_') + tau_idx = split_label.index('tau') + # the [0:-2] removes the trailing units of ns + tau_val = f"{float(split_label[tau_idx + 1][0:-2]):.4f}" + + if tau_val in contracted_data: + # check that number of qubits measured is the same + assert(contracted_data[tau_val][0] == tup_data[1]) + # append additional data to tau_val + contracted_data[tau_val][1].append(copy.deepcopy(tup_data[2])) + else: + contracted_data[tau_val] = (tup_data[1], [copy.deepcopy(tup_data[2])]) + + return contracted_data + +def contract_by_marker(working_data): + """ + Given that self.working_data is populated by tuple data, + contracts data into [contracted_data] dict by combining all + data with shared marker into single data point, i.e. + contracted_data[marker] = [{counts_dict1}, {counts_dict2}, ...] + where each counts_dict is over same marker. + """ + contracted_data = {} + for tup_data in working_data: + split_label = tup_data[0].split('_') + mark_idx = split_label.index('grover') + markers = split_label[mark_idx + 1].strip('[]') + + if markers in contracted_data: + # check that number of qubits measured is the same + assert(contracted_data[marker][0] == tup_data[1]) + # append additional data + contracted_data[markers][1].append(copy.deepcopy(tup_data[2])) + else: + contracted_data[markers] = (tup_data[1], [copy.deepcopy(tup_data[2])]) + + return contracted_data + + +def get_grover_succ_and_fail_num(dict_counts, num_qubits_measured, markers): + if type(markers) is not list: + markers = [markers] + + total_counts = 0 + success_num = 0 + for key, value in dict_counts.items(): + total_counts += value + if str(key) in markers: + success_num += value + + return success_num, (total_counts - success_num) + + +def get_success_failure_count(dict_counts, num_qubits_measured): + # results have hex keys--convert them to binary keys first + counts = hex_dict_to_bin_dict(dict_counts, num_qubits_measured) + # obtain the number 0s from each shot of experiment and create a (degenerate) array + # That is, if '001' shows up 50 times, populate array with 50 instances of 2 + num_0s_list = dict_data_to_n0_array(counts) + num_0s = sum(num_0s_list) + num_1s = (num_qubits_measured * len(num_0s_list) - num_0s) + return num_0s, num_1s + +################################################## +# Statistics (such as bootstrapping) +################################################## + +def safe_beta(a,b,n): + if a==0: + return np.zeros(n) + if b==0: + return np.ones(n) + if a!=0 and b!=0: + return np.random.beta(a,b,size=n) + +def beta_bayesian_bootstrap(successes,failures,n): + # successes = list of the number of successes in each gauge + # failures = list of the number of failures in each gauge + # n is number of samples + bb_weights=np.random.dirichlet(np.ones(len(successes)),n) + out=np.zeros(n) + for i in range(0,len(successes)): + out+=bb_weights[:,i]*safe_beta(successes[i],failures[i],n) + return out + +def bootstrapci(data, n=1000, func=np.mean, ci=.95): + """ + Generate `n` bootstrap samples, evaluating `func` + at each resampling. Then computes error bar with + 'ci' confidence interval on data. + """ + simulations = np.zeros(n) + sample_size = len(data) + xbar = func(data) + for c in range(n): + itersample = np.random.choice(data, size=sample_size, replace=True) + simulations[c] = func(itersample) + diff = np.sort(simulations - xbar) + lefterr_idx = int(np.floor(ci*n)) + righterr_idx = int(np.ceil((1 - ci)*n)) + + conf_int = (xbar - diff[lefterr_idx], xbar - diff[righterr_idx]) + + return conf_int + +def per_err(mu, errbar): + """ + Computes percent error with respect to mean. In particular, we calculate difference between + (mu - errbar[0]) / mu and (mu - errbar[1]) / mu and takes the larger of the two in abs value then + just multiply by 100. + """ + diff1 = abs(mu - errbar[0]) + diff2 = abs(mu - errbar[1]) + + perdif1 = (diff1 / mu) * 100 + perdif2 = (diff2 / mu) * 100 + + if perdif1 > perdif2: + return perdif1 + else: + return perdif2 + +def calc_exper_fid(counts, num_qubits_measured): + """ + Given an single experiment's results from IBMQ, calculates the fidelity and computes bootstrapped error + bars. By "fidelity" we mean the total number of zeros--that is, we assume experiment is designed to output + 0 at the end. (If you encode state--decode it!) + For example, if 3 qubit experiment with 10 shots gives results {'101': 3, '000': 5, '111': 2}, then the + total number of zeros is 3(1) + 5(3) + 2(0) = 18, and fid = 18 / (10 * 3). + + Input + ---------------- + exper_res -- "results" of IBMQ experiemnt run, i.e. if you run result = submit_job(...).to_dict(), then + this expects results['results'][j], i.e. the results of jth experiment. + num_qubits_measured -- int + + Output + ---------------- + fid -- tuple, (fidelity, -.95ci, +.95ci, p_err) -(+).95ci is lower(upper) bound of 95% confidence + interval via bootstrapping and err_mag is magnitude of error w.r.t. mean. + + Also prints if abs mag of error is less than input tol. + """ + # results have hex keys--convert them to binary keys first + counts = hex_dict_to_bin_dict(counts, num_qubits_measured) + # obtain the number 0s from each shot of experiment and create a (degenerate) array + # That is, if '001' shows up 50 times, populate array with 50 instances of 2 + num_0s = dict_data_to_n0_array(counts) + # turn num0s to fideltiy by dividing by number of qubits + fids = num_0s / num_qubits_measured + # calculate mean + mean_fid = np.mean(fids) + # calculate confidence interval with 1000 bootstrap samples and .95 confidence interval + ci = bootstrapci(fids, 1000, np.mean, .95) + # calculate percent error + p_err = per_err(mean_fid, ci) + + return (mean_fid, ci[0], ci[1], p_err) + +def fix_measurement_mistake(count_list): + """ + Take data where I accidently measured idle qubits + and ignore them to create single qubit counts. + """ + new_count_list = [] + for counts in count_list: + new_counts = {} + num_0 = 0 + num_1 = 0 + for key in counts: + binary = hex_to_bin(key, 5) + if binary[-1] == '0': + num_0 += counts[key] + else: + num_1 += counts[key] + new_counts['0x0'] = num_0 + new_counts['0x1'] = num_1 + new_count_list.append(new_counts) + + return new_count_list diff --git a/src/edd/data/_ibmq_data_test.py b/src/edd/data/_ibmq_data_test.py new file mode 100644 index 0000000..56b9a3a --- /dev/null +++ b/src/edd/data/_ibmq_data_test.py @@ -0,0 +1,152 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os.path + +from edd.data._ibmq_data import IBMQData +from edd.backend import IBMQBackend +from edd.circuit import IBMQDdCircuit +from edd.experiments import theta_sweep_frev + +import unittest + +class IBMQDataTest(unittest.TestCase): + """ Tests that IBMQData class works smoothly. """ + + # First, we test if we can initialize with no input data + def test_init_no_input(self): + test_data = IBMQData() + self.assertIs(test_data.data, None) + self.assertEqual(test_data.name, 'test') + + def test_init_with_result(self): + ourense = IBMQBackend('ibmq_ourense') + # build up some arbitrary simple circuit + circ = IBMQDdCircuit(5) + circ.x(0) + circ.measure_all() + # submit test job to get "Results" class object + results = ourense.submit_test(circ, 'test') + # init IBMQData with Results object + test_data = IBMQData(results, 'x_gate_data') + self.assertDictEqual(test_data.data, results.to_dict()) + self.assertEqual(test_data.name, 'x_gate_data') + + def test_init_with_dict_result(self): + ourense = IBMQBackend('ibmq_ourense') + # build up some arbitrary simple circuit + circ = IBMQDdCircuit(5) + circ.x(0) + circ.measure_all() + # submit test job to get "Results" class object + results = ourense.submit_test(circ, 'test') + # init IBMQData with dictionary data + test_data = IBMQData(results.to_dict(), 'x_gate_data') + self.assertDictEqual(test_data.data, results.to_dict()) + self.assertEqual(test_data.name, 'x_gate_data') + + def test_add_data(self): + ourense = IBMQBackend('ibmq_ourense') + # build up some arbitrary simple circuit + circ = IBMQDdCircuit(5) + circ.x(0) + circ.measure_all() + # submit test job to get "Results" class object + results = ourense.submit_test(circ, 'test') + # init IBMQData with Result object + test_data = IBMQData(results, 'x_gate_data') + # add same data set a second time as Results object + test_data.add_data(results) + self.assertIsInstance(test_data.data, list) + self.assertDictEqual(results.to_dict(), test_data.data[0]) + self.assertDictEqual(results.to_dict(), test_data.data[1]) + # add data a third time as a dict + test_data.add_data(results.to_dict()) + self.assertIsInstance(test_data.data, list) + self.assertDictEqual(results.to_dict(), test_data.data[0]) + self.assertDictEqual(results.to_dict(), test_data.data[1]) + self.assertDictEqual(results.to_dict(), test_data.data[2]) + + def test_save_data(self): + ourense = IBMQBackend('ibmq_ourense') + # build up some arbitrary simple circuit + circ = IBMQDdCircuit(5) + circ.x(0) + circ.measure_all() + # submit test job to get "Results" class object + results = ourense.submit_test(circ, 'test') + # init IBMQData with Results object + test_data = IBMQData(results, 'x_gate_data') + # save the data--this saves as .yml file + fname = 'test_x_gate_data.yml' + test_data.save_data(fname) + # check if file exists now + file_exists = os.path.exists(fname) + + self.assertIs(file_exists, True) + + def test_load_data(self): + ourense = IBMQBackend('ibmq_ourense') + # build up some arbitrary simple circuit + circ = IBMQDdCircuit(5) + circ.x(0) + circ.measure_all() + # submit test job to get "Results" class object + results = ourense.submit_test(circ, 'test') + # init IBMQData with Result object + test_data = IBMQData(results, 'x_gate_data') + # save the data + fname = 'test_x_gate_data.yml' + test_data.save_data(fname) + + # load the data and test whether it is added + test_data.load_data(fname) + self.assertIsInstance(test_data.data, list) + self.assertDictEqual(results.to_dict(), test_data.data[0]) + self.assertDictEqual(results.to_dict(), test_data.data[1]) + # load data again to see if added again + test_data.load_data(fname) + self.assertIsInstance(test_data.data, list) + self.assertDictEqual(results.to_dict(), test_data.data[0]) + self.assertDictEqual(results.to_dict(), test_data.data[1]) + self.assertDictEqual(results.to_dict(), test_data.data[2]) + + def test_full_wrangle_theta_sweep(self): + ourense = IBMQBackend('ibmq_ourense') + # run a quick theta sweep and use to init data set + results = theta_sweep_frev(ourense, 5, [0.209, .418]) + test_data = IBMQData(results, 'theta_sweep_test') + # perform theta_sweep full analysis (which tests all parts) + (data, fname, plot) = test_data.full_wrangle_theta_sweep() + + # assertions for data + self.assertIsInstance(data[0][0], str) + self.assertIsInstance(data[0][1], str) + self.assertIsInstance(data[1], tuple) + self.assertIsInstance(data[1][0][0], float) + self.assertIsInstance(data[1][1][0], float) + self.assertIsInstance(data[1][2][0], float) + self.assertIsInstance(data[1][3][0], float) + self.assertIsInstance(data[1][4][0], float) + + # assertions for fname + self.assertIsInstance(fname, str) + file_exists = os.path.exists(fname) + self.assertIs(file_exists, True) + + # assertions for plt + pltname = fname[0:-3] + 'png' + file_exists = os.path.exists(pltname) + self.assertIs(file_exists, True) + +if __name__=="__main__": + unittest.main() diff --git a/src/edd/experiments/__init__.py b/src/edd/experiments/__init__.py new file mode 100644 index 0000000..b35336a --- /dev/null +++ b/src/edd/experiments/__init__.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +typically this is a bad import, but I think since it keeps +'unpacked' namespace local to this package, it's okay to import +all the 'utility' experiment functions in this way +""" +#TODO: Figure out if the above statement is actually true +import edd.experiments._circuit_experiments as circ +import edd.experiments._pulse_experiments as pulse diff --git a/src/edd/experiments/_circuit_experiments.py b/src/edd/experiments/_circuit_experiments.py new file mode 100644 index 0000000..ccb9472 --- /dev/null +++ b/src/edd/experiments/_circuit_experiments.py @@ -0,0 +1,585 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the Licens + +import datetime +import numpy as np +import os +from edd.circuit import IBMQDdCircuit +from edd.data import IBMQData + +###################################################################### +# This file hosts functions which carry out experiments on IBMQ +###################################################################### + +###################################################################### +# THETA SWEEP FUNCTIONS +# Description: Generically, these experiemnts test the efficacy of +# different DD sequences over a range of easy to prepare superposition +# states of the form cos(t/2)|0> + sin(t/2)|1>. This prevents undue +# biasing due to robustness of the |0> state. +###################################################################### +def theta_sweep_free(num_ids, backend, encoding_qubits='all', + dd_qubits='all', theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- free -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [num_ids] identity gates is applied on [dd_qubits]. + -->[backend] is needed to specify DD circuit time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_circ = IBMQDdCircuit(1, ibmq_backend=backend) + time_circ.add_free(0, num_ids) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"theta_sweep_free_encodeqs_{encoding_qubits}_ddqs_{dd_qubits}_" + job_tag += f"ids_{num_ids}_T_{T}ns" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a circuit with correct number of qubits + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=exp_tag.format(t), ibmq_backend=backend) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + circ.encode_theta_state(encoding_qubits, t) + # apply identity gates + circ.add_free(dd_qubits, num_ids) + # decode superposition state back into |0> state + circ.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + circ.measure(q, idx) + experiments.append(circ) + + return (job_tag, experiments) + +def theta_sweep_dd(seq_name, num_reps, id_pad, backend, encoding_qubits='all', + dd_qubits='all', theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- DD seq -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [id_pad] identities between pulses. + -->[backend] is needed to specify DD circuit time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # prepend the 'add' to seq name string + seq_method = 'add_' + seq_name + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this 'DD' sequence + time_circ = IBMQDdCircuit(1, ibmq_backend=backend) + getattr(time_circ, seq_method)(0, num_reps, id_pad) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"theta_sweep_{seq_name}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_idpad_{id_pad}_T_{T}ns" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a circuit with correct number of qubits + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=exp_tag.format(t), ibmq_backend=backend) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + circ.encode_theta_state(encoding_qubits, t) + # apply DD sequence with correct parameters + getattr(circ, seq_method)(0, num_reps, id_pad) + # decode superposition state back into |0> state + circ.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + circ.measure(q, idx) + experiments.append(circ) + + return (job_tag, experiments) + +###################################################################### +# Fideltiy Decay Functions +# Description: Experimentally measures the fidelity of different DD +# sequenences as a function of total sequence time. To be specific, +# suppose XY4 sequence takes 100ns to run. Then We'd measure +# the fidelity for XY4 1 rep (100ns), 2 reps (200ns), etc... +# Ideally, we'd like to know the fidelity "on average" for a "typical +# state", so perform the DD sequence on a list of haar random states. +###################################################################### + +############################################################ +# Type 1 Fidelity Decay +# Here, we allow the function to take as input which haar +# random states to try and protect for all times tried. +# That is, we try same inputted haar random states for 1 rep +# of XY4 as we do for 2 reps of XY4. +############################################################ +def static_haar_fid_decay_free(haar_params_list, num_ids, backend, + encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- free -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + [num_ids] identity gates is applied on [dd_qubits]. + -->[backend] is needed to specify DD circuit time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_circ = IBMQDdCircuit(1, ibmq_backend=backend) + time_circ.add_free(0, num_ids) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"static_haar_fid_decay_free_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_ids_{num_ids}_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=exp_tag.format(t, p, l), ibmq_backend=backend) + circ.u3(t, p, l, encoding_qubits) + circ.barrier(encoding_qubits) + circ.add_free(dd_qubits, num_ids) + circ.barrier(encoding_qubits) + circ.u3(-t, -l, -p, encoding_qubits) + circ.barrier(encoding_qubits) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + circ.measure(q, idx) + circ.draw() + # append this circuit to experiments + experiments.append(circ) + + return (job_tag, experiments) + +def static_haar_fid_decay_dd(haar_params_list, seq_name, num_reps, id_pad, + backend, encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector) and n \in num_id_list of the form: + |0> -- U3(t,0,0) -- DD seq -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [id_pad] identities between pulses. + -->[backend] is needed to specify DD circuit time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # prepend the 'add' to seq name string + seq_method = 'add_' + seq_name + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many reps of dd seq + time_circ = IBMQDdCircuit(1, ibmq_backend=backend) + getattr(time_circ, seq_method)(0, num_reps, id_pad) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"static_haar_fid_decay_{seq_name}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_idpad_{id_pad}_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=exp_tag.format(t, p, l), ibmq_backend=backend) + circ.u3(t, p, l, encoding_qubits) + circ.barrier(encoding_qubits) + getattr(circ, seq_method)(dd_qubits, num_reps, id_pad) + circ.barrier(encoding_qubits) + circ.u3(-t, -l, -p, encoding_qubits) + circ.barrier(encoding_qubits) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + circ.measure(q, idx) + circ.draw() + # append this circuit to experiments + experiments.append(circ) + + return (job_tag, experiments) + +def pauli_pode_fid_decay_free(offset, tau, backend, + encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments which probe fidelity of Pauli podes. + |0> -- encode_pode_state(offset) -- free -- decode_pode_state(offset), + where encoding is prepared on [encoding_qubits] and + [num_ids] identity gates is applied on [dd_qubits]. + -->[backend] is needed to specify DD circuit time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + n_qubits = backend.get_number_qubits() + if encoding_qubits == 'all': + encoding_qubits = list(range(n_qubits)) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_circ = IBMQDdCircuit(n_qubits, ibmq_backend=backend) + time_circ.add_free(0, tau) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"pauli_pode_offset_{offset}_fid_decay_free_encodeqs" + job_tag += f"_{encoding_qubits}_ddqs_{dd_qubits}_tau_{tau}_T_{T}ns" + exp_tag = job_tag + "_pode_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for pode in range(6): + # create circuit + #n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=exp_tag.format(pode), ibmq_backend=backend) + circ.barrier(encoding_qubits) + circ.encode_podal_state(encoding_qubits, pode, offset) + circ.barrier(encoding_qubits) + circ.add_free(dd_qubits, num_ids) + circ.barrier(encoding_qubits) + circ.decode_podal_state(encoding_qubits, pode, offset) + circ.barrier(encoding_qubits) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + circ.measure(q, idx) + circ.draw() + # append this circuit to experiments + experiments.append(circ) + + return (job_tag, experiments) + + +def pauli_pode_fid_decay_dd(offset, seq_name, num_reps, tau, + backend, encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments which probe fidelity of Pauli podes. + |0> -- encode_pode_state(offset) -- DD -- decode_pode_state(offset), + where encoding done on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [id_pad] identities between pulses. + -->[backend] is needed to specify DD circuit time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # prepend the 'add' to seq name string + seq_method = 'add_' + seq_name + # if encoding_qubits set to 'all,' get all backend qubits + n_qubits = backend.get_number_qubits() + if encoding_qubits == 'all': + encoding_qubits = list(range(n_qubits)) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many reps of dd seq + time_circ = IBMQDdCircuit(n_qubits, ibmq_backend=backend) + getattr(time_circ, seq_method)(0, 1, tau) + T = (time_circ.get_phys_time(backend) * num_reps) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = f"circ_pauli_pode_offset_{offset}_" + job_tag += f"fid_decay_{seq_name}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_tau_{tau}_T_{T}ns" + exp_tag = job_tag + "_pode_{}" + + #n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + + # create single rep of DD seq + sing_dd_circ = IBMQDdCircuit(n_qubits, n_bits, ibmq_backend=backend) + getattr(sing_dd_circ, seq_method)(dd_qubits, 1, tau) + + # create dd circ with number of reps desired + dd_circ = IBMQDdCircuit(n_qubits, n_bits, ibmq_backend=backend) + for j in range(num_reps): + dd_circ.extend(sing_dd_circ) + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for pode in range(6): + circ = IBMQDdCircuit(n_qubits, n_bits, + name=exp_tag.format(pode), ibmq_backend=backend) + circ.encode_podal_state(encoding_qubits, pode, offset) + circ.barrier(encoding_qubits) + circ.extend(dd_circ) + circ.barrier(encoding_qubits) + circ.decode_podal_state(encoding_qubits, pode, offset) + circ.barrier(encoding_qubits) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + circ.measure(q, idx) + circ.draw() + # append this circuit to experiments + experiments.append(circ) + + return (job_tag, experiments) + +############################################################ +# Type 2 Fidelity Decay +# Here, the haar random states which we try to protect with +# a DD sequence are randomly generated on the fly. In this +# case, there's no chance that XY4 1 rep and XY4 2 rep are +# protecting the same states, so convergence would require +# a much larger sample size of haar random states (we think). +############################################################ + +############################################################ +# Type 3 Fidelity over Entangled States +# Rather than try a set of haar-random states, here we simply +# prepare various entangled states and try to protect them +# with various DD sequences. +############################################################ +def bell_fid_decay_free(qubit_pairs, num_ids, backend, + dd_qubits='all'): + """ + -->Generates list of experiments indexed by q \in qubit_pairs_list + (q is tuple) of the form: + |0> -- bell_state_prep -- free -- bell_state_decode -- measure + |0> -- bell_state_prep -- free -- bell_state_decode -- measure, + where bell_states are prepared between [qubit_pairs] and [num_ids] + identity gates applied to qubits in [dd_qubits]. + -->[backend] is needed to specify DD circuit time. + -->If [dd_qubits] = 'all', then set dd_qubits = union([qubit_pairs]). + """ + # get list of qubits from qubit_pairs + encoding_qubits = [] + for pair in qubit_pairs: + encoding_qubits.append(pair[0]) + encoding_qubits.append(pair[1]) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + + # get T from building a test circuit over a single qubit + time_circ = IBMQDdCircuit(1, ibmq_backend=backend) + time_circ.add_free(0, num_ids) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"bell_fid_decay_free_pairs_{qubit_pairs}_ddqs_{dd_qubits}_" + job_tag += f"ids_{num_ids}_T_{T}ns" + exp_tag = job_tag + "_{}" + + experiments = [] + # first, add the phi plus bell state + # want to "access" all qubits from 0 to n since connections + # are non-linear, but only use/measure len(qubits) of them + phi_plus_tag = exp_tag.format('phi+') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=phi_plus_tag, ibmq_backend=backend) + circ.encode_bell_phi_plus(qubit_pairs) + circ.barrier(encoding_qubits) + circ.add_free(dd_qubits, num_ids) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_phi_plus(qubit_pairs, measure=True) + experiments.append(circ) + + # next add phi minus bell state following same steps + phi_minus_tag = exp_tag.format('phi-') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=phi_minus_tag, ibmq_backend=backend) + circ.encode_bell_phi_minus(qubit_pairs) + circ.barrier(encoding_qubits) + circ.add_free(dd_qubits, num_ids) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_phi_minus(qubit_pairs, measure=True) + experiments.append(circ) + + # add psi plus bell state + psi_plus_tag = exp_tag.format('psi+') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=psi_plus_tag, ibmq_backend=backend) + circ.encode_bell_psi_plus(qubit_pairs) + circ.barrier(encoding_qubits) + circ.add_free(dd_qubits, num_ids) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_psi_plus(qubit_pairs, measure=True) + experiments.append(circ) + + # add psi minus bell state + psi_minus_tag = exp_tag.format('psi-') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=psi_minus_tag, ibmq_backend=backend) + circ.encode_bell_psi_minus(qubit_pairs) + circ.barrier(encoding_qubits) + circ.add_free(dd_qubits, num_ids) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_psi_minus(qubit_pairs, measure=True) + experiments.append(circ) + + return (job_tag, experiments) + +def bell_fid_decay_dd(qubit_pairs, seq_name, num_reps, id_pad, backend, + dd_qubits='all'): + """ + -->Generates list of experiments indexed by q \in qubit_pairs_list + (q is tuple) of the form: + |0> -- bell_state_prep -- dd_seq -- bell_state_decode -- measure + |0> -- bell_state_prep -- dd_seq -- bell_state_decode -- measure, + where bell_states are prepared between [qubit_pairs] and + dd_seq is applied to [dd_qubits] for [num_reps] repetitions with + [id_pad] identities between dd pulses. + -->[backend] is needed to specify DD circuit time. + -->If [dd_qubits] = 'all', then set dd_qubits = union([qubit_pairs]) + """ + seq_method = 'add_' + seq_name + # get list of qubits from qubit_pairs + encoding_qubits = [] + for pair in qubit_pairs: + encoding_qubits.append(pair[0]) + encoding_qubits.append(pair[1]) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get T from building a test circuit over a single qubit + time_circ = IBMQDdCircuit(1, ibmq_backend=backend) + getattr(time_circ, seq_method)(0, num_reps, id_pad) + T = time_circ.get_phys_time(backend) + # hvaing parsed input args, set job and exp tags which associate + # input params to circuit + job_tag = "circ_" + job_tag += f"bell_fid_decay_{seq_name}_pairs_{qubit_pairs}_ddqs_{dd_qubits}_" + job_tag += f"reps_{num_reps}_idpad_{id_pad}_T_{T}ns" + exp_tag = job_tag + "_{}" + + experiments = [] + # first, add the phi plus bell state + # want to "access" all qubits from 0 to n since connections + # are non-linear, but only use/measure len(qubits) of them + phi_plus_tag = exp_tag.format('phi+') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=phi_plus_tag, ibmq_backend=backend) + circ.encode_bell_phi_plus(qubit_pairs) + circ.barrier(encoding_qubits) + getattr(circ, seq_method)(dd_qubits, num_reps, id_pad) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_phi_plus(qubit_pairs, measure=True) + experiments.append(circ) + + # next add phi minus bell state following same steps + phi_minus_tag = exp_tag.format('phi-') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=phi_minus_tag, ibmq_backend=backend) + circ.encode_bell_phi_minus(qubit_pairs) + circ.barrier(encoding_qubits) + getattr(circ, seq_method)(dd_qubits, num_reps, id_pad) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_phi_minus(qubit_pairs, measure=True) + experiments.append(circ) + + # add psi plus bell state + psi_plus_tag = exp_tag.format('psi+') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=psi_plus_tag, ibmq_backend=backend) + circ.encode_bell_psi_plus(qubit_pairs) + circ.barrier(encoding_qubits) + getattr(circ, seq_method)(dd_qubits, num_reps, id_pad) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_psi_plus(qubit_pairs, measure=True) + experiments.append(circ) + + # add psi minus bell state + psi_minus_tag = exp_tag.format('psi-') + n_qubits = max(max(encoding_qubits), max(dd_qubits)) + 1 + n_bits = len(encoding_qubits) + circ = IBMQDdCircuit(n_qubits, n_bits, + name=psi_minus_tag, ibmq_backend=backend) + circ.encode_bell_psi_minus(qubit_pairs) + circ.barrier(encoding_qubits) + getattr(circ, seq_method)(dd_qubits, num_reps, id_pad) + circ.barrier(encoding_qubits) + # this decoding automatically adds measurements + circ.decode_bell_psi_minus(qubit_pairs, measure=True) + experiments.append(circ) + + return (job_tag, experiments) diff --git a/src/edd/experiments/_circuit_experiments_test.py b/src/edd/experiments/_circuit_experiments_test.py new file mode 100644 index 0000000..3634106 --- /dev/null +++ b/src/edd/experiments/_circuit_experiments_test.py @@ -0,0 +1,314 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.backend import IBMQBackend +import edd.experiments as edde + +# type check imports +from edd.circuit import IBMQDdCircuit + +import unittest + +class CircuitExperimentsTest(unittest.TestCase): + """ Tests that experiments run smoothly. """ + + def test_theta_sweep_free_default(self): + '''test theta_sweep_free with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + num_ids = 5 + experiments = edde.circ.theta_sweep_free(num_ids, london) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + time = num_ids * id_time + qubits = [n for n in range(london.get_number_qubits())] + job_tag = f"theta_sweep_free_encodeqs_{qubits}_ddqs_{qubits}_" + job_tag += f"ids_{num_ids}_T_{time}ns" + self.assertEquals(experiments[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(experiments[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(experiments[1][0].name, exp_tag1) + + def test_theta_sweep_free_not_default(self): + '''test theta_sweep_free with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + # this is the most complicated case with non-contiguous + # encoding_qubits and dd_qubits + e_qubits = [0, 3] + dd_qubits = [1, 2, 4] + num_ids = 5 + exps = edde.circ.theta_sweep_free(num_ids, london, e_qubits, dd_qubits) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + time = num_ids * id_time + job_tag = f"theta_sweep_free_encodeqs_{e_qubits}_ddqs_{dd_qubits}_" + job_tag += f"ids_{num_ids}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_theta_sweep_dd_default(self): + '''test theta_sweep_dd with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + num_reps = 5 + id_pad = 1 + exps = edde.circ.theta_sweep_dd('xy4', num_reps, id_pad, london) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + u3_time = london.get_gate_times()['u3'] + # there are 4 u3 gates for each rep of xy4 and 5 reps + # then there is 1 identity gate in between each pulse + time = (4 * num_reps) * (u3_time + id_time) + qubits = [n for n in range(london.get_number_qubits())] + job_tag = f"theta_sweep_xy4_encodeqs_{qubits}_ddqs_{qubits}_" + job_tag += f"reps_{num_reps}_idpad_{id_pad}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_theta_sweep_dd_not_default(self): + # load backend and create experiments + london = IBMQBackend('ibmq_london') + # this is the most complicated case with non-contiguous + # encoding_qubits and dd_qubits + e_qubits = [0, 3] + dd_qubits = [1, 2, 4] + num_reps = 5 + id_pad = 1 + exps = edde.circ.theta_sweep_dd('xy4', num_reps, id_pad, london, + e_qubits, dd_qubits) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + u3_time = london.get_gate_times()['u3'] + # there are 4 u3 gates for each rep of xy4 + # then there is 1 identity gate in between each pulse + time = (4 * num_reps) * (u3_time + id_time) + job_tag = f"theta_sweep_xy4_encodeqs_{e_qubits}_ddqs_{dd_qubits}_" + job_tag += f"reps_{num_reps}_idpad_{id_pad}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_static_haar_fid_decay_free_default(self): + '''tests static_haar_fid_decay_free with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + num_ids = 10 + exps = edde.circ.static_haar_fid_decay_free(haar_params_list, + num_ids, london) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + time = num_ids * id_time + qubits = [n for n in range(london.get_number_qubits())] + job_tag = f"static_haar_fid_decay_free_encodeqs_{qubits}_" + job_tag += f"ddqs_{qubits}_ids_{num_ids}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_static_haar_fid_decay_free_not_default(self): + # load backend and create experiments + london = IBMQBackend('ibmq_london') + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + num_ids = 10 + e_qubits = [1, 4] + dd_qubits = [1, 3] + exps = edde.circ.static_haar_fid_decay_free(haar_params_list, + num_ids, london, + e_qubits, dd_qubits) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + time = num_ids * id_time + job_tag = f"static_haar_fid_decay_free_encodeqs_{e_qubits}_" + job_tag += f"ddqs_{dd_qubits}_ids_{num_ids}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + def test_static_haar_fid_decay_dd_default(self): + '''tests static_haar_fid_decay_dd with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + num_reps = 5 + id_pad = 1 + exps = edde.circ.static_haar_fid_decay_dd(haar_params_list, 'xy4', + num_reps, id_pad, london) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + u3_time = london.get_gate_times()['u3'] + time = (4 * num_reps) * (u3_time + id_time) + qubits = [n for n in range(london.get_number_qubits())] + job_tag = f"static_haar_fid_decay_xy4_encodeqs_{qubits}_" + job_tag += f"ddqs_{qubits}_reps_{num_reps}_idpad_{id_pad}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_static_haar_fid_decay_dd_not_default(self): + # load backend and create experiments + london = IBMQBackend('ibmq_london') + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + num_reps = 5 + id_pad = 1 + e_qubits = [1, 4] + dd_qubits = [1, 3] + exps = edde.circ.static_haar_fid_decay_dd(haar_params_list, 'xy4', + num_reps, id_pad, london, + e_qubits, dd_qubits) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + u3_time = london.get_gate_times()['u3'] + time = (4 * num_reps) * (u3_time + id_time) + job_tag = f"static_haar_fid_decay_xy4_encodeqs_{e_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_idpad_{id_pad}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_bell_fid_decay_free_default(self): + '''tests bell_fid_decay_free with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + qubit_pairs = [(0, 1), (3, 4)] + num_ids = 5 + exps = edde.circ.bell_fid_decay_free(qubit_pairs, num_ids, london) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + time = num_ids * id_time + qubits = [0, 1, 3, 4] + job_tag = f"bell_fid_decay_free_pairs_{qubit_pairs}_ddqs_{qubits}_" + job_tag += f"ids_{num_ids}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_phi+" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_bell_fid_decay_free_not_default(self): + # load backend and create experiments + london = IBMQBackend('ibmq_london') + qubit_pairs = [(0, 1), (3, 4)] + num_ids = 5 + dd_qubits = [1, 2, 4] + exps = edde.circ.bell_fid_decay_free(qubit_pairs, num_ids, london, + dd_qubits) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + time = num_ids * id_time + job_tag = f"bell_fid_decay_free_pairs_{qubit_pairs}_ddqs_{dd_qubits}_" + job_tag += f"ids_{num_ids}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_phi+" + self.assertEquals(exps[1][0].name, exp_tag1) + + def test_bell_fid_decay_dd_default(self): + '''tests bell_fid_decay_dd with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + qubit_pairs = [(0, 1), (3, 4)] + num_reps = 5 + id_pad = 1 + exps = edde.circ.bell_fid_decay_dd(qubit_pairs, 'xy4', num_reps, + id_pad, london) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + u3_time = london.get_gate_times()['u3'] + time = (4 * num_reps) * (u3_time + id_time) + qubits = [0, 1, 3, 4] + job_tag = f"bell_fid_decay_xy4_pairs_{qubit_pairs}_ddqs_{qubits}_" + job_tag += f"reps_{num_reps}_idpad_{id_pad}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_phi+" + self.assertEquals(exps[1][0].name, exp_tag1) + + def test_bell_fid_decay_dd_not_default(self): + '''tests bell_fid_decay_dd with default args''' + # load backend and create experiments + london = IBMQBackend('ibmq_london') + qubit_pairs = [(0, 1), (3, 4)] + num_reps = 5 + id_pad = 1 + dd_qubits = [1, 2, 4] + exps = edde.circ.bell_fid_decay_dd(qubit_pairs, 'xy4', num_reps, + id_pad, london, dd_qubits) + + # first check that job title is as expected + id_time = london.get_gate_times()['id'] + u3_time = london.get_gate_times()['u3'] + time = (4 * num_reps) * (u3_time + id_time) + job_tag = f"bell_fid_decay_xy4_pairs_{qubit_pairs}_ddqs_{dd_qubits}_" + job_tag += f"reps_{num_reps}_idpad_{id_pad}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdCircuit) + exp_tag1 = job_tag + "_phi+" + self.assertEquals(exps[1][0].name, exp_tag1) + + + + +if __name__=="__main__": + unittest.main() + diff --git a/src/edd/experiments/_pulse_experiments.py b/src/edd/experiments/_pulse_experiments.py new file mode 100644 index 0000000..3cc703f --- /dev/null +++ b/src/edd/experiments/_pulse_experiments.py @@ -0,0 +1,931 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the Licens + +import datetime +import numpy as np +import os +from edd.pulse import IBMQDdSchedule + +###################################################################### +# Hosts functions for experiments using the OpenPulse API +###################################################################### + +# Load in relevant state input data +import pathlib +path = pathlib.Path(__file__).parent.resolve() +u3_params = np.load(os.path.join(path, "../states/u3_list.npy")) + +###################################################################### +# THETA SWEEP FUNCTIONS +# Description: Generically, these experiemnts test the efficacy of +# different DD sequences over a range of easy to prepare superposition +# states of the form cos(t/2)|0> + sin(t/2)|1>. This prevents undue +# biasing due to robustness of the |0> state. +###################################################################### +def theta_sweep_free(num_ids, backend, basis, encoding_qubits='all', + dd_qubits='all', theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- free -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [num_ids] identity gates is applied on [dd_qubits]. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_free(0, num_ids) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"theta_sweep_free_encodeqs_{encoding_qubits}_ddqs_{dd_qubits}_" + job_tag += f"ids_{num_ids}_T_{T}ns" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a schedule with correct number of qubits + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t)) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + sched.encode_theta_state(encoding_qubits, t) + # apply identity gatesb + sched.add_free(dd_qubits, num_ids) + # decode superposition state back into |0> state + sched.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + #TODO: add support for choosing classical register + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + experiments.append(sched) + + return (job_tag, experiments) + +def theta_sweep_dd(seq_name, num_reps, tau, backend, basis, + encoding_qubits='all', dd_qubits='all', + theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- DD seq -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [tau] pause between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # prepend the 'add' to seq name string + seq_method = 'add_' + seq_name + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this 'DD' sequence + time_sched = IBMQDdSchedule(backend, basis) + getattr(time_sched, seq_method)(0, num_reps, tau) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + phys_tau = ns_time_to_dt(tau, time_sched.dt) + job_tag = f"pulse_{basis}_" + job_tag += f"theta_sweep_{seq_name}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_tau_{phys_tau}ns_T_{T}ns" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a schedule with correct number of qubits + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t)) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + sched.encode_theta_state(encoding_qubits, t) + # apply DD sequence with correct parameters + getattr(sched, seq_method)(0, num_reps, tau) + # decode superposition state back into |0> state + sched.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + #TODO: add support for choosing classical regsiter + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + experiments.append(sched) + + return (job_tag, experiments) + +def theta_sweep_uddx(n, time, backend, basis, encoding_qubits='all', + dd_qubits='all', theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- udd_x -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + UDDx_n is applied on [dd_qubits] [num_reps] times + in repetition with [tau] pause between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_uddx(0, n, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"theta_sweep_uddx_{n}_T_{time}dt_{T}ns_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a schedule with correct number of qubits + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t)) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + sched.encode_theta_state(encoding_qubits, t) + # apply uddx_[n] sequence + sched.add_uddx(dd_qubits, n, time) + # decode superposition state back into |0> state + sched.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + #TODO: add support for choosing classical regsiter + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + experiments.append(sched) + + return (job_tag, experiments) + +def theta_sweep_uddy(n, time, backend, basis, encoding_qubits='all', + dd_qubits='all', theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- udd_y -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + UDDx_n is applied on [dd_qubits] [num_reps] times + in repetition with [tau] pause between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_uddy(0, n, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"theta_sweep_uddy_{n}_T_{time}dt_{T}ns_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a schedule with correct number of qubits + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t)) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + sched.encode_theta_state(encoding_qubits, t) + # apply uddx_[n] sequence + sched.add_uddy(dd_qubits, n, time) + # decode superposition state back into |0> state + sched.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + #TODO: add support for choosing classical regsiter + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + experiments.append(sched) + + return (job_tag, experiments) + +def theta_sweep_qdd(n, m, time, backend, basis, encoding_qubits='all', + dd_qubits='all', theta_list=np.linspace(0, np.pi, 16)): + """ + -->Generates list of experiments indexed by t \in theta_list of form: + |0> -- U3(t,0,0) -- qdd -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + QDD_{n}_{m} is applied on [dd_qubits] [num_reps] times + in repetition with [tau] pause between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_qdd(0, n, m, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"theta_sweep_qdd_{n}_{m}_T_{time}dt_{T}ns_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}" + exp_tag = job_tag + "_theta_{}" + + # create the list of experiments to run as batch job on QC + experiments = [] + for t in theta_list: + # create a schedule with correct number of qubits + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t)) + # prepare cos(theta / 2) |0> + cos(theta / 2) |1> state + sched.encode_theta_state(encoding_qubits, t) + # apply uddx_[n] sequence + sched.add_qdd(dd_qubits, n, m, time) + # decode superposition state back into |0> state + sched.decode_theta_state(encoding_qubits, t) + # measure encoded qubits + #TODO: add support for choosing classical regsiter + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + experiments.append(sched) + + return (job_tag, experiments) + +###################################################################### +# Fideltiy Decay Functions +# Description: Experimentally measures the fidelity of different DD +# sequenences as a function of total sequence time. To be specific, +# suppose XY4 sequence takes 100ns to run. Then We'd measure +# the fidelity for XY4 1 rep (100ns), 2 reps (200ns), etc... +# Ideally, we'd like to know the fidelity "on average" for a "typical +# state", so perform the DD sequence on a list of haar random states. +###################################################################### + +############################################################ +# Type 1 Fidelity Decay +# Here, we allow the function to take as input which haar +# random states to try and protect for all times tried. +# That is, we try same inputted haar random states for 1 rep +# of XY4 as we do for 2 reps of XY4. +############################################################ +def static_haar_fid_decay_free(haar_params_list, time, backend, basis, + encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- free -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + idenity (i.e. pause) is applied for [time]dt units of time. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_pause(0, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_free_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + sched.add_pause(dd_qubits, time) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def static_haar_fid_decay_dd(haar_params_list, seq_name, num_reps, tau, backend, + basis, encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector) and n \in num_id_list of the form: + |0> -- U3(t,0,0) -- DD seq -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [tau] identities between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # prepend the 'add' to seq name string + seq_method = 'add_' + seq_name + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many reps of dd seq + time_sched = IBMQDdSchedule(backend, basis) + getattr(time_sched, seq_method)(0, num_reps, tau) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_{seq_name}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_tau_{tau}dt_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + getattr(sched, seq_method)(dd_qubits, num_reps, tau) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def pauli_pode_fid_decay_free(offset, time, backend, basis, + encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments which probe fidelity of Pauli podes. + |0> -- encode_pode_state(offset) -- free -- decode_pode_state(offset), + where encoding is prepared on [encoding_qubits] and + idenity (i.e. pause) is applied for [time]dt units of time. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_pause(0, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_pauli_pode_offset_{offset}_" + job_tag += f"fid_decay_free_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_T_{T}ns" + exp_tag = job_tag + "_pode_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for pode in range(6): + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(pode)) + sched.encode_podal_state(encoding_qubits, pode, offset) + sched.add_pause(dd_qubits, time) + sched.decode_podal_state(encoding_qubits, pode, offset) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def pauli_pode_fid_decay_dd(offset, seq_name, sym, num_reps, d, + d_label, backend, basis, + encoding_qubit, dd_qubit): + """ + -->Generates list of experiments which probe fidelity of Pauli podes. + |0> -- encode_pode_state(offset) -- DD -- decode_pode_state(offset), + where encoding done on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [tau] identities between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + if "uddx" in seq_name: + seq_method = "add_uddx" + ord_n = int(seq_name[4:]) + elif "uddy" in seq_name: + seq_method = "add_uddy" + ord_n = int(seq_name[4:]) + elif "qdd" in seq_name: + seq_method = "add_qdd" + _, ord_n, ord_m = seq_name.split("_") + ord_n = int(ord_n) + ord_m = int(ord_m) + elif "ur" in seq_name and "x" not in seq_name and "y" not in seq_name: + seq_method = "add_ur" + ord_n = int(seq_name[2:]) + else: + seq_method = "add_" + seq_name + # set up which qubits to run DD sequence on (not same as those + # get amount of time it takes to run this many reps of dd seq + qubit = dd_qubit + time_sched = IBMQDdSchedule(backend, basis) + if "uddx" in seq_name or "uddy" in seq_name or ("ur" in seq_name and "x" not in seq_name and "y" not in seq_name): + getattr(time_sched, seq_method)(ord_n, qubit, 1, d, sym) + elif "qdd" in seq_name: + getattr(time_sched, seq_method)(ord_n, ord_m, qubit, 1, d, sym) + else: + getattr(time_sched, seq_method)(qubit, 1, d, sym) + T = (time_sched.get_phys_time() * num_reps) + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_pauli_pode_offset_{offset}_dtype_{d_label}_" + job_tag += f"fid_decay_{seq_name}_sym_{sym}_encodeq_{encoding_qubit}_" + job_tag += f"ddq_{dd_qubit}_reps_{num_reps}_delay_{d}dt_T_{T}ns" + exp_tag = job_tag + "_pode_{}" + + # create single rep of DD seq + sing_dd_sched = IBMQDdSchedule(backend, basis) + if "uddx" in seq_name or "uddy" in seq_name or ("ur" in seq_name and "x" not in seq_name and "y" not in seq_name): + getattr(sing_dd_sched, seq_method)(ord_n, dd_qubit, 1, d, sym) + elif "qdd" in seq_name: + getattr(sing_dd_sched, seq_method)(ord_n, ord_m, dd_qubit, 1, d, sym) + else: + getattr(sing_dd_sched, seq_method)(dd_qubit, 1, d, sym) + + # create dd sched with number of reps desired + dd_sched = IBMQDdSchedule(backend, basis) + for j in range(num_reps): + dd_sched.sched += sing_dd_sched.sched + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for pode in range(6): + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(pode)) + sched.encode_podal_state(encoding_qubit, pode, offset) + sched.sched += dd_sched.sched + sched.decode_podal_state(encoding_qubit, pode, offset) + # diagnose acquire delay constraint issues + aa = backend.get_acquire_alignment() + r = (num_reps * sing_dd_sched.tot_delay) % aa + if r != 0: + #print(f"Diagnosed with {aa - r}dt delay.") + sched.add_pause(encoding_qubit, aa - r) + # measure encoded qubits + sched.add_measurement(encoding_qubit, 0) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + + +def haar_fid_decay_dd(N, seq_name, sym, num_reps, + pause_padding, d, d_label, + backend, basis, + encoding_qubit, dd_qubit): + """ + -->Generates list of experiments which probe fidelity of Pauli podes. + |0> -- encode_pode_state(offset) -- DD -- decode_pode_state(offset), + where encoding done on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [tau] identities between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + if "uddx" in seq_name: + seq_method = "add_uddx" + ord_n = int(seq_name[4:]) + elif "uddy" in seq_name: + seq_method = "add_uddy" + ord_n = int(seq_name[4:]) + elif "qdd" in seq_name: + seq_method = "add_qdd" + _, ord_n, ord_m = seq_name.split("_") + ord_n = int(ord_n) + ord_m = int(ord_m) + elif "ur" in seq_name and "x" not in seq_name and "y" not in seq_name: + seq_method = "add_ur" + ord_n = int(seq_name[2:]) + else: + seq_method = "add_" + seq_name + # get amount of time it takes to run this many reps of dd seq + qubit = dd_qubit + time_sched = IBMQDdSchedule(backend, basis) + if "uddx" in seq_name or "uddy" in seq_name or ("ur" in seq_name and "x" not in seq_name and "y" not in seq_name): + getattr(time_sched, seq_method)(ord_n, qubit, 1, d, sym) + elif "qdd" in seq_name: + getattr(time_sched, seq_method)(ord_n, ord_m, qubit, 1, d, sym) + else: + getattr(time_sched, seq_method)(qubit, 1, d, sym) + T = (time_sched.get_phys_time() * num_reps) + delay_sched = IBMQDdSchedule(backend, basis) + delay_sched.add_pause(qubit, pause_padding) + T += delay_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_haar_{N}_dtype_{d_label}_" + job_tag += f"fid_decay_{seq_name}_sym_{sym}_encodeqs_{encoding_qubit}_" + job_tag += f"ddqs_{dd_qubit}_reps_{num_reps}_delay_{d}dt_T_{T}ns" + exp_tag = job_tag + "_pode_{}" + + # create single rep of DD seq + sing_dd_sched = IBMQDdSchedule(backend, basis) + if "uddx" in seq_name or "uddy" in seq_name or ("ur" in seq_name and "x" not in seq_name and "y" not in seq_name): + getattr(sing_dd_sched, seq_method)(ord_n, dd_qubit, 1, d, sym) + elif "qdd" in seq_name: + getattr(sing_dd_sched, seq_method)(ord_n, ord_m, dd_qubit, 1, d, sym) + else: + getattr(sing_dd_sched, seq_method)(dd_qubit, 1, d, sym) + + # create dd sched with number of reps desired + dd_sched = IBMQDdSchedule(backend, basis) + for j in range(num_reps): + dd_sched.sched += sing_dd_sched.sched + # create pause that completes the schedule time + dd_sched.add_pause(dd_qubit, pause_padding) + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for pode in range(N): + p, inv_p = u3_params[pode] + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(pode)) + sched.add_u3(encoding_qubit, *p) + sched.sched += dd_sched.sched + sched.add_u3(encoding_qubit, *inv_p) + # diagnose acquire delay constraint issues + aa = backend.get_acquire_alignment() + r = (num_reps * sing_dd_sched.tot_delay) % aa + if r != 0: + #print(f"Diagnosed with {aa - r}dt delay.") + sched.add_pause(encoding_qubit, aa - r) + # measure encoded qubits + sched.add_measurement(encoding_qubit, 0) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + + +def static_haar_fid_decay_uddx(haar_params_list, n, backend, basis, + time='min', num_reps=1, encoding_qubits='all', + dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- uddx([n], [time]) -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + uddx of order [n] is applied over time [time]. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_uddx(0, n, time, num_reps) + T = time_sched.get_phys_time() + if time == 'min': + time = int(time_sched.get_duration() / num_reps) + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_uddx_{n}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_reps_{num_reps}_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + sched.add_uddx(dd_qubits, n, time, num_reps) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def static_haar_fid_decay_uddxm(haar_params_list, n, time, backend, basis, + encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- uddx([n], [time]) -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + uddx of order [n] is applied over time [time]. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_uddxm(0, n, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_uddxm_{n}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + sched.add_uddxm(dd_qubits, n, time) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def static_haar_fid_decay_uddxe(haar_params_list, n, time, backend, basis, + encoding_qubits='all', dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- uddx([n], [time]) -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + uddx of order [n] is applied over time [time]. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_uddxe(0, n, time) + T = time_sched.get_phys_time() + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_uddxe_{n}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + sched.add_uddxe(dd_qubits, n, time) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def static_haar_fid_decay_uddy(haar_params_list, n, backend, basis, + time='min', num_reps=1, encoding_qubits='all', + dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- uddx([n], [time]) -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + uddx of order [n] is applied over time [time]. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_uddy(0, n, time, num_reps) + T = time_sched.get_phys_time() + if time == 'min': + time = int(time_sched.get_duration() / num_reps) + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_uddy_{n}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_reps_{num_reps}_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + sched.add_uddy(dd_qubits, n, time, num_reps) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + + +def static_haar_fid_decay_qdd(haar_params_list, n, m, backend, basis, + time='min', num_reps=1, encoding_qubits='all', + dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector): + |0> -- U3(h) -- qdd([n], [m], [time]) -- U3^{dag}(h), + where U3(h) state is prepared on [encoding_qubits] and + qdd of order [n, m] is applied over time [time]. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many id gates + time_sched = IBMQDdSchedule(backend, basis) + time_sched.add_qdd(0, n, m, time, num_reps) + T = time_sched.get_phys_time() + if time == 'min': + time = int(time_sched.get_duration() / num_reps) + # hvaing parsed input args, set job and exp tags which associate + # input params to schedule + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_fid_decay_qdd_{n}_{m}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{time}dt_reps_{num_reps}_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + sched.add_qdd(dd_qubits, n, m, time, num_reps) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +###################################################################### +# Tau Test Functions +# Description: Experimentally measures the fidelity of different DD +# sequences as a function of pulse spacing \tau. To be specific, +# suppose we run 25 repetitions of XY4 with tau = 0, tau = 10, tau = 100 +# in schedule normalized units (dt). +# Ideally, we'd like to know the fidelity "on average" for a "typical +# state", so perform the DD sequence on a list of haar random states. +###################################################################### +def static_haar_tau_test_dd(haar_params_list, seq_name, num_reps, tau, + backend, basis, encoding_qubits='all', + dd_qubits='all'): + """ + -->Generates list of experiments indexed by h \in haar_param_list + (h is 3 vector) and n \in num_id_list of the form: + |0> -- U3(t,0,0) -- DD seq -- U3^{dag}(t,0,0), + where U3(t,0,0) state is prepared on [encoding_qubits] and + [seq_name] DD sequence is applied on [dd_qubits] [num_reps] times + in repetition with [tau] identities between pulses. + -->[backend] is needed to specify DD schedule time. + -->If [encoding_qubits] = 'all,' use all backend qubits. + -->If [dd_qubits] = 'all', then set dd_qubits = encoding_qubits. + """ + # prepend the 'add' to seq name string + seq_method = 'add_' + seq_name + # if encoding_qubits set to 'all,' get all backend qubits + if encoding_qubits == 'all': + encoding_qubits = list(range(backend.get_number_qubits())) + # set up which qubits to run DD sequence on (not same as those + # we measure necessarily unless 'all' is specified) + if dd_qubits == 'all': + dd_qubits = encoding_qubits + # get amount of time it takes to run this many reps of dd seq + time_sched = IBMQDdSchedule(backend, basis) + getattr(time_sched, seq_method)(0, num_reps, tau) + T = time_sched.get_phys_time() + # having parsed input args, set job and exp tags which associate + # input params to schedule + phys_tau = ns_time_to_dt(tau, time_sched.dt) + job_tag = f"pulse_{basis}_" + job_tag += f"static_haar_tau_test_{seq_name}_encodeqs_{encoding_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_tau_{phys_tau}ns_T_{T}ns" + exp_tag = job_tag + "_theta_{}_phi_{}_lambda_{}" + + # now iterate over the haar_params, i.e the "experiment" loop + experiments = [] + for params in haar_params_list: + # extract theta, phi, and lambda to construct u3 + t, p, l = params + # create circ add u3-->DD seq-->u3^{dag} + sched = IBMQDdSchedule(backend, basis, name=exp_tag.format(t, p, l)) + sched.add_u3(encoding_qubits, t, p, l) + getattr(sched, seq_method)(dd_qubits, num_reps, tau) + sched.add_u3(encoding_qubits, -t, -l, -p) + # measure encoded qubits + for idx, q in enumerate(encoding_qubits): + sched.add_measurement(q) + # append this schedule to experiments + experiments.append(sched) + + return (job_tag, experiments) + +def dt_time_to_ns(dt_time, dt): + '''converts time in normalzied dt units to ns''' + return (dt_time * dt * 1e9) + +def ns_time_to_dt(phys_time, dt): + '''converts time from physical (ns) to dt normalized time''' + return (phys_time * 1e-9) / dt diff --git a/src/edd/experiments/_pulse_experiments_test.py b/src/edd/experiments/_pulse_experiments_test.py new file mode 100644 index 0000000..11b3dcc --- /dev/null +++ b/src/edd/experiments/_pulse_experiments_test.py @@ -0,0 +1,235 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.backend import IBMQBackend +import edd.experiments as edde + +# type check imports +from edd.pulse import IBMQDdSchedule + +import unittest + +def dt_time_to_ns(dt_time, dt): + '''converts time in normalzied dt units to ns''' + return (dt_time * dt * 1e9) + +class PulseExperimentsTest(unittest.TestCase): + """ Tests that experiments run smoothly. """ + + def test_theta_sweep_free_default(self): + '''test theta_sweep_free with default args''' + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + num_ids = 5 + experiments = edde.pulse.theta_sweep_free(num_ids, armonk) + + # first check that job title is as expected + sched = IBMQDdSchedule(armonk) + sched.add_id(0) + id_time = sched.get_phys_time() + time = num_ids * id_time + qubits = [n for n in range(armonk.get_number_qubits())] + job_tag = f"theta_sweep_free_encodeqs_{qubits}_ddqs_{qubits}_" + job_tag += f"ids_{num_ids}_T_{time}ns" + self.assertEquals(experiments[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(experiments[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(experiments[1][0].name, exp_tag1) + + def test_theta_sweep_free_not_default(self): + '''test theta_sweep_free with default args''' + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + # this is the most complicated case with non-contiguous + # encoding_qubits and dd_qubits + e_qubits = [0] + dd_qubits = [0] + num_ids = 5 + exps = edde.pulse.theta_sweep_free(num_ids, armonk, e_qubits, dd_qubits) + + # first check that job title is as expected + sched = IBMQDdSchedule(armonk) + sched.add_id(0) + id_time = sched.get_phys_time() + time = num_ids * id_time + job_tag = f"theta_sweep_free_encodeqs_{e_qubits}_ddqs_{dd_qubits}_" + job_tag += f"ids_{num_ids}_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_theta_sweep_dd_default(self): + '''test theta_sweep_dd with default args''' + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk) + dt = sched.dt + num_reps = 5 + tau = 100 + exps = edde.pulse.theta_sweep_dd('xy4', num_reps, tau, armonk) + + # first check that job title is as expected + sched.add_x(0) + x_time = sched.get_phys_time() + # there are 4 u3 gates for each rep of xy4 and 5 reps + # then there is 1 identity gate in between each pulse + time = (4 * num_reps) * (x_time + dt_time_to_ns(tau, dt)) + qubits = [n for n in range(armonk.get_number_qubits())] + job_tag = f"theta_sweep_xy4_encodeqs_{qubits}_ddqs_{qubits}_" + job_tag += f"reps_{num_reps}_tau_{tau}dt_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_theta_sweep_dd_not_default(self): + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk) + dt = sched.dt + # this is the most complicated case with non-contiguous + # encoding_qubits and dd_qubits + e_qubits = [0] + dd_qubits = [0] + num_reps = 5 + tau = 100 + exps = edde.pulse.theta_sweep_dd('xy4', num_reps, tau, armonk, + e_qubits, dd_qubits) + + # first check that job title is as expected + sched.add_x(0) + x_time = sched.get_phys_time() + # there are 4 u3 gates for each rep of xy4 + # then there is 1 identity gate in between each pulse + time = (4 * num_reps) * (x_time + dt_time_to_ns(tau, dt)) + job_tag = f"theta_sweep_xy4_encodeqs_{e_qubits}_ddqs_{dd_qubits}_" + job_tag += f"reps_{num_reps}_tau_{tau}dt_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_0.0" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_static_haar_fid_decay_free_default(self): + '''tests static_haar_fid_decay_free with default args''' + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + free_time = 100 + exps = edde.pulse.static_haar_fid_decay_free(haar_params_list, + free_time, armonk) + + # first check that job title is as expected + sched = IBMQDdSchedule(armonk) + T = dt_time_to_ns(free_time, sched.dt) + qubits = [n for n in range(armonk.get_number_qubits())] + job_tag = f"static_haar_fid_decay_free_encodeqs_{qubits}_" + job_tag += f"ddqs_{qubits}_time_{free_time}dt_T_{T}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_static_haar_fid_decay_free_not_default(self): + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + free_time = 100 + e_qubits = [0] + dd_qubits = [0] + exps = edde.pulse.static_haar_fid_decay_free(haar_params_list, + free_time, armonk, + e_qubits, dd_qubits) + + # first check that job title is as expected + sched = IBMQDdSchedule(armonk) + T = dt_time_to_ns(free_time, sched.dt) + job_tag = f"static_haar_fid_decay_free_encodeqs_{e_qubits}_" + job_tag += f"ddqs_{dd_qubits}_time_{free_time}dt_T_{T}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + def test_static_haar_fid_decay_dd_default(self): + '''tests static_haar_fid_decay_dd with default args''' + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk) + dt = sched.dt + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + num_reps = 5 + tau = 100 + exps = edde.pulse.static_haar_fid_decay_dd(haar_params_list, 'xy4', + num_reps, tau, armonk) + + # first check that job title is as expected + sched.add_x(0) + x_time = sched.get_phys_time() + time = (4 * num_reps) * (x_time + dt_time_to_ns(tau, dt)) + qubits = [n for n in range(armonk.get_number_qubits())] + job_tag = f"static_haar_fid_decay_xy4_encodeqs_{qubits}_" + job_tag += f"ddqs_{qubits}_reps_{num_reps}_tau_{tau}dt_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + + def test_static_haar_fid_decay_dd_not_default(self): + # load backend and create experiments + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk) + dt = sched.dt + haar_params_list = [(1, 1, 1), (0.26, .6, .178)] + num_reps = 5 + tau = 100 + e_qubits = [0] + dd_qubits = [0] + exps = edde.pulse.static_haar_fid_decay_dd(haar_params_list, 'xy4', + num_reps, tau, armonk, + e_qubits, dd_qubits) + + # first check that job title is as expected + sched.add_x(0) + x_time = sched.get_phys_time() + time = (4 * num_reps) * (x_time + dt_time_to_ns(tau, dt)) + job_tag = f"static_haar_fid_decay_xy4_encodeqs_{e_qubits}_" + job_tag += f"ddqs_{dd_qubits}_reps_{num_reps}_tau_{tau}dt_T_{time}ns" + self.assertEquals(exps[0], job_tag) + + # next, check that experiment list is as expected + self.assertIsInstance(exps[1][0], IBMQDdSchedule) + exp_tag1 = job_tag + "_theta_1_phi_1_lambda_1" + self.assertEquals(exps[1][0].name, exp_tag1) + + +if __name__=="__main__": + unittest.main() diff --git a/src/edd/pulse/__init__.py b/src/edd/pulse/__init__.py new file mode 100644 index 0000000..f29fe10 --- /dev/null +++ b/src/edd/pulse/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.pulse._ibmq_pulse import IBMQDdSchedule diff --git a/src/edd/pulse/_ibmq_pulse.py b/src/edd/pulse/_ibmq_pulse.py new file mode 100644 index 0000000..efc8a8b --- /dev/null +++ b/src/edd/pulse/_ibmq_pulse.py @@ -0,0 +1,2840 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +from edd.circuit import IBMQDdCircuit +from qiskit.pulse import Waveform, Schedule, Delay, Play, DriveChannel, Drag +from qiskit import transpile, QuantumCircuit, schedule as build_schedule + +class IBMQDdSchedule(Schedule): + """ + Our in-house Schedule class inhereting from + qiskit Pulse + """ + + def __init__(self, ibmq_backend, basis_version, name=''): + super(IBMQDdSchedule, self).__init__(name=name) + + self.ibmq_backend = ibmq_backend + # for now, we just create basis based on 1st qubit + #TODO: possibly change this behavior when access to multi-qubit pulse + self.basis_version = basis_version + n = ibmq_backend.get_number_qubits() + if basis_version == 'x_basis': + qubit_bases = [] + for q in range(n): + basis = create_from_x_basis(ibmq_backend, q) + qubit_bases.append(basis) + self.basis = {} + for basis in qubit_bases: + for key, value in basis.items(): + self.basis[key] = value + elif basis_version == 'g_basis': + qubit_bases = [] + for q in range(n): + basis = create_from_greg_basis(ibmq_backend, q) + qubit_bases.append(basis) + self.basis = {} + for basis in qubit_bases: + for key, value in basis.items(): + self.basis[key] = value + elif basis_version == 'c_basis': + qubit_bases = [] + for q in range(n): + basis = create_from_circ_basis(ibmq_backend, n, q) + qubit_bases.append(basis) + self.basis = {} + for basis in qubit_bases: + for key, value in basis.items(): + self.basis[key] = value + # get normalized time unit of backend object (not our personal one) + self.dt = ibmq_backend.backend.configuration().dt + self.sched = Schedule(name=name) + self.tot_delay = 0 + return + + def get_schedule(self): + return self.sched + + def get_pulse_list(self): + return list(self.sched._instructions()) + + def get_tot_delay(self): + return self.tot_delay + + def get_pulse_names(self): + pulse_list = self.get_pulse_list() + pulse_names = [] + for pulse in pulse_list: + pulse_names.append(pulse[1].name) + return pulse_names + + def get_duration(self): + return self.sched.duration + + def get_phys_time(self): + '''returns physical time of schedule in ns''' + return dt_time_to_ns(self.get_duration(), self.dt) + + def draw(self): + return self.sched.draw() + + def reset(self): + '''removes all pulses added so far''' + self.sched = Schedule(name=self.sched.name) + return self.sched + + ################################################## + # Add simple pulses (X, Y, measurement, ...) + ################################################## + + # identity pulse of same length as X/Y + def add_id(self, qubits): + '''adds id pulse to [qubits] of same length as X/Y pulses''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + if self.basis_version == 'c_basis': + self.sched += self.basis[f'I_{qubit}'] + else: + self.sched += Play(self.basis[f'I_{qubit}'], DriveChannel(qubit)) + return + + # X pulses + def add_x(self, qubits): + '''adds X pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + if self.basis_version == 'c_basis': + self.sched += self.basis[f'X_{qubit}'] + else: + self.sched += Play(self.basis[f'X_{qubit}'], DriveChannel(qubit)) + return + + def add_xb(self, qubits): + '''adds Xb pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + if self.basis_version == 'c_basis': + self.sched += self.basis[f'Xb_{qubit}'] + else: + self.sched += Play(self.basis[f'Xb_{qubit}'], DriveChannel(qubit)) + return + + def add_x90(self, qubits): + '''adds X90 pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + self.sched += Play(self.basis[f'X2_{qubit}'], DriveChannel(qubit)) + return + + def add_x90b(self, qubits): + '''adds X90b pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + self.sched += Play(self.basis[f'X2b_{qubit}'], DriveChannel(qubit)) + return + + def add_y(self, qubits): + '''adds Y pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + if self.basis_version == 'c_basis': + self.sched += self.basis[f'Y_{qubit}'] + else: + self.sched += Play(self.basis[f'Y_{qubit}'], DriveChannel(qubit)) + return + + def add_symy(self, qubits): + '''adds symmetric Y (circuit) pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + if self.basis_version == 'c_basis': + self.sched += self.basis[f'symY_{qubit}'] + else: + raise ValueError("Only works for circuit basis.") + return + + def add_yb(self, qubits): + '''adds Yb pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + if self.basis_version == 'c_basis': + self.sched += self.basis[f'Yb_{qubit}'] + else: + self.sched += Play(self.basis[f'Yb_{qubit}'], DriveChannel(qubit)) + return + + def add_y90(self, qubits): + '''adds Y90 pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + self.sched += Play(self.basis[f'Y2_{qubit}'], DriveChannel(qubit)) + return + + def add_y90b(self, qubits): + '''adds Y pulse to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + self.sched += Play(self.basis[f'Y2b_{qubit}'], DriveChannel(qubit)) + return + + def add_z(self, qubits): + '''adds Z pulse to [qubits].''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + self.sched += self.basis[f'Z_{qubit}'] + return + + def add_zb(self, qubits): + '''adds Zb pulse to [qubits].''' + if type(qubits) is int: + qubits = [qubits] + for qubit in qubits: + self.sched += self.basis[f'Zb_{qubit}'] + return + + def add_zi(self, qubits): + '''adds Z then I to [qubits]''' + self.add_z(qubits) + self.add_id(qubits) + return + + def add_old_measurement(self, qubit): + """ + Adds measurement to [qubit] along with any ancillary qubits (that is, + not all qubits can be measured solo due to hardware constraints.) + #TODO: allow adding measurements to more than one qubit at once + """ + backend_defaults = self.ibmq_backend.backend.defaults() + backend_config = self.ibmq_backend.backend.configuration() + meas_map_idx = None + for i, measure_group in enumerate(backend_config.meas_map): + if qubit in measure_group: + meas_map_idx = i + break + assert meas_map_idx is not None, f"Couldn't find qubit {qubit} in the meas_map!" + # now create measurement with measurement indices + inst_sched_map = backend_defaults.instruction_schedule_map + measure = inst_sched_map.get('measure', qubits=backend_config.meas_map[meas_map_idx]) + + # add measurement to schedule (shift to end with <<) + self.sched += measure << self.get_duration() + return + + def add_measurement(self, qubits, cbits): + """ + Adds measurement to [qubit]. + """ + if type(qubits) == int: + qubits = [qubits] + if type(cbits) == int: + cbits = [cbits] + # first build the measurement circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n, len(cbits)) + for i in range(len(qubits)): + circ.measure(qubits[i], cbits[i]) + t_circ = transpile(circ, self.ibmq_backend.backend) + # make Pulse schedule of measurement + measure = build_schedule(t_circ, self.ibmq_backend.backend) + # append measurement to schedule at the end + self.sched += measure << self.get_duration() + + return + + def add_measurement(self, qubits, cbits): + """ + Adds measurement to [qubit]. + """ + if type(qubits) == int: + qubits = [qubits] + if type(cbits) == int: + cbits = [cbits] + # first build the measurement circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n, len(cbits)) + for i in range(len(qubits)): + circ.measure(qubits[i], cbits[i]) + t_circ = transpile(circ, self.ibmq_backend.backend) + # make Pulse schedule of measurement + measure = build_schedule(t_circ, self.ibmq_backend.backend) + # append measurement to schedule at the end + self.sched += measure << self.get_duration() + + return + + def add_error_mitigation_0(self, qubit): + """ + Adds measurement error mitigation schedule for |0>, + Id-Measure. + """ + self.add_id(qubit) + self.add_measurement(qubit, 0) + return + + def add_error_mitigation_1(self, qubit): + """ + Adds measurement error mitigation schedule for |1>, + X-Measure. + """ + self.add_x(qubit) + self.add_measurement(qubit, 0) + return + + ################################################## + # Native Gate Pulses + ################################################## + def add_u3(self, qubits, t, p, l): + '''adds u3(t, p, l) to [qubits]''' + if type(qubits) is int: + qubits = [qubits] + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + circ.u(t, p, l, qubits) + t_circ = transpile(circ, self.ibmq_backend.backend) + u3sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += u3sched + + return + + def add_circDD_part1(self, gidx, qubits): + """ + Adds state prep and Q1/Q2 swap. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # prepare state + circ.unitary(g1, [q0, q1], label="U_1") + circ.unitary(g2, [q1, q2], label="U_2") + circ.barrier([q0, q1, q2]) + # swap operation between Q1 and Q2 + circ.cnot(q2, q1) + circ.cnot(q1, q2) + circ.cnot(q2, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + + def add_circDD_part2(self, qubits): + """ + Adds entangling operation on Q0 and Q1 + """ + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + circ.h(q0) + circ.cnot(q0, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + + def add_circDD_part3(self, qubits): + """ + Adds dientangling operation on Q0 and Q1 + """ + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + circ.cnot(q0, q1) + circ.h(q0) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + + def add_circDD_part4(self, qubits): + """ + Swaps Q1 and Q2 back to original position. + """ + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + circ.cnot(q2, q1) + circ.cnot(q1, q2) + circ.cnot(q2, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + + def add_circDD_part5(self, gidx, qubits): + """ + Decodes input state. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # prepare state + circ.unitary(g2dg, [q1, q2], label="U_2^{dg}") + circ.unitary(g1dg, [q0, q1], label="U_1^{dg}") + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + cbits = list(range(len(qubits))) + self.add_measurement(qubits, cbits) + + return + + def add_circDDv2_part1(self, gidx, qubits): + """ + Adds first state prep on Q0/Q1. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # prepare state on Q0/Q1 + circ.unitary(g1, [q0, q1], label="U_1") + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + def add_circDDv2_part2(self, gidx, qubits): + """ + Adds seconds state prep on Q1/Q2. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # prepare state on Q1/Q2 + circ.unitary(g2, [q1, q2], label="U_2") + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + def add_circDDv2_part3(self, qubits): + """ + Adds SWAP between Q1 and Q2. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # swap operation between Q1 and Q2 + circ.cnot(q2, q1) + circ.cnot(q1, q2) + circ.cnot(q2, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + def add_circDDv2_part4(self, qubits): + """ + Adds entangling operation on Q0 and Q1 + """ + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + circ.h(q0) + circ.cnot(q0, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + def add_circDDv2_part5(self, qubits): + """ + Adds disentangling operation on Q0/Q1. + """ + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + circ.h(q0) + circ.cnot(q0, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + def add_circDDv2_part6(self, qubits): + """ + Swaps Q1 and Q2 back to original position. + """ + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + circ.cnot(q2, q1) + circ.cnot(q1, q2) + circ.cnot(q2, q1) + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + + return + + def add_circDDv2_part7(self, gidx, qubits): + """ + Decodes input state on Q1/Q2. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # prepare state + circ.unitary(g2dg, [q1, q2], label="U_2^{dg}") + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + cbits = list(range(len(qubits))) + self.add_measurement(qubits, cbits) + + return + + def add_circDDv2_part8(self, gidx, qubits): + """ + Decodes input state on Q0/Q1. + """ + # extract state prep gates + big_gate_list = np.load("2q_gate_list.npy", allow_pickle=True) + gates = big_gate_list[gidx] + g1, g1dg, g2, g2dg, _, _ = gates + # form circuit + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + q0, q1, q2 = qubits + # prepare state + circ.unitary(g1dg, [q0, q1], label="U_1^{dg}") + circ.barrier([q0, q1, q2]) + # transpile the circuit, convert to sched, and append + t_circ = transpile(circ, self.ibmq_backend.backend) + circ_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += circ_sched + cbits = list(range(len(qubits))) + self.add_measurement(qubits, cbits) + + return + + + + ################################################## + # Encoding and Decoding of States + ################################################## + def encode_podal_state(self, qubits, pole=2, offset=0): + """ + Assuming you are start in the |0> state, prepares a podal + state--essentially a variation of Pauli-states. If offset=0, + this method prepares the following states based on [pole] + pole = 0 --> |0> + pole = 1 --> |1> + pole = 2 --> |+> + pole = 3 --> |-> + pole = 4 --> |+i> + pole = 5 --> |-i> + [offset] sets a systematic tilt to all states in same direction. + In particular, when [offset] = pi / 2, we get + |0> --> |+> + |1> --> |-> + |+> --> |+i> + |-> --> |-i> + |+i> --> |0> + |-i> --> |1> + """ + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + if pole == 0: + circ.ry(0 + offset, qubits) + elif pole == 1: + circ.ry(np.pi + offset, qubits) + elif pole == 2: + circ.ry(np.pi/2, qubits) + circ.rz(offset, qubits) + elif pole == 3: + circ.ry(-np.pi/2 , qubits) + circ.rz(offset, qubits) + elif pole == 4: + circ.rx(-np.pi/2 + offset, qubits) + elif pole == 5: + circ.rx(np.pi/2 + offset, qubits) + + t_circ = transpile(circ, self.ibmq_backend.backend) + pode_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += pode_sched + return + + def decode_podal_state(self, qubits, pole=2, offset=0): + """ + Decodes podal state of same parameters to return back to |0>. + """ + n = self.ibmq_backend.get_number_qubits() + circ = QuantumCircuit(n) + if pole == 0: + circ.ry(0 - offset, qubits) + elif pole == 1: + circ.ry(-np.pi - offset, qubits) + elif pole == 2: + circ.rz(-offset, qubits) + circ.ry(-np.pi/2, qubits) + elif pole == 3: + circ.rz(-offset, qubits) + circ.ry(np.pi/2 , qubits) + elif pole == 4: + circ.rx(np.pi/2 - offset, qubits) + elif pole == 5: + circ.rx(-np.pi/2 - offset, qubits) + + t_circ = transpile(circ, self.ibmq_backend.backend) + pode_sched = build_schedule(t_circ, self.ibmq_backend.backend) + self.sched += pode_sched + return + + def encode_theta_state(self, qubits, t): + """ + Adds U3(t, 0, 0) to [qubits] which takes |0> state to + cos(t/2)|0> + sin(t/2)|1> state. + """ + self.add_u3(qubits, t, 0, 0) + + def decode_theta_state(self, qubits, t): + """ + Adds U3(-t, 0, 0) to [qubits] which takes the + cos(t/2)|0> + sin(t/2)|1> back to |0> state. + """ + self.add_u3(qubits, -t, 0, 0) + ################################################## + # DD Sequences + ################################################## + def add_pause(self, qubit, time): + '''adds pause (id gate) for [time]dt on [qubits]''' + if time != 0: + self.sched += Delay(time, DriveChannel(qubit)) + self.tot_delay += time + return + + # diagnose sum of delays not respecting acquire constraint + def diagnose_acquire_constraint(self, qubit): + """ + Adds additional pause at the end (before measurement) + if necessary to ensure acquire constraint is satisfied. + """ + aa = self.ibmq_backend.get_acquire_alignment() + r = self.tot_delay % aa + if r != 0: + #print(f"Diagnosed with {aa - r}dt delay.") + self.add_pause(qubit, aa - r) + + return r + + + + + def add_free(self, qubit, num_reps=1, du=0, sym=False): + """ + Adds [num_reps] identity gates with [du] pause + between them (redundanet but for consistency) + onto [qubits]. [sym] is also not used but there + for consistency. + """ + for _ in range(num_reps): + # add I + self.add_id(qubit) + # add delay + self.add_pause(qubit, du) + + + def add_hahn(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of Hahn sequence to [qubits] back-to-back. + * sym=False: X- + * sym=True: -X- + where - is delay of duration [d] + """ + for _ in range(num_reps): + if sym is False: + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_purex(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CPMG sequence to [qubit]. + * sym=False: X-X- + * sym=True: -X=X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + def add_purey(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CPMG sequence to [qubit]. + * sym=False: Y-Y- + * sym=True: -Y=Y- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_sympurey(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CPMG sequence to [qubit]. + * sym=False: Y-Y- + * sym=True: -Y=Y- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # Y + self.add_symy(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_symy(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_symy(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_symy(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_xy4(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of XY4 sequence to [qubit]. + * sym=False: Y-X- + * sym=True: -X=X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_cdd_n(self, n, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CDD_n sequence to [qubit]. + * sym=False + Y(CDD_{n-1})X(CDD_{n-1})Y(CDD_{n-1})X(CDD_{n-1}) + * sym=True + -Y(CDD_{n-1})X(CDD_{n-1})Y(CDD_{n-1})X(CDD_{n-1})- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + + # step 1: create cdd schedule as a str + cdd_str = simplify_cdd_str(make_cdd_n_str(n), sym) + + # step 2: add it num_reps times + for _ in range(num_reps): + if sym is True: + # free evo (d) + self.add_pause(qubit, d) + # add relevant instructions from cdd_str + for loc, inst in enumerate(cdd_str): + if inst == 'X': + self.add_x(qubit) + elif inst == 'Y': + self.add_y(qubit) + elif inst == 'Z': + self.add_z(qubit) + elif inst == 'f': + # second condition ensures we add (d) pause at end always + if sym is True and (loc < len(cdd_str) - 2): + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + + return + + + def add_cdd2(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CDD_2 sequence to [qubit]. + * sym=False + Y(CDD_{1})X(CDD_{1})Y(CDD_{1})X(CDD_{1}) + * sym=True + -Y(CDD_{1})X(CDD_{1})Y(CDD_{1})X(CDD_{1})- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + self.add_cdd_n(2, qubit, num_reps, d, sym) + return + + + def add_cdd3(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CDD_3 sequence to [qubit]. + * sym=False + Y(CDD_{2})X(CDD_{2})Y(CDD_{2})X(CDD_{2}) + * sym=True + -Y(CDD_{2})X(CDD_{2})Y(CDD_{2})X(CDD_{2})- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + self.add_cdd_n(3, qubit, num_reps, d, sym) + return + + + def add_cdd4(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CDD_4 sequence to [qubit]. + * sym=False + Y(CDD_{3})X(CDD_{3})Y(CDD_{3})X(CDD_{3}) + * sym=True + -Y(CDD_{3})X(CDD_{3})Y(CDD_{3})X(CDD_{3})- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + self.add_cdd_n(4, qubit, num_reps, d, sym) + return + + + def add_cdd5(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of CDD_5 sequence to [qubit]. + * sym=False + Y(CDD_{4})X(CDD_{4})Y(CDD_{4})X(CDD_{4}) + * sym=True + -Y(CDD_{4})X(CDD_{4})Y(CDD_{4})X(CDD_{4})- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + self.add_cdd_n(5, qubit, num_reps, d, sym) + return + + + ################################################# + # Begin ROBUST Sequences + ################################################# + def add_super_hahn(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of super Hahn sequence to [qubit]. + * sym=False: X-Xb- + * sym=True: -X=Xb- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Xb + self.add_xb(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_super_cpmg(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of super CPMG sequence to [qubit]. + * sym=False: X-X-Xb-Xb- + * sym=True: -X=X=Xb=Xb- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Xb + self.add_xb(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Xb + self.add_xb(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_super_euler(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of super euler sequence to [qubit]. + * sym=False + X-Y-X-Y-Y-X-Y-X-Xb-Yb-Xb-Yb-Yb-Xb-Yb-Xb- + * sym=True + -X=Y=X=Y=Y=X=Y=X=Xb=Yb=Xb=Yb=Yb=Xb=Yb=Xb- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + if sym is False: + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + else: + # free evo (d) + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # Y + self.add_y(qubit) + # free evo (ds) + self.add_pause(qubit, ds) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + ################################################## + # KDD Sequence + ################################################## + # define the KDD_\phi composite pulse + def add_comp_kdd(self, phi, qubit, num_reps=1, d=0, sym=False): + """ + Appends the KDD_\phi composite pulse which consists of the following: + f_{tau/2} - (\pi)_{\pi/6 + \phi} - f_{\tau} - (\pi)_{\phi} - + f_{\tau} - (\pi)_{\pi/2 + \phi} - f_{\tau} - (\pi)_{\phi} - + f_{\tau} - (\pi)_{\pi/6 + \phi} - f_{\tau/2}, + where f_{\tau} is free evo for time \tau and (\pi)_{\phi} is + a Pi rotation pulse about the \phi axis where \phi is angle between + positive x-axis and point in x-y axis, counter-clockwise. + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + # create pulses from defaults + if phi == 0: + p1 = self.basis[f'X30_{qubit}'] + p2 = self.basis[f'X_{qubit}'] + p3 = self.basis[f'Y_{qubit}'] + elif np.isclose(phi, np.pi / 2): + p1 = self.basis[f'X120_{qubit}'] + p2 = self.basis[f'Y_{qubit}'] + p3 = self.basis[f'Xb_{qubit}'] + else: + # obtain X\pi pulse, i.e. [180]_(0) pulse from defaults lib + # p1 + p1_ang = np.pi/6 + phi + p1_name = f'[pi]_({p1_ang:.6f})' + if self.basis_version == 'x_basis': + p1 = Waveform(samples = rotate(self.basis[f'X_{qubit}'].samples, + p1_ang), name = p1_name) + elif self.basis_version == 'g_basis': + dur = g_basis[f'X_{qubit}'].duration + amp = g_basis[f'X_{qubit}'].amp + sigma = g_basis[f'X_{qubit}'].sigma + beta = g_basis[f'X_{qubit}'].beta + p1 = Drag(dur, rotate(amp, p1_ang), sigma, beta, name=p1_name) + elif self.basis_version == 'c_basis': + num_regs = self.ibmq_backend.get_number_qubit() + circ = IBMQDdCircuit(num_regs, name=p1_name, ibmq_backend=self.ibmq_backend) + circ.add_pi_eta(p1_ang, qubit) + p1 = build_schedule(circ, self.ibmq_backend.backend) + # p2 + p2_ang = phi + p2_name = f'[pi]_({p2_ang:.6f})' + if self.basis_version == 'x_basis': + p2 = Waveform(samples = rotate(self.basis[f'X_{qubit}'].samples, + p2_ang), name = p2_name) + elif self.basis_version == 'g_basis': + dur = g_basis[f'X_{qubit}'].duration + amp = g_basis[f'X_{qubit}'].amp + sigma = g_basis[f'X_{qubit}'].sigma + beta = g_basis[f'X_{qubit}'].beta + p2 = Drag(dur, rotate(amp, p2_ang), sigma, beta, name=p2_name) + elif self.basis_version == 'c_basis': + num_regs = self.ibmq_backend.get_number_qubit() + circ = IBMQDdCircuit(num_regs, name=p2_name, ibmq_backend=self.ibmq_backend) + circ.add_pi_eta(p2_ang, qubit) + p2 = build_schedule(circ, self.ibmq_backend.backend) + # p3 + p3_ang = np.pi/2 + phi + p3_name = f'[pi]_({p3_ang:.6f})' + if self.basis_version == 'x_basis': + p3 = Waveform(samples = rotate(self.basis[f'X_{qubit}'].samples, + p3_ang), name = p3_name) + elif self.basis_version == 'g_basis': + dur = g_basis[f'X_{qubit}'].duration + amp = g_basis[f'X_{qubit}'].amp + sigma = g_basis[f'X_{qubit}'].sigma + beta = g_basis[f'X_{qubit}'].beta + p3 = Drag(dur, rotate(amp, p3_ang), sigma, beta, name=p3_name) + elif self.basis_version == 'c_basis': + num_regs = self.ibmq_backend.get_number_qubit() + circ = IBMQDdCircuit(num_regs, name=p3_name, ibmq_backend=self.ibmq_backend) + circ.add_pi_eta(p3_ang, qubit) + p3 = build_schedule(circ, self.ibmq_backend.backend) + + # add the composite pulse building block + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # p1 + if self.basis_version == 'c_basis': + self.sched += p1 + else: + self.sched += Play(p1, DriveChannel(qubit)) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # add p2 + if self.basis_version == 'c_basis': + self.sched += p2 + else: + self.sched += Play(p2, DriveChannel(qubit)) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # add p3 + if self.basis_version == 'c_basis': + self.sched += p3 + else: + self.sched += Play(p3, DriveChannel(qubit)) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # add p2 + if self.basis_version == 'c_basis': + self.sched += p2 + else: + self.sched += Play(p2, DriveChannel(qubit)) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # add p1 + if self.basis_version == 'c_basis': + self.sched += p1 + else: + self.sched += Play(p1, DriveChannel(qubit)) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_kdd(self, qubit, num_reps=1, d=0, sym=False): + """ + Appends KDD sequence to [qubit] for [num_reps] times with [tau] + pause between each DD pulse. + + (KDD_pi/2)(KDD_0)(KDD_pi/2)(KDD_0) = "Y.X.Y.X" with composite pulses + """ + for _ in range(num_reps): + self.add_comp_kdd(np.pi / 2, qubit, 1, d, sym) + self.add_comp_kdd(0, qubit, 1, d, sym) + self.add_comp_kdd(np.pi / 2, qubit, 1, d, sym) + self.add_comp_kdd(0, qubit, 1, d, sym) + + return + + + ###################################################################### + # RGA DD Sequences + ###################################################################### + def add_rga2x(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of rga2x sequence to [qubit]. + * sym=False + Xb-X- + * sym=True + -Xb=X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_rga4(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of rga4 sequence to [qubit]. + * sym=False + Yb-X-Yb-X- + * sym=True + -Yb=X=Yb=X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_rga4p(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of rga4p sequence to [qubit]. + * sym=False + Yb-Xb-Yb-X- + * sym=True + -Yb=Xb=Yb=X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_rga8a(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of rga8a sequence to [qubit]. + * sym=False + X-Yb-X-Yb-Y-Xb-Y-Xb- + * sym=True + -X=Yb=X=Yb=Y=Xb=Y=Xb- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Xb + self.add_xb(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_rga8c(self, qubit, num_reps=1, d=0, sym=False): + """ + Adds [num_reps] of rga8c sequence to [qubit]. + * sym=False + X-Y-X-Y-Y-X-Y-X- + * sym=True + -X=Y=X=Y=Y=X=Y=X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # Y + self.add_y(qubit) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # free evo (d) + self.add_pause(qubit, d) + + return + + + def add_rga16a(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga16a sequence to [qubit]. + * sym=False + Zb(RGA8a)Z(RGA8a) + * sym=True + Zb(RGA8a)Z(RGA8a) + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Zb + self.add_zb(qubit) + # RGA8a + self.add_rga8a(qubit, 1, d, sym) + # Z + self.add_z(qubit) + # RGA8a + self.add_rga8a(qubit, 1, d, sym) + if sym is True: + self.add_pause(qubit, d) + + return + + + def add_rga16b(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga16b'' sequence to [qubit]. + * sym=False + RGA4'[RGA4'] + * sym=True + RGA4'[RGA4'] + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width and + RGA4 is Yb-Xb-Yb-X- + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # RGA4p + self.add_rga4p(qubit, 1, d, sym) + # Xb + self.add_xb(qubit) + # RGA4p + self.add_rga4p(qubit, 1, d, sym) + # Yb + self.add_yb(qubit) + # RGA4p + self.add_rga4p(qubit, 1, d, sym) + # X + self.add_x(qubit) + # RGA4p + self.add_rga4p(qubit, 1, d, sym) + if sym is True: + self.add_pause(qubit, d) + + return + + + def add_rga32a(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga32a sequence to [qubit]. + * sym=False + RGA4[RGA8a] for RGA4 is -Yb--X--Yb--X- + * sym=True + RGA4[RGA8a] for RGA4 is -Yb--X--Yb--X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Yb + self.add_yb(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + + return + + + def add_rga32c(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga32c sequence to [qubit]. + * sym=False + RGA8c[RGA4] for RGA8c is -X--Y--X--Y--Y--X--Y--X- + * sym=True + RGA8c[RGA4] for RGA8c is -X--Y--X--Y--Y--X--Y--X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA4 + self.add_rga4(qubit, num_reps, d, sym) + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + + return + + + def add_rga64a(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga64a sequence to [qubit]. + * sym=False + RGA8a[RGA8a] for RGA8a is -X--Yb--X--Yb--Y--Xb--Y--Xb- + * sym=True + RGA8a[RGA8a] for RGA8a is -X--Yb--X--Yb--Y--Xb--Y--Xb- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Yb + self.add_yb(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Yb + self.add_yb(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Xb + self.add_xb(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # Xb + self.add_xb(qubit) + # RGA8a + self.add_rga8a(qubit, num_reps, d, sym) + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + + return + + + def add_rga64c(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga sequence to [qubit]. + * sym=False + RGA8c[RGA8c] for RGA8c is -X--Y--X--Y--Y--X--Y--X- + * sym=True + RGA8c[RGA8c] for RGA8c is -X--Y--X--Y--Y--X--Y--X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # X + self.add_x(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # Y + self.add_y(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA8c + self.add_rga8c(qubit, num_reps, d, sym) + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + + return + + + def add_rga256a(self, qubit, num_reps=1, d=1, sym=False): + """ + Adds [num_reps] of rga256a sequence to [qubit]. + * sym=False + RGA4[RGA64a] where RGA4 is -Yb--X--Yb--X- + * sym=True + RGA4[RGA64a] where RGA4 is -Yb--X--Yb--X- + where - is delay of duration [d] and = is delay + of duration [2ds + delta] for delta pulse width + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # Yb + self.add_yb(qubit) + # RGA64a + self.add_rga64a(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA64a + self.add_rga64a(qubit, num_reps, d, sym) + # Yb + self.add_yb(qubit) + # RGA64a + self.add_rga64a(qubit, num_reps, d, sym) + # X + self.add_x(qubit) + # RGA64a + self.add_rga64a(qubit, num_reps, d, sym) + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + + return + + + ################################################## + # Universal Robust (UR) DD Sequence + ################################################## + def add_ur(self, n, qubit, num_reps=1, d=0, sym=False): + """ + Appends the UR_n sequence to [qubit] for [num_reps] times with + [tau] pause between each DD pulse. + The sequence consists of [pi]_\phi_k rotations where phi_k is + rotation axis (standard phi in x-y plane polar coords) in: + "Arbitrarily Accurate Pulse Sequences for Robust" DD by + Genov, Schraft, Vitanov, and Halfmann in PRL. The + get_urdd_phis() function below should also make it clear. + """ + # assumes all pulses have same delay as X which is typical + delta = self.basis[f'X_{qubit}'].duration + ds = 2 * d + # get list of pulses from unique phi information + _, unique_phi, indices = get_urdd_phis(n) + pulse_list = [] + for phi in unique_phi: + # check if any standard pulses + if np.isclose(phi, 0): + pulse_list.append(self.basis[f'X_{qubit}']) + elif np.isclose(phi, np.pi): + pulse_list.append(self.basis[f'Xb_{qubit}']) + elif np.isclose(phi, np.pi / 2): + pulse_list.append(self.basis[f'Y_{qubit}']) + elif np.isclose(phi, ((np.pi / 2) + np.pi)): + pulse_list.append(self.basis[f'Yb_{qubit}']) + elif np.isclose(phi, (np.pi / 6)): + pulse_list.append(self.basis[f'X30_{qubit}']) + elif np.isclose(phi, ((2 * np.pi) / 3)): + pulse_list.append(self.basis[f'X120_{qubit}']) + else: + # make pulse manually + name = fr'[$\pi$]_({phi:.2f})' + if self.basis_version == 'x_basis': + pulse = Waveform(samples = rotate(self.basis[f'X_{qubit}'].samples, + phi), name = name) + elif self.basis_version == 'g_basis': + dur = self.basis[f'X_{qubit}'].duration + amp = self.basis[f'X_{qubit}'].amp + sigma = self.basis[f'X_{qubit}'].sigma + beta = self.basis[f'X_{qubit}'].beta + pulse = Drag(dur, rotate(amp, phi), sigma, beta, name) + elif self.basis_version == 'c_basis': + num_regs = self.ibmq_backend.get_number_qubits() + circ = IBMQDdCircuit(num_regs, name=name, ibmq_backend=self.ibmq_backend) + circ.add_pi_eta(phi, qubit) + pulse = build_schedule(circ, self.ibmq_backend.backend) + + pulse_list.append(pulse) + + # create pulse schedule from indices and pulses + pulse_sch = [] + for idx in indices: + pulse_sch.append(pulse_list[idx]) + + for _ in range(num_reps): + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + # first pulse + if self.basis_version == 'c_basis': + self.sched += pulse_sch[0] + else: + self.sched += Play(pulse_sch[0], DriveChannel(qubit)) + # add 2nd to n-1st pulse + for pulse in pulse_sch[1:len(pulse_sch)-1]: + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # add pulse + if self.basis_version == 'c_basis': + self.sched += pulse + else: + self.sched += Play(pulse, DriveChannel(qubit)) + # free evo (d or ds) + if sym is True: + self.add_pause(qubit, ds) + else: + self.add_pause(qubit, d) + # add final pulse + if self.basis_version == 'c_basis': + self.sched += pulse_sch[-1] + else: + self.sched += Play(pulse_sch[-1], DriveChannel(qubit)) + # free evo (d) + if sym is True: + self.add_pause(qubit, d) + + return pulse_sch + + + ################################################## + # UDD and QDD Sequences + ################################################## + def add_uddx(self, n, qubit, num_reps=1, d=0, sym=False): + """ + Appends the UDD_x sequence of order [n] to [qubits] where + total sequence lasts time [T]. Appends sequence [num_reps] times in + succession. + If [T]='min', finds the minimum time that + the UDD_x sequence of order [n] can run and uses this T. + """ + # default to smallest pulse interval--add controlled delay later + x_width = self.basis[f'X_{qubit}'].duration + T = find_min_udd_time(n, x_width) + udd_times = make_udd_sched(n, T, x_width) + # adjust times with possible delay [d] and symmetry [sym] + ds = 2 * d + add_delay_T = 0 + for i in range(len(udd_times)): + if sym is False: + if i > 0: + udd_times[i] += (i * d) + add_delay_T += d + else: + if i == 0: + udd_times[i] += d + add_delay_T += d + else: + udd_times[i] += (i * ds) + d + add_delay_T += ds + + # offset times by current running time + for _ in range(num_reps): + app_times = udd_times[::] + curr_dur = self.get_duration() + for i in range(len(app_times)): + app_times[i] += curr_dur + added_time = 0 + for t in app_times: + curr_dur = self.get_duration() + self.add_pause(qubit, t - curr_dur) + added_time += (t - curr_dur) + self.add_x(qubit) + added_time += x_width + # if even n, doesn't end on pulse, so need to add last free evo + if n % 2 == 0: + pause_dur = T - (added_time - add_delay_T) + d + self.add_pause(qubit, pause_dur) + else: + if d > 0: + self.add_pause(qubit, d) + + return udd_times, T + + + def add_uddy(self, n, qubit, num_reps=1, d=0, sym=False): + """ + Appends the UDD_y sequence of order [n] to [qubits] where + total sequence lasts time [T]. Appends sequence [num_reps] times in + succession. + If [T]='min', finds the minimum time that + the UDD_y sequence of order [n] can run and uses this T. + """ + # default to smallest pulse interval--add controlled delay later + y_width = self.basis[f'Y_{qubit}'].duration + T = find_min_udd_time(n, y_width) + udd_times = make_udd_sched(n, T, y_width) + # adjust times with possible delay [d] and symmetry [sym] + ds = 2 * d + add_delay_T = 0 + for i in range(len(udd_times)): + if sym is False: + if i > 0: + udd_times[i] += (i * d) + add_delay_T += d + else: + if i == 0: + udd_times[i] += d + add_delay_T += d + else: + udd_times[i] += (i * ds) + d + add_delay_T += ds + + # offset times by current running time + for _ in range(num_reps): + app_times = udd_times[::] + curr_dur = self.get_duration() + for i in range(len(app_times)): + app_times[i] += curr_dur + added_time = 0 + for t in app_times: + curr_dur = self.get_duration() + self.add_pause(qubit, t - curr_dur) + added_time += (t - curr_dur) + self.add_y(qubit) + added_time += y_width + # if even n, doesn't end on pulse, so need to add last free evo + if n % 2 == 0: + pause_dur = T - (added_time - add_delay_T) + d + self.add_pause(qubit, pause_dur) + else: + if d > 0: + self.add_pause(qubit, d) + + return udd_times, T + + + def add_qdd(self, n, m, qubit, num_reps=1, d=0, sym=False): + """ + Appends the QDD sequence of outer order [n] and inner order [m] + to [qubits] where total sequence lasts time [T]. + Here, we choose Y in outer sequence and X in inner, i.e. + Y X(s_(n+1)\tau) Y X(s_n\tau) ... Y X(s_2\tau) Y X(s_1\tau). + """ + # default to smallest pulse interval--add controlled delay later + x_width = self.basis[f'X_{qubit}'].duration + y_width = self.basis[f'Y_{qubit}'].duration + z_width = self.basis[f'Z_{qubit}'].duration + T = find_min_qdd_time(n, m, x_width, y_width, z_width) + qdd_times, pulses = make_qdd_sched(n, m, T, x_width, y_width, z_width) + # adjust times with possible delay [d] and symmetry [sym] + ds = 2 * d + add_delay_T = 0 + for i in range(len(qdd_times)): + if sym is False: + if i > 0: + qdd_times[i] += (i * d) + add_delay_T += d + else: + if i == 0: + qdd_times[i] += d + add_delay_T += d + else: + qdd_times[i] += (i * ds) + d + add_delay_T += ds + + for _ in range(num_reps): + # offset times by current time + app_times = qdd_times[::] + curr_dur = self.get_duration() + for i in range(len(qdd_times)): + app_times[i] += curr_dur + added_time = 0 + for idx, t in enumerate(app_times): + curr_dur = self.get_duration() + self.add_pause(qubit, t - curr_dur) + added_time += (t - curr_dur) + if pulses[idx] == f'X': + self.add_x(qubit) + added_time += x_width + elif pulses[idx] == 'Y': + self.add_y(qubit) + added_time += y_width + elif pulses[idx] == 'Z': + self.add_z(qubit) + # if even n, doesn't end on pulse, so need to add last free evo + if n % 2 == 0: + pause_dur = T - (added_time - add_delay_T) + d + self.add_pause(qubit, pause_dur) + else: + if d > 0: + self.add_pause(qubit, d) + + return qdd_times, pulses + + + def add_qddzi(self, n, m, qubits, num_reps=1, d=0, sym=False): + """ + Appends the QDD sequence of outer order [n] and inner order [m] + to [qubits] where total sequence lasts time [T]. + Here, we choose Y in outer sequence and X in inner, i.e. + Y X(s_(n+1)\tau) Y X(s_n\tau) ... Y X(s_2\tau) Y X(s_1\tau). + """ + # default to smallest pulse interval--add controlled delay later + x_width = self.basis[f'X_{qubit}'].duration + y_width = self.basis['Y'].duration + z_width = self.basis['Z'].duration + self.basis['I'].duration + T = find_min_qdd_time(n, m, x_width, y_width, z_width) + qdd_times, pulses = make_qdd_sched(n, m, T, x_width, y_width, z_width) + # adjust times with possible delay [d] and symmetry [sym] + ds = 2 * d + add_delay_T = 0 + for i in range(len(qdd_times)): + if sym is False: + if i > 0: + qdd_times[i] += (i * d) + add_delay_T += d + else: + if i == 0: + qdd_times[i] += d + add_delay_T += d + else: + qdd_times[i] += (i * ds) + d + add_delay_T += ds + + for _ in range(num_reps): + # offset times by current time + app_times = qdd_times[::] + curr_dur = self.get_duration() + for i in range(len(qdd_times)): + app_times[i] += curr_dur + added_time = 0 + for idx, t in enumerate(app_times): + curr_dur = self.get_duration() + self.add_pause(qubits, t - curr_dur) + added_time += (t - curr_dur) + if pulses[idx] == f'X_{qubit}': + self.add_x(qubits) + added_time += x_width + elif pulses[idx] == 'Y': + self.add_y(qubits) + added_time += y_width + elif pulses[idx] == 'Z': + self.add_z(qubits) + self.add_id(qubits) + # if even n, doesn't end on pulse, so need to add last free evo + if n % 2 == 0: + pause_dur = T - (added_time - add_delay_T) + d + self.add_pause(qubits, pause_dur) + else: + if d > 0: + self.add_pause(qubits, d) + + return qdd_times, pulses + +###################################################################### +# Helper Functions +###################################################################### +# Rotate and create_basis obtained from Greg Quiroz and Lina Tewala +# but written by Jacob Epstein + +######################################## +# General Signal Utils +######################################## +def rotate(complex_signal: np.ndarray, delta_phi: float) -> np.ndarray: + '''Adds a phase to a complex signal.''' + phi = np.angle(complex_signal) + delta_phi + return np.abs(complex_signal) * np.exp(1j*phi) + +def dt_time_to_ns(dt_time, dt): + '''converts time in normalzied dt units to ns''' + return (dt_time * dt * 10**9) + + +def ns_time_to_dt(phys_time, dt): + '''converts time from physical (ns) to dt normalized time''' + return (phys_time * 10**(-9)) / dt + +# CDD_n helper functions +def make_cdd_n_str(n, cdd_str=""): + """ + Creates string representation of CDD_n + using recursion. + """ + if n == 1: + cdd_str += "Y-f-X-f-Y-f-X-f-" + else: + cdd_str += "Y" + cdd_str = make_cdd_n_str(n - 1, cdd_str) + cdd_str += "X" + cdd_str = make_cdd_n_str(n - 1, cdd_str) + cdd_str += "Y" + cdd_str = make_cdd_n_str(n - 1, cdd_str) + cdd_str += "X" + cdd_str = make_cdd_n_str(n - 1, cdd_str) + + return cdd_str + +def mult_paulies(p1, p2): + """ + Multiplies two string representations + of Pauli operators [p1] and [p2]. + """ + # handle trivial cases first + if p1 == "I": + return p2 + elif p2 == "I": + return p1 + elif p1 == p2: + return "I" + # now handle non-trivial case + else: + p_list = ["X", "Y", "Z"] + p_other = [p for p in p_list if p != "X" and p != "Y"] + return p_other[0] + +def simplify_cdd_str(cdd_str, sym=False): + """ + Simplifies CDD_n str by using Pauli rules, + i.e. YYY --> Y and whatnot. + """ + simp_str = "" + for p_combo in cdd_str.split("-f-"): + # add simplfieid Pauli + if len(p_combo) == 0: + continue + elif len(p_combo) == 1: + simp_str += p_combo + else: + mult = mult_paulies(p_combo[0], p_combo[1]) + for j in range(len(p_combo) - 2): + mult = mult_paulies(mult, p_combo[j]) + simp_str += mult + # add delay after + simp_str += "-f-" + + if sym is True: + simp_str = "-f-" + simp_str + "-f-" + + return simp_str + +######################################## +# Init Helper Function +######################################## +def create_basis(backend, qubit): + '''given a backend, return APL style basis set of single qubit gates in the IBMQ pulse format''' + # extract backend optimized pulses (no virtual Z gates) + defaults = backend.backend.defaults(refresh=True) + x90_index = [pulse.name for pulse in defaults.pulse_library].index(f'X90p_d{qubit}') + x180_index = [pulse.name for pulse in defaults.pulse_library].index(f'Xp_d{qubit}') + x90_samples = defaults.pulse_library[x90_index].samples + x180_samples = defaults.pulse_library[x180_index].samples + id_samples = np.zeros(len(x180_samples)) + + # construct a 'basis' of pulses + basis = {} + # construct the standard X, Y pulses with 'half rotations' + basis['I'] = SamplePulse(samples = id_samples, name = 'I') + basis['X'] = SamplePulse(samples = x180_samples, name = 'X') + basis['X90'] = SamplePulse(samples = x90_samples, name = 'X90') + basis['Y'] = SamplePulse(samples = rotate(x180_samples, np.pi/2), name = 'Y') + basis['Y90'] = SamplePulse(samples = rotate(x90_samples, np.pi/2), name = 'Y90') + # constuct the robust pulses, i.e. X-bar, Y-bar, etc... + basis['Xb'] = SamplePulse(samples = rotate(x180_samples, np.pi), name = 'Xb') + basis['X90b'] = SamplePulse(samples = rotate(x90_samples, np.pi), name = 'X90b') + basis['Yb'] = SamplePulse(samples = rotate(x180_samples, -np.pi/2), name = 'Yb') + basis['Y90b'] = SamplePulse(samples = rotate(x90_samples, -np.pi/2), name = 'Y90b') + # construct additional pulses needed for KDD + basis['X30'] = SamplePulse(samples = rotate(x180_samples, np.pi/6), name = 'X30') + basis['X120'] = SamplePulse(samples = rotate(x180_samples, (2*np.pi)/3), name = 'X120') + return basis + +def create_from_x_basis(backend, qubit): + '''given a backend, return APL style basis set of single qubit gates in the IBMQ pulse format''' + pulse_basis = {} + defaults = backend.backend.defaults(refresh=True).instruction_schedule_map + x180 = defaults.get('x',qubit).instructions[0][1].pulse + x180_samples = x180.get_waveform().samples + id_samples = np.zeros(len(x180_samples)) + + # construct the standard X, Y pulses with 'half rotations' + pulse_basis[f'I_{qubit}'] = Waveform(samples = id_samples, name = 'I') + pulse_basis[f'X_{qubit}'] = Waveform(samples = x180_samples, name = 'X') + pulse_basis[f'Y_{qubit}'] = Waveform(samples = rotate(x180_samples, np.pi/2), name = 'Y') + # constuct the robust pulses, i.e. X-bar, Y-bar, etc... + pulse_basis[f'Xb_{qubit}'] = Waveform(samples = rotate(x180_samples, np.pi), name = 'Xb') + pulse_basis[f'Yb_{qubit}'] = Waveform(samples = rotate(x180_samples, -np.pi/2), name = 'Yb') + # construct additional pulses needed for KDD + pulse_basis[f'X30_{qubit}'] = Waveform(samples = rotate(x180_samples, np.pi/6), name = 'X30') + pulse_basis[f'X120_{qubit}'] = Waveform(samples = rotate(x180_samples, (2*np.pi)/3), name = 'X120') + return pulse_basis + +def create_from_greg_basis(backend, qubit): + '''given a backend, return APL style basis set of single qubit gates in the IBMQ pulse format''' + defaults = backend.backend.defaults(refresh=True).instruction_schedule_map + greg_basis = {} + x180 = defaults.get('x', qubit).instructions[0][1].pulse + x180_samples = x180.get_waveform().samples + dur = x180.parameters['duration'] + amp = x180.parameters['amp'] + sigma = x180.parameters['sigma'] + beta = x180.parameters['beta'] + x180 = Drag(dur, amp, sigma, beta, name="X") + x90 = Drag(dur, amp/2, sigma, beta, name='X2') + x90_samples = x90.get_waveform().samples + x90m = Drag(dur, -amp/2, sigma, beta, name='X2m') + x180m = Drag(dur, -amp, sigma, beta, name='Xb') + + # get the X30 and X120 pulses for KDD + x30 = Drag(dur, rotate(amp, np.pi/6), sigma, beta, name='X30') + x120 = Drag(dur, rotate(amp, (2*np.pi)/3), sigma, beta, name='X120') + success = False + index = 1 + while not success: + try: + y90 = defaults.get('u2', qubit, P0=0, P1=0).instructions[index][1].pulse + success = True + except: + index += 1 + + dur = y90.parameters['duration'] + amp = y90.parameters['amp'] + sigma = y90.parameters['sigma'] + beta = y90.parameters['beta'] + y90m = Drag(dur, -amp, sigma, beta, name='Y2m') + y180 = Drag(dur, 2*amp, sigma, beta, name='Y') + y180m = Drag(dur, -2*amp, sigma, beta, name='Yb') + + empty_samples = np.zeros(len(x180_samples)) + + greg_basis[f'X_{qubit}'] = x180 #Waveform(samples = x180_samples, name = 'X') + greg_basis[f'X2_{qubit}'] = x90 #Waveform(samples = x90_samples, name = 'X2') + greg_basis[f'Y_{qubit}'] = y180 #Waveform(samples = rotate(x180_samples,np.pi/2), name = 'Y') + greg_basis[f'Y2_{qubit}'] = y90 #Waveform(samples = rotate(x90_samples,np.pi/2), name = 'Y2') + greg_basis[f'Xb_{qubit}'] = x180m #Waveform(samples = rotate(x180_samples,np.pi), name = 'Xm') + greg_basis[f'X2m_{qubit}'] = x90m #Waveform(samples = rotate(x90_samples,np.pi), name = 'X2m') + greg_basis[f'Yb_{qubit}'] = y180m #Waveform(samples = rotate(x180_samples,-np.pi/2), name = 'Ym') + greg_basis[f'Y2m_{qubit}'] = y90m #Waveform(samples = rotate(x90_samples,-np.pi/2), name = 'Y2m') + greg_basis[f'X30_{qubit}'] = x30 + greg_basis[f'X120_{qubit}'] = x120 + greg_basis[f"I_{qubit}"] = defaults.get('id', qubit).instructions[0][1].pulse + #greg_basis[f'I_{qubit}'] = Waveform(samples = empty_samples, name = 'I') + # create Z pulse + num_regs = backend.get_number_qubits() + circ = IBMQDdCircuit(num_regs, name='Z', ibmq_backend=backend) + circ.add_z(qubit) + greg_basis[f'Z_{qubit}'] = build_schedule(circ, backend.backend) + circ = IBMQDdCircuit(num_regs, name='Zb', ibmq_backend=backend) + circ.add_zb(qubit) + greg_basis[f'Zb_{qubit}'] = build_schedule(circ, backend.backend) + return greg_basis + +def create_from_circ_basis(backend, num_regs, qubit): + '''given a backend, return APL style basis set of single qubit gates in the IBMQ pulse format derived from Circuit API choices''' + circ_basis = {} + # create X pulse + circ = IBMQDdCircuit(num_regs, name='X', ibmq_backend=backend) + circ.add_x(qubit) + circ_basis[f'X_{qubit}'] = build_schedule(circ, backend.backend) + # create Xb pulse + circ = IBMQDdCircuit(num_regs, name='Xb', ibmq_backend=backend) + circ.add_xb(qubit) + circ_basis[f'Xb_{qubit}'] = build_schedule(circ, backend.backend) + # create Y pulse + circ = IBMQDdCircuit(num_regs, name='Y', ibmq_backend=backend) + circ.add_y(qubit) + circ_basis[f'Y_{qubit}'] = build_schedule(circ, backend.backend) + # add symmetric Y + circ = IBMQDdCircuit(num_regs, name="sym-Y", ibmq_backend=backend) + circ.rz(np.pi/2, qubit) + circ.barrier(qubit) + circ.x(qubit) + circ.barrier(qubit) + circ.rz(-np.pi/2, qubit) + circ.barrier(qubit) + circ_basis[f'symY_{qubit}'] = build_schedule(circ, backend.backend) + # create Yb pulse + circ = IBMQDdCircuit(num_regs, name='Yb', ibmq_backend=backend) + circ.add_yb(qubit) + circ_basis[f'Yb_{qubit}'] = build_schedule(circ, backend.backend) + # create X30 pulse + circ = IBMQDdCircuit(num_regs, name='X30', ibmq_backend=backend) + circ.add_pi_eta(30, qubit) + circ_basis[f'X30_{qubit}'] = build_schedule(circ, backend.backend) + # create X120 pulse + circ = IBMQDdCircuit(num_regs, name='X120', ibmq_backend=backend) + circ.add_pi_eta(120, qubit) + circ_basis[f'X120_{qubit}'] = build_schedule(circ, backend.backend) + # create I pulse + circ = IBMQDdCircuit(num_regs, name='I', ibmq_backend=backend) + circ.id(qubit) + circ.barrier(qubit) + circ.id(qubit) + circ_basis['I'] = build_schedule(circ, backend.backend) + # create Z pulse + circ = IBMQDdCircuit(num_regs, name='Z', ibmq_backend=backend) + circ.add_z(qubit) + circ_basis[f'Z_{qubit}'] = build_schedule(circ, backend.backend) + + return circ_basis + + +######################################## +# DD Sequence Helper Functions +######################################## + +######################### +# URDD Functions +######################### +def get_urdd_phis(n): + ''' Gets \phi_k values for n pulse UR sequence''' + if n % 2 == 1: + raise ValueError("n must be even") + elif n < 4: + raise ValueError("n must be >= 4") + # phi1 = 0 by convention + phis = [0] + + # get capital Phi value + if n % 4 == 0: + m = int(n / 4) + big_phi = np.pi / m + else: + m = int((n - 2) / 4) + big_phi = (2 * m * np.pi) / (2 * m + 1) + + # keep track of unique phi added; we choose phi2 = big_phi by convention-- + # only real requirement is (n * big_phi = 2pi * j for j int) + unique_phi = [0, big_phi] + # map each phi in [phis] to location (by index) of corresponding [unique_phi] + phi_indices = [0, 1] + # populate remaining phi values + for k in range(3, n+1): + phi_k = (k * (k - 1) * big_phi) / 2 + # values only matter modulo 2 pi + phi_k = (phi_k) % (2 * np.pi) + if np.isclose(phi_k, 0): + phi_k = 0 + elif np.isclose(phi_k, 2 * np.pi): + phi_k = 0 + + added_new = False + for idx, u_phi in enumerate(unique_phi): + if np.isclose(u_phi, phi_k, atol=0.001): + added_new = True + phi_indices.append(idx) + + if added_new == False: + unique_phi.append(phi_k) + phi_indices.append(len(unique_phi)-1) + + # construct phi list + phis = [] + for idx in phi_indices: + phis.append(unique_phi[idx]) + + return (phis, unique_phi, phi_indices) + +######################### +# UDD/QDD Functions +######################### +def tj(j, n, T): + """ Returns [j]th UDD_[n] time for total DD time [T].""" + frac = (j * np.pi) / (2 * n + 2) + return T * (np.sin(frac))**2 + +def make_tj_list(n, T): + """ Returns list of tj times for UDD_[n] for DD time [T].""" + if n % 2 == 0: + j_list = [j for j in range(1, n+1)] + else: + j_list = [j for j in range(1, n+2)] + return np.array([tj(j, n, T) for j in j_list]) + +def tj_k(k, m, tau_j, tj_m_1): + """ + Returns tj_[k] for QDD_n_[m] where inner pulses occur over + time [tau_j] and last outer pulse is at [tj_m_1]. + """ + frac = (k * np.pi) / (2 * m + 2) + return tau_j * (np.sin(frac))**2 + tj_m_1 + +def make_tj_k_list(m, tau_j, tj_m_1): + """ + Returns list of tj_k times for QDD_n_[m] inner pulses + over time [tau_j] where last outer pulse is at [tj_m_1]. + """ + if m % 2 == 0: + k_list = [k for k in range(1, m+1)] + else: + k_list = [k for k in range(1, m+2)] + return np.array([tj_k(k, m, tau_j, tj_m_1) for k in k_list]) + +def make_udd_sched(n, T, pulse_width=0): + """ + Make a UDD_[n] time schedule over total DD time [T] where non-ideal + pulses have finite [pulse_width]. Makes idealized tj times and then + substracts off [pulse_width] then checks no pulse overlaps. + + Returns physical_times when pulses should START. + """ + # get idealized tj times + tj_list = make_tj_list(n, T) + # turn physical times into integers + tj_list = np.array(list(map(int, np.floor(tj_list)))) + # subtract off pulse_width to get correct time to begin gates + phys_tj_list = list(map(int, tj_list - pulse_width)) + + # ensure that no gates must be applied at "negative" times + if phys_tj_list[0] < 0: + e = (f"Either n too large or T too small to accomodate pulses with\ + width {pulse_width} since first gate must be applied at t1\ + = {phys_tj_list[0]}.\n") + raise ValueError(e) + # ensure no diff in t between gates is smaller than pulse_width + diffs = [] + for idx in range(1, len(phys_tj_list)): + diffs.append(phys_tj_list[idx] - phys_tj_list[idx - 1]) + min_diff = min(diffs) + if min_diff < pulse_width: + e = (f"Minimum pulse spacing required for n={n} and T={T} is\ + {min_diff}, but pulse_width is {pulse_width}.") + + return phys_tj_list + +def find_min_udd_time(n, x_width): + """ + Finds smallest acceptable UDD time which + corresponds to smallest pulse delay. + """ + # first, get order of magnitude guess for T + T = x_width + success = False + while success is False: + try: + make_udd_sched(n, T, x_width) + success = True + except: + prev_T = T + T *= 10 + + # now try guesses incrementally until minimum T found + for Tg in range(prev_T, T): + try: + make_udd_sched(n, Tg, x_width) + break + except: + continue + + return Tg + +def make_mid_udd_sched(n, T, pulse_width=0): + """ + Make a UDD_[n] time schedule over total DD time [T] where non-ideal + pulses have finite [pulse_width]. Makes idealized tj times and then + substracts off [pulse_width]/2 then checks no pulse overlaps. + + Returns physical_times when pulses should START. + """ + # get idealized tj times + tj_list = make_tj_list(n, T) + # subtract off pulse_width to get correct time to begin gates + phys_tj_list = tj_list - (pulse_width / 2) + # schedule can only be specified to nearest integer + phys_tj_list = list(map(int, np.ceil(phys_tj_list))) + + # ensure that no gates must be applied at "negative" times + if phys_tj_list[0] < 0: + e = (f"Either n too large or T too small to accomodate pulses with\ + width {pulse_width} since first gate must be applied at t1\ + = {phys_tj_list[0]}.\n") + raise ValueError(e) + # ensure no diff in t between gates is smaller than pulse_width + diffs = [] + for idx in range(1, len(phys_tj_list)): + diffs.append(phys_tj_list[idx] - phys_tj_list[idx - 1]) + min_diff = min(diffs) + if min_diff < pulse_width: + e = (f"Minimum pulse spacing required for n={n} and T={T} is\ + {min_diff}, but pulse_width is {pulse_width}.") + + return phys_tj_list + +def make_end_udd_sched(n, T, pulse_width=0): + """ + Make a UDD_[n] time schedule over total DD time [T] where non-ideal + pulses have finite [pulse_width]. Makes idealized tj times and uses + these as start times of pulses. + + Returns physical_times when pulses should START. + """ + # get idealized tj times + tj_list = make_tj_list(n, T) + # subtract off pulse_width to get correct time to begin gates + phys_tj_list = tj_list - 0 + # schedule can only be specified to nearest integer + phys_tj_list = list(map(int, np.ceil(phys_tj_list))) + + # ensure that no gates must be applied at "negative" times + if phys_tj_list[0] < 0: + e = (f"Either n too large or T too small to accomodate pulses with\ + width {pulse_width} since first gate must be applied at t1\ + = {phys_tj_list[0]}.\n") + raise ValueError(e) + # ensure no diff in t between gates is smaller than pulse_width + diffs = [] + for idx in range(1, len(phys_tj_list)): + diffs.append(phys_tj_list[idx] - phys_tj_list[idx - 1]) + min_diff = min(diffs) + if min_diff < pulse_width: + e = (f"Minimum pulse spacing required for n={n} and T={T} is\ + {min_diff}, but pulse_width is {pulse_width}.") + + return phys_tj_list + +def make_qdd_sched(n, m, T, xpw=0, ypw=0, zpw=0): + """ + Make a QDD_[n]_[m] schedule over total DD time [T] where non-ideal + X pulse has width [xpw] and non-ideal Y pulse has width [Y]. Makes + idealized times and then substracts of [xpw] or [ypw] as appropriate. + Then checks no pulses overlap. + + Returns physical_times when pulses should START as well as the pulse + order, i.e. [1, 5, 7] and ['X', 'Y', 'X']. + """ + # first, we will make an idealized qdd schedule + # first, get ideal outer pulse times + outer_times = make_tj_list(n, T) + # cast as integers + outer_times = list(map(int, np.floor(outer_times))) + # keep track of all ideal times (inner and outer) along + # with what pulse is applied (order associated to ideal_times) + ideal_times = [] + pulses = [] + # add inner pulses to ideal_times/ pulses + for j in range(len(outer_times)): + tj = outer_times[j] + # if first element, then previous time is implicitly 0 + if j == 0: + tj_m_1 = 0 + else: + tj_m_1 = outer_times[j-1] + # tau_j is the pulse interval (or diff, t_j - t_(j-1)) + tau_j = tj - tj_m_1 + inner_times = make_tj_k_list(m, tau_j, tj_m_1) + inner_times = list(map(int, np.floor(inner_times))) + + # add inner times/ X pulses to combined lists + ideal_times.extend(inner_times) + pulses.extend(['X' for _ in range(len(inner_times))]) + # add outer times/ Y pulses to combined lists + ideal_times.append(outer_times[j]) + pulses.append('Y') + + # when m is odd, last inner pulse is simultaneous to outer + if m % 2 == 1: + pulses = pulses[:-2 or None] + pulses.append('Z') + ideal_times = ideal_times[:-1 or None] + + # now, make these ideal times "physical" by subtracting pulse widths + # off of times when ideal gate should be applied (so that pulse + # end when it should ideally be applied) + ideal_times = list(map(int, np.ceil(ideal_times))) + physical_times = [] + for (idx, p) in enumerate(pulses): + if p == 'X': + tj = ideal_times[idx] - xpw + physical_times.append(tj) + + # make finite pulse width checks + if idx == 0: + tj_m_1 = 0 + else: + tj_m_1 = physical_times[idx - 1] + tau_j = tj - tj_m_1 + if xpw > 0 and tau_j < xpw: + e = (f"Either n, m too large or T too small to accomodate\ + X pulse of width {xpw} since two pulses must be applied\ + with tau_{idx} = {tau_j} < {xpw}.") + raise ValueError(e) + elif p == 'Y': + tj = ideal_times[idx] - ypw + physical_times.append(tj) + + # make finite pulse width checks + if idx == 0: + tj_m_1 = 0 + else: + tj_m_1 = physical_times[idx - 1] + tau_j = tj - tj_m_1 + if ypw > 0 and tau_j < ypw: + e = (f"Either n, m too large or T too small to accomodate\ + Y pulse of width {ypw} since two pulses must be applied\ + with tau_{idx} = {tau_j} < {ypw}.") + raise ValueError(e) + elif p == 'Z': + tj = ideal_times[idx] - zpw + physical_times.append(tj) + + # make finite pulse width checks + if idx == 0: + tj_m_1 = 0 + else: + tj_m_1 = physical_times[idx - 1] + tau_j = tj - tj_m_1 + if zpw > 0 and tau_j < zpw: + e = (f"Either n, m too large or T too small to accomodate\ + Z pulse of width {zpw} since two pulses must be applied\ + with tau_{idx} = {tau_j} < {zpw}.") + raise ValueError(e) + + return physical_times, pulses + +def find_min_qdd_time(n, m, x_width, y_width, z_width): + """ + Finds smallest acceptable UDD time which + corresponds to smallest pulse delay. + """ + # first, get order of magnitude guess for T + T = max([x_width, y_width, z_width]) + success = False + while success is False: + try: + #print(f"trying T: {T}") + make_qdd_sched(n, m, T, x_width, y_width) + success = True + except: + #print(f"failue with T: {T}") + prev_T = T + T *= 10 + + # now try guesses incrementally until minimum T found + for Tg in range(prev_T, T): + try: + #print(f"trying T: {T} in second loop") + make_qdd_sched(n, m, Tg, x_width, y_width) + break + except: + #print(f"failue with T: {T}") + continue + + return Tg diff --git a/src/edd/pulse/_ibmq_pulse_test.py b/src/edd/pulse/_ibmq_pulse_test.py new file mode 100644 index 0000000..78893c1 --- /dev/null +++ b/src/edd/pulse/_ibmq_pulse_test.py @@ -0,0 +1,746 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import numpy as np +from edd.backend import IBMQBackend +from edd.pulse import IBMQDdSchedule + +# type check imports +from qiskit.pulse import Schedule +from qiskit.pulse import Play +from qiskit.pulse import Delay +from matplotlib.figure import Figure + +import unittest + +class IBMQScheduleTest(unittest.TestCase): + """ Tests that IBMQPulse class works smoothly. """ + + ################################################## + # Basic Tests + ################################################## + + def test_init(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + # check that member data is init properly + self.assertIsInstance(sched.ibmq_backend, IBMQBackend) + self.assertIsInstance(sched.basis, dict) + self.assertIsInstance(sched.dt, float) + self.assertIsInstance(sched.sched, Schedule) + + def test_get_schedule(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + # check that schedule is actually a Schedule object + self.assertIsInstance(sched.get_schedule(), Schedule) + + def test_get_pulse_list(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + # check that it returns a list + pulse_list = sched.get_pulse_list() + self.assertIsInstance(pulse_list, list) + # add an Play element to pulse and check type + sched.add_id(0) + pulse_list = sched.get_pulse_list() + self.assertIsInstance(pulse_list[0][1], Play) + # add Delay element to pulse and check type + sched.add_pause(0, 500) + pulse_list = sched.get_pulse_list() + self.assertIsInstance(pulse_list[1][1], Delay) + + def test_get_pulse_names(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_x(0) + pulse_names = sched.get_pulse_names() + self.assertIsInstance(pulse_names[0], str) + + def test_get_duration(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_free(0, 500) + durr = sched.get_duration() + self.assertIsInstance(durr, int) + + def test_get_phys_time(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_free(0, 500) + time = sched.get_phys_time() + self.assertIsInstance(time, float) + + def test_draw(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + fig = sched.draw() + self.assertIsInstance(fig, Figure) + + def test_reset(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_free(0, 500) + sched.reset() + durr = sched.get_duration() + self.assertEquals(durr, 0) + + ################################################## + # Simple Pulse Method Tests + ################################################## + def test_add_id(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_id(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'I') + + def test_add_x(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_x(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'X') + + def test_add_xb(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_xb(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'Xb') + + def test_add_x90(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_x90(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'X90') + + def test_add_x90b(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_x90b(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'X90b') + + def test_add_y(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_y(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'Y') + + def test_add_yb(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_yb(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'Yb') + + def test_add_y90(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_y90(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'Y90') + + def test_add_y90b(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_y90b(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'Y90b') + + def test_add_measurement(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_measurement(0) + pulse_name = sched.get_pulse_names()[0] + self.assertEquals(pulse_name, 'M_m0') + + def test_add_u3(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_u3(0, 1, 1, 1) + pulse_name1 = sched.get_pulse_names()[0] + pulse_name2 = sched.get_pulse_names()[1] + self.assertEquals(pulse_name1, 'X90p_d0') + self.assertEquals(pulse_name2, 'X90m_d0') + + ################################################## + # Test DD Sequence Methods + ################################################## + def test_add_pause(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_pause(0, 500) + pulse = sched.get_pulse_list()[0][1] + self.assertIsInstance(pulse, Delay) + self.assertEquals(sched.get_duration(), 500) + + def test_add_free(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + sched.add_free(0, 5) + pulse_names = sched.get_pulse_names() + pulse_order = ['I' for x in range(5)] + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_hahn(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_hahn(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = [None, 'X', None] + pulse_order.extend(rep) + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_cpmg(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_cpmg(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = [None, 'X', None, 'X', None] + pulse_order.extend(rep) + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_xy4(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_xy4(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['Y', None, 'X', None, 'Y', None, 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_xy4_s(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_xy4_s(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + # construct manual pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = [None, 'Y', None, 'X', None, 'Y', None, 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_cdd_n(self): + armonk = IBMQBackend('ibmq_armonk') + n = 1 + tau = 100 + # check that n = 1 works + cdd_1_sched = IBMQDdSchedule(armonk) + cdd_1_sched.add_cdd_n(0, n, tau) + pulse_names = cdd_1_sched.get_pulse_names() + # construct manual pulse order list + pulse_order = ['Y', None, 'X', None, 'Y', None, 'X', None] + self.assertSequenceEqual(pulse_names, pulse_order) + + # check that n = 2 works + n = 2 + cdd_2_sched = IBMQDdSchedule(armonk) + cdd_2_sched.add_cdd_n(0, n, tau) + pulse_names = cdd_2_sched.get_pulse_names() + # construct expected pulse order list + pulse_order2 = [] + pulse_order2.extend(['Y', None]) + pulse_order2.extend(pulse_order) + pulse_order2.extend(['X', None]) + pulse_order2.extend(pulse_order) + pulse_order2.extend(['Y', None]) + pulse_order2.extend(pulse_order) + pulse_order2.extend(['X', None]) + pulse_order2.extend(pulse_order) + self.assertSequenceEqual(pulse_names, pulse_order2) + + def test_add_rga2x(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga2x(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['Xb', None, 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga2y(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga2y(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['Yb', None, 'Y', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga4(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga4(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['Yb', None, 'X', None, 'Yb', None, 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga4p(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga4p(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['Yb', None, 'Xb', None, 'Yb', None,\ + 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga8a(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga8a(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['I', None, 'Xb', None, 'Y', None,\ + 'Xb', None, 'I', None, 'X', None,\ + 'Yb', None, 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga8c(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga8c(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + pulse_order = [] + for _ in range(num_reps): + rep = ['X', None, 'Y', None, 'X', None,\ + 'Y', None, 'Y', None, 'X', None,\ + 'Y', None, 'X', None] + pulse_order.extend(rep) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga16b(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga16b(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + rga4p = IBMQDdSchedule(armonk) + rga4p.add_rga4p(0, 1, tau) + rga4p_pulses = rga4p.get_pulse_names() + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga4p_pulses) + pulse_order.extend(['Xb', None]) + pulse_order.extend(rga4p_pulses) + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga4p_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga4p_pulses) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga32a(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga32a(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + rga8a = IBMQDdSchedule(armonk) + rga8a.add_rga8a(0, 1, tau) + rga8a_pulses = rga8a.get_pulse_names() + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8a_pulses) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga32c(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga32c(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + rga4 = IBMQDdSchedule(armonk) + rga4.add_rga4(0, 1, tau) + rga4_pulses = rga4.get_pulse_names() + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(['X', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga4_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga4_pulses) + + self.assertSequenceEqual(pulse_names, pulse_order) + + + def test_add_rga64a(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga64a(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + rga8a = IBMQDdSchedule(armonk) + rga8a.add_rga8a(0, 1, tau) + rga8a_pulses = rga8a.get_pulse_names() + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(['I', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['Xb', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['Xb', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['I', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga8a_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8a_pulses) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga64c(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga64c(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + rga8c = IBMQDdSchedule(armonk) + rga8c.add_rga8c(0, 1, tau) + rga8c_pulses = rga8c.get_pulse_names() + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(['X', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['Y', None]) + pulse_order.extend(rga8c_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga8c_pulses) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_rga256a(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_rga256a(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + + # construct manually pulse order list + rga64a = IBMQDdSchedule(armonk) + rga64a.add_rga64a(0, 1, tau) + rga64a_pulses = rga64a.get_pulse_names() + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga64a_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga64a_pulses) + pulse_order.extend(['Yb', None]) + pulse_order.extend(rga64a_pulses) + pulse_order.extend(['X', None]) + pulse_order.extend(rga64a_pulses) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_comp_kdd(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'x_basis', name='test') + tau = 100 + # test the phi = 0 case first + phi = 0 + sched.add_comp_kdd(0, phi, tau) + pulse_names = sched.get_pulse_names() + pulse_order = [None, 'X30', None, 'X', None, 'Y', None, + 'X', None, 'X30', None] + self.assertSequenceEqual(pulse_names, pulse_order) + # test the phi = pi/2 case next + phi = np.pi / 2 + sched.reset() + sched.add_comp_kdd(0, phi, tau) + pulse_names = sched.get_pulse_names() + pulse_order = [None, 'X120', None, 'Y', None, 'Xb', None, + 'Y', None, 'X120', None] + self.assertSequenceEqual(pulse_names, pulse_order) + # test a more general case with phi = random float + phi = random.random() + sched.reset() + sched.add_comp_kdd(0, phi, tau) + pulse_names = sched.get_pulse_names() + # create manual pulse name schedule + p1_ang = np.pi/6 + phi + p1_name = f'[pi]_({p1_ang:.6f})' + p2_ang = phi + p2_name = f'[pi]_({p2_ang:.6f})' + p3_ang = np.pi/2 + phi + p3_name = f'[pi]_({p3_ang:.6f})' + pulse_order = [None, p1_name, None, p2_name, None, p3_name, None, + p2_name, None, p1_name, None] + self.assertSequenceEqual(pulse_names, pulse_order) + + + def test_add_kdd(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'x_basis', name='test') + num_reps = 5 + tau = 100 + sched.add_kdd(0, num_reps, tau) + pulse_names = sched.get_pulse_names() + # compare to expected answer + x_pulse_order = [None, 'X30', None, 'X', None, 'Y', None, + 'X', None, 'X30', None] + y_pulse_order = [None, 'X120', None, 'Y', None, 'Xb', None, + 'Y', None, 'X120', None] + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(y_pulse_order) + pulse_order.extend(x_pulse_order) + pulse_order.extend(y_pulse_order) + pulse_order.extend(x_pulse_order) + + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_add_ur(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'x_basis', name='test') + num_reps = 5 + tau = 100 + # try the n = 4 case first + sched.add_ur(0, 4, num_reps, tau) + pulse_names = sched.get_pulse_names() + # compare to expected answer + ur4_pulses = [None, 'X', None, 'Xb', None, 'Xb', + None, 'X', None] + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(ur4_pulses) + self.assertSequenceEqual(pulse_names, pulse_order) + + # now try the n = 16 case + sched.reset() + sched.add_ur(0, 16, num_reps, tau) + pulse_names = sched.get_pulse_names() + p0 = 'X' + p1 = f'[pi]_({(1*np.pi / 4):.6f})' + p2 = f'[pi]_({(3*np.pi / 4):.6f})' + p3 = f'Yb' + p4 = f'Y' + p5 = f'[pi]_({(7*np.pi / 4):.6f})' + p6 = f'[pi]_({(5*np.pi / 4):.6f})' + p7 = f'Xb' + ur16_pulses = [None, p0, None, p1, None, p2, None, p3, None, + p4, None, p5, None, p6, None, p7, None, + p7, None, p6, None, p5, None, p4, None, + p3, None, p2, None, p1, None, p0, None] + pulse_order = [] + for _ in range(num_reps): + pulse_order.extend(ur16_pulses) + self.assertSequenceEqual(pulse_names, pulse_order) + + def test_udd_x(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + n = 1 + T = 10000 + # first, let's just check that pulse order is correct + times = sched.add_udd_x(0, n, T) + pulse_names = sched.get_pulse_names() + # compare to expected answer + pulse_order = [None, 'X', None, 'X'] + self.assertSequenceEqual(pulse_names, pulse_order) + # now check that times are correct + x_width = sched.basis['X'].duration + theory_times = np.array([5000, 10000]) - x_width + array_equals = (theory_times == times).all() + self.assertEquals(array_equals, True) + + # now try n = 2, an even number + n = 2 + sched.reset() + times = sched.add_udd_x(0, n, T) + pulse_names = sched.get_pulse_names() + pulse_order = [None, 'X', None, 'X', None] + self.assertSequenceEqual(pulse_names, pulse_order) + theory_times = np.array([2500, 7500]) - x_width + array_equals = (theory_times == times).all() + self.assertEquals(array_equals, True) + + def test_udd_y(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + n = 1 + T = 10000 + # first, let's just check that pulse order is correct + times = sched.add_udd_y(0, n, T) + pulse_names = sched.get_pulse_names() + # compare to expected answer + pulse_order = [None, 'Y', None, 'Y'] + self.assertSequenceEqual(pulse_names, pulse_order) + # now check that times are correct + y_width = sched.basis['Y'].duration + theory_times = np.array([5000, 10000]) - y_width + array_equals = (theory_times == times).all() + self.assertEquals(array_equals, True) + + # now try n = 2, an even number + n = 2 + sched.reset() + times = sched.add_udd_y(0, n, T) + pulse_names = sched.get_pulse_names() + pulse_order = [None, 'Y', None, 'Y', None] + self.assertSequenceEqual(pulse_names, pulse_order) + theory_times = np.array([2500, 7500]) - y_width + array_equals = (theory_times == times).all() + self.assertEquals(array_equals, True) + + def test_qdd(self): + armonk = IBMQBackend('ibmq_armonk') + sched = IBMQDdSchedule(armonk, 'g_basis', name='test') + # NOTE: inner order can never be odd or pulses overlap + n = 1 + m = 2 + T = 100000 + # first, let's just check that pulse order is correct + times = sched.add_qdd(0, n, m, T) + pulse_names = sched.get_pulse_names() + # compare to expected answer + pulse_order = [None, 'X', None, 'X', None, 'Y', None, + 'X', None, 'X', None, 'Y'] + self.assertSequenceEqual(pulse_names, pulse_order) + # now check that times are correct + x_width = sched.basis['X'].duration + y_width = sched.basis['Y'].duration + y_times = np.array([50000, 100000]) - y_width + x_times = np.array([12500, 37500, 62500, 87500]) - x_width + theory_times = [] + theory_times.append(x_times[0]) + theory_times.append(x_times[1]) + theory_times.append(y_times[0]) + theory_times.append(x_times[2]) + theory_times.append(x_times[3]) + theory_times.append(y_times[1]) + theory_times = np.array(theory_times) + array_equals = (theory_times == times).all() + self.assertEquals(array_equals, True) + +if __name__=="__main__": + unittest.main() diff --git a/src/edd/states/2q_gate_list.npy b/src/edd/states/2q_gate_list.npy new file mode 100644 index 0000000000000000000000000000000000000000..576972027d576356d672884a728816026aa7c7c1 GIT binary patch literal 451690 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 TWI`e(n`XSf&cp+)q?7*x%gLD! literal 0 HcmV?d00001 diff --git a/src/edd/workflow/__init__.py b/src/edd/workflow/__init__.py new file mode 100644 index 0000000..0a60cd6 --- /dev/null +++ b/src/edd/workflow/__init__.py @@ -0,0 +1,13 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from edd.workflow._workflow_utils import * diff --git a/src/edd/workflow/_workflow_utils.py b/src/edd/workflow/_workflow_utils.py new file mode 100644 index 0000000..6b16f53 --- /dev/null +++ b/src/edd/workflow/_workflow_utils.py @@ -0,0 +1,2299 @@ +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import itertools +import random +import datetime +import os +import time +import pandas as pd +import scipy +import edd.experiments as edde +from edd.backend import IBMQBackend +from edd.data import IBMQData +from edd.pulse import IBMQDdSchedule + +############################################# +# Post Experiment Analysis Utils +############################################# +def load_raw_data(file_list): + """ + Given [file_list], loads all the .yml files + into list of [data_sets]. + """ + data_sets = [] + for f in file_list: + # get job index + split_f = f.split('_') + job_idx = split_f.index('job') + job_n = int(split_f[job_idx + 1]) + data = IBMQData(name=f"job_{job_n}") + data.load_raw_data(f) + data.format_data() + try: + data.apply_error_mitigation() + except: + pass + + qubit = None + error_mit = True + j = 0 + while error_mit: + data_label = data.tuple_data[j][0] + if 'error_mitigate' in data_label: + j += 1 + continue + else: + split_label = data_label.split('_') + try: + ddqs_idx = split_label.index('ddqs') + except: + ddqs_idx = split_label.index('ddq') + qubit = int(split_label[ddqs_idx + 1].strip('[]')) + error_mit = False + + txt_f = f"{f[:f.find('_raw_data')]}_properties.txt" + with open(txt_f, 'r') as tf: + lines = [l.strip('\n') for l in tf.readlines()] + # get index of "qubit info" + qi_idx = lines.index('Qubit Info') + header = lines[qi_idx + 2].split(',') + # get index of T1 and T2 times + t1_idx = None + t2_idx = None + for i in range(len(header)): + c_label = header[i] + if 'T1(us)' in c_label: + t1_idx = i + elif 'T2(us)' in c_label: + t2_idx = i + # extract T1 and T2 time for relevant qubit + line_idx = qi_idx + 2 + qubit + 1 + qubit_info = lines[line_idx].split(',') + T1 = float(qubit_info[t1_idx]) + T2 = float(qubit_info[t2_idx]) + # get index of "pulse calibration" + pc_idx = lines.index("Pulse calibration information") + # get pulses used and current calibration + line_used_x = lines[pc_idx + 2].split(': ') + used_x = line_used_x[1] + line_calib_x = lines[pc_idx + 3].split(': ') + calib_x = line_calib_x[1] + line_used_y = lines[pc_idx + 4].split(': ') + used_y = line_used_y[1] + line_calib_y = lines[pc_idx + 5].split(': ') + calib_y = line_calib_y[1] + # determine if calibration was same or not + if used_x == calib_x and used_y == calib_y: + good_calib = True + else: + good_calib = False + + # add T1, T2, and good_calib labels to data label + for j in range(len(data.tuple_data)): + tup_data = data.tuple_data[j] + split_label = tup_data[0].split("_") + try: + try: + ddqs_idx = split_label.index('ddqs') + except: + ddqs_idx = split_label.index('ddq') + except: + continue + pre_ddqs = "_".join(split_label[0: ddqs_idx + 2]) + post_ddqs = "_".join(split_label[ddqs_idx + 2: ]) + mid_str = f"_T1_{T1}_T2_{T2}_goodc_{good_calib}_" + new_str = pre_ddqs + mid_str + post_ddqs + data.tuple_data[j] = (new_str, tup_data[1], tup_data[2]) + + data_sets.append(data) + + return data_sets + +def load_fixed_mistake_raw_data(file_list): + """ + Given [file_list], loads all the .yml files + into list of [data_sets]. + """ + data_sets = [] + for f in file_list: + # get job index + split_f = f.split('_') + job_idx = split_f.index('job') + job_n = int(split_f[job_idx + 1]) + data = IBMQData(name=f"job_{job_n}") + data.load_raw_data(f) + data.format_data() + data_sets.append(data) + + return data_sets + +def perform_pauli_pode_analysis(data_sets, list_of_seq, offset=0, + gen_plots = True, cwd="."): + """ + Given [data_sets] and [list_of_seqs], performs + the main rel_adv_to_XY4 pauli pode analysis. + """ + # make data container + rel_adv_data = {} + + # make directory for each seq if not already made + for seq in list_of_seq: + try: + os.mkdir(f"{cwd}/{seq}") + except: + continue + + # perform the analysis + count = 0 + for data in data_sets: + # do some complicated stuff to extract non-XY4 sequence + enter_else_lab = False + # extract the sequence name from a representative tuple data + rep_tup = data.tuple_data[0] + # extract seq and offset + for tup in data.tuple_data: + label = tup[0] + if 'offset' in label: + if 'xy4' in label and 'c_basis' in label: + continue + else: + enter_else_lab = True + split_label = label.split('_') + offset_idx = split_label.index('offset') + 1 + offset = float(split_label[offset_idx]) + seq_idx = split_label.index('decay') + 1 + seq = split_label[seq_idx] + sym_idx = split_label.index('sym') + 1 + sym = split_label[sym_idx] + if seq == 'super': + seq += f"-{split_label[seq_idx+1]}" + elif seq == 'qdd': + seq += f"-{split_label[seq_idx+1]}-{split_label[seq_idx+2]}" + break + + if enter_else_lab is False: + continue + # once seq extracted, we can actually do analysis + else: + # make fname for data to save + fname = "rel_adv_to_xy4_seq_{}_sym_{}_pode_{}_offset_{}_count_{}" + # perform analysis state by state + state_data = {} + for pode in range(6): + # analyze non-XY4 sequence performance + pode_lab = f"pode_{pode}" + data.change_working_data_by_label(seq, pode_lab) + seq_pdata = data.wrangle_fid_decay() + data.save_plot_data(f"{cwd}/{seq}/{fname.format(seq, sym, pode, offset, count)}") + seq_fit = bootstrap_pub_fit(seq_pdata) + seq_lam = seq_fit[0][0] + seq_err = seq_fit[0][1] + seq_gam = seq_fit[2][0] + seq_gam_err = seq_fit[2][1] + + # now analyze XY4 sequence performance + data.change_working_data_by_label('xy4', pode_lab) + xy4_pdata = data.wrangle_fid_decay() + data.save_plot_data(f"{cwd}/{seq}/{fname.format('xy4', sym, pode, offset, count)}") + xy4_fit = bootstrap_pub_fit(xy4_pdata) + xy4_lam = xy4_fit[0][0] + xy4_err = xy4_fit[0][1] + xy4_gam = xy4_fit[2][0] + xy4_gam_err = xy4_fit[2][1] + + # now get relative performance to XY4 + rel_adv = xy4_lam / seq_lam + rel_adv_err = np.sqrt((xy4_err / xy4_lam)**2 + (seq_err / seq_lam)**2) * rel_adv + # append ratio to list for that state + state_data[pode] = [rel_adv, rel_adv_err, seq_gam, seq_gam_err] + + if gen_plots is True: + title = fname.format(seq, sym, pode, offset, count) + fig, ax = plot_pub_decay([seq_pdata, xy4_pdata], True, title=title.replace("_", "-"), seqs=[seq.replace("_", "-"), 'xy4']) + fig.savefig(f"{cwd}/{seq}/{title}.png") + + # append state_data to total data set for this seq + if seq not in rel_adv_data: + rel_adv_data[seq] = [state_data] + else: + rel_adv_data[seq].append(state_data) + + count += 1 + + return rel_adv_data + +def extract_and_save_plot_datas(data_sets, data_dir, cwd='.'): + """ + Given [data_sets] and [list_of_seqs], generates + plot data from raw data. + """ + # keep track of file names + fnames = [] + + # make directory to store data unless already exists + try: + os.mkdir(f"{cwd}/{data_dir}") + except: + print("Directory already exists") + + # generate plot data files + for count, data in enumerate(data_sets): + # do some complicated stuff to extract non-XY4 sequence + enter_else_lab = False + # extract the sequence name from a representative tuple data + rep_tup = data.tuple_data[0] + # extract seq and offset + for tup in data.tuple_data: + label = tup[0] + if 'offset' in label: + enter_else_lab = True + split_label = label.split('_') + backname_idx = split_label.index("backName") + 1 + back_name = split_label[backname_idx] + basis_idx = split_label.index("pulse") + 1 + basis = split_label[basis_idx] + offset_idx = split_label.index('offset') + 1 + offset = float(split_label[offset_idx]) + dtype_idx = split_label.index("dtype") + 1 + delay_type = split_label[dtype_idx] + seq_idx = split_label.index('decay') + 1 + seq = split_label[seq_idx] + sym_idx = split_label.index('sym') + 1 + sym = split_label[sym_idx] + try: + qubit_idx = split_label.index('ddqs') + except: + qubit_idx = split_label.index('ddq') + qubit = split_label[qubit_idx][1:][0:-1] + goodc_idx = split_label.index('goodc') + 1 + goodc = split_label[goodc_idx] + t1_idx = split_label.index("T1") + 1 + T1 = float(split_label[t1_idx]) + t2_idx = split_label.index("T2") + 1 + T2 = float(split_label[t2_idx]) + date_idx = split_label.index("date") + 1 + date = split_label[date_idx] + if seq == 'super': + seq += f"-{split_label[seq_idx+1]}" + elif seq == 'qdd': + seq += f"-{split_label[seq_idx+1]}-{split_label[seq_idx+2]}" + if 'c_basis' in label and "xy4" in label: + seq = "ref-xy4" + break + + if enter_else_lab is False: + continue + # once seq extracted, we can actually extract data + else: + # make fname for data to save + fname = f"fidDecay_backend_{back_name}_qubit_{qubit}_" + fname += f"basis_{basis}_goodc_{goodc}_T1_{T1:0.2f}_T2_{T2:0.2f}_" + fname += f"dtype_{delay_type}_offset_{offset}_" + fname += "seq_{}_sym_{}_pode_{}_" + fname += f"date_{date}" + # perform analysis state by state + state_data = {} + for pode in range(6): + # analyze non-XY4 sequence performance + pode_lab = f"pode_{pode}" + if seq == "ref-xy4": + data.change_working_data_by_label("xy4".replace("-", "_"), pode_lab) + else: + data.change_working_data_by_label(seq.replace("-", "_"), pode_lab) + #print(data.working_data[0]) + seq_pdata = data.wrangle_fid_decay() + sfname = f"{cwd}/{data_dir}/{fname.format(seq, sym, pode)}" + fnames.append(sfname) + data.save_plot_data(sfname) + + return fnames + + +def extract_and_save_xy4_comp_plot_datas(data_sets, data_dir, cwd='.'): + """ + Given [data_sets] and [list_of_seqs], generates + plot data from raw data. + """ + # keep track of file names + fnames = [] + + # make directory to store data unless already exists + try: + os.mkdir(f"{cwd}/{data_dir}") + except: + print("Directory already exists") + + # generate plot data files + for count, data in enumerate(data_sets): + # do some complicated stuff to extract non-XY4 sequence + enter_else_lab = False + # extract the sequence name from a representative tuple data + rep_tup = data.tuple_data[0] + # extract seq and offset + for tup in data.tuple_data: + label = tup[0] + if 'offset' in label: + if 'xy4' in label and 'c_basis' in label: + continue + else: + enter_else_lab = True + split_label = label.split('_') + backname_idx = split_label.index("backName") + 1 + back_name = split_label[backname_idx] + offset_idx = split_label.index('offset') + 1 + offset = float(split_label[offset_idx]) + dtype_idx = split_label.index("dtype") + 1 + delay_type = split_label[dtype_idx] + seq_idx = split_label.index('decay') + 1 + seq = split_label[seq_idx] + sym_idx = split_label.index('sym') + 1 + sym = split_label[sym_idx] + qubit_idx = split_label.index("ddqs") + 1 + qubit = split_label[qubit_idx][1:][0:-1] + t1_idx = split_label.index("T1") + 1 + T1 = float(split_label[t1_idx]) + t2_idx = split_label.index("T2") + 1 + T2 = float(split_label[t2_idx]) + date_idx = split_label.index("date") + 1 + date = split_label[date_idx] + if seq == 'super': + seq += f"-{split_label[seq_idx+1]}" + elif seq == 'qdd': + seq += f"-{split_label[seq_idx+1]}-{split_label[seq_idx+2]}" + break + + if enter_else_lab is False: + continue + # once seq extracted, we can actually extract data + else: + # make fname for data to save + fname = f"rel_adv_to_xy4_backend_{back_name}_qubit_{qubit}_" + fname += f"T1_{T1:0.2f}_T2_{T2:0.2f}_" + fname += f"dtype_{delay_type}_offset_{offset}_" + fname += "seq_{}_sym_{}_pode_{}_" + fname += f"date_{date}" + # perform analysis state by state + state_data = {} + for pode in range(6): + # analyze non-XY4 sequence performance + pode_lab = f"pode_{pode}" + data.change_working_data_by_label(seq.replace("-", "_"), pode_lab) + #print(data.working_data[0]) + seq_pdata = data.wrangle_fid_decay() + sfname = f"{cwd}/{data_dir}/{fname.format(seq, sym, pode)}" + fnames.append(sfname) + data.save_plot_data(sfname) + + # now analyze XY4 sequence performance + data.change_working_data_by_label('xy4', 'c_basis', pode_lab) + #print(data.working_data[0]) + xy4_pdata = data.wrangle_fid_decay() + sfname = f"{cwd}/{data_dir}/{fname.format('ref-xy4', False, pode)}" + fnames.append(sfname) + data.save_plot_data(sfname) + + return fnames + + +def extract_and_save_fixed_mistake_plot_datas(data_sets, list_of_seq, + data_dir,offset=0, + cwd='.'): + """ + Given [data_sets] and [list_of_seqs], generates + plot data from raw data. + """ + # keep track of file names + fnames = [] + + # make individual directories for each sequence + for seq in list_of_seq: + try: + os.mkdir(f"{cwd}/{data_dir}/{seq}") + except: + continue + + # generate plot data files + for count, data in enumerate(data_sets): + # do some complicated stuff to extract non-XY4 sequence + enter_else_lab = False + # extract the sequence name from a representative tuple data + rep_tup = data.tuple_data[0] + # extract seq and offset + for tup in data.tuple_data: + label = tup[0] + if 'offset' in label: + if 'xy4' in label and 'c_basis' in label: + continue + else: + enter_else_lab = True + split_label = label.split('_') + offset_idx = split_label.index('offset') + 1 + offset = float(split_label[offset_idx]) + seq_idx = split_label.index('decay') + 1 + seq = split_label[seq_idx] + sym_idx = split_label.index('sym') + 1 + sym = split_label[sym_idx] + if seq == 'super': + seq += f"_{split_label[seq_idx+1]}" + elif seq == 'qdd': + seq += f"_{split_label[seq_idx+1]}_{split_label[seq_idx+2]}" + break + + if enter_else_lab is False: + continue + # once seq extracted, we can actually extract data + else: + # make fname for data to save + fname = "rel_adv_to_xy4_seq_{}_sym_{}_pode_{}_offset_{}_count_{}" + # perform analysis state by state + state_data = {} + for pode in range(6): + # analyze non-XY4 sequence performance + pode_lab = f"pode_{pode}" + data.change_working_data_by_label(seq, pode_lab) + #print(data.working_data[0]) + seq_pdata = data.wrangle_mistake_fid_decay() + sfname = f"{cwd}/{data_dir}/{seq}/{fname.format(seq, sym, pode, offset, count)}" + fnames.append(sfname) + data.save_plot_data(sfname) + + # now analyze XY4 sequence performance + data.change_working_data_by_label('xy4', 'c_basis', pode_lab) + #print(data.working_data[0]) + xy4_pdata = data.wrangle_mistake_fid_decay() + sfname = f"{cwd}/{data_dir}/{seq}/{fname.format('xy4', False, pode, offset, count)}" + fnames.append(sfname) + data.save_plot_data(sfname) + + return fnames + + +def perform_pauli_pode_seq1_to_seq2_analysis(data_sets, seq1, seq2, + offset=0, gen_plots = True, + cwd="."): + """ + Given [data_sets] and [list_of_seqs], performs + the main rel_adv_to_XY4 pauli pode analysis. + """ + # make data container + rel_adv_data = [] + + fprefix = f"{cwd}/{seq1}_to_{seq2}" + + # make directory for each seq if not already made + try: + os.mkdir(fprefix) + except: + pass + + # perform the analysis + count = 0 + for data in data_sets: + # make fname for data to save + fname = f"rel_adv_{seq1}_to_{seq2}" + fname += "_pode_{}_offset_{}_count_{}" + # perform analysis state by state + state_data = {} + for pode in range(6): + # analyze non-XY4 sequence performance + pode_lab = f"pode_{pode}" + data.change_working_data_by_label(seq1, pode_lab) + seq1_pdata = data.wrangle_fid_decay() + data.save_plot_data(f"{fprefix}/{fname.format(pode, offset, count)}") + seq1_fit = data.bootstrap_fit_aug_cosexp() + seq1_lam = seq1_fit[0][0] + seq1_err = seq1_fit[0][1] + seq1_gam = seq1_fit[2][0] + seq1_gam_err = seq1_fit[2][1] + + # now analyze XY4 sequence performance + data.change_working_data_by_label(seq2, pode_lab) + seq2_pdata = data.wrangle_fid_decay() + data.save_plot_data(f"{fprefix}/{fname.format(pode, offset, count)}") + seq2_fit = data.bootstrap_fit_aug_cosexp() + seq2_lam = seq2_fit[0][0] + seq2_err = seq2_fit[0][1] + seq2_gam = seq2_fit[2][0] + seq2_gam_err = seq2_fit[2][1] + + # now get relative performance to XY4 + rel_adv = seq2_lam / seq1_lam + rel_adv_err = np.sqrt((seq1_err / seq1_lam)**2 + (seq2_err / seq2_lam)**2) * rel_adv + # append ratio to list for that state + state_data[pode] = [rel_adv, rel_adv_err, seq1_gam, seq1_gam_err] + + if gen_plots is True: + title = fname.format(pode, offset, count) + fig, ax = plot_cosfid_decay([seq1_pdata, seq2_pdata], True, title=title.replace("_", "-"), + seqs=[seq1.replace("_", "-"), seq2.replace("_", "-")]) + fig.savefig(f"{fprefix}/{title}.png") + + rel_adv_data.append(state_data) + + count += 1 + + return rel_adv_data + +def perform_pode_max_seq_vs_min_xy4_analysis(data_sets, list_of_seq, offset=0, + gen_plots = True, cwd="."): + """ + Given [data_sets] and [list_of_seqs], performs + the main rel_adv_to_XY4 pauli pode analysis. + """ + # make data container + rel_adv_data = {} + + # make directory for each seq if not already made + for seq in list_of_seq: + try: + os.mkdir(f"{cwd}/{seq}") + except: + continue + + # perform the analysis + count = 0 + for data in data_sets: + # do some complicated stuff to extract non-XY4 sequence + enter_else_lab = False + # extract the sequence name from a representative tuple data + rep_tup = data.tuple_data[0] + # extract seq and offset + for tup in data.tuple_data: + label = tup[0] + if 'offset' in label: + if 'xy4' in label and 'tau_0dt' in label: + continue + else: + enter_else_lab = True + split_label = label.split('_') + offset_idx = split_label.index('offset') + 1 + offset = float(split_label[offset_idx]) + seq_idx = split_label.index('decay') + 1 + seq = split_label[seq_idx] + if seq == 'super': + seq += f"_{split_label[seq_idx+1]}" + elif seq == 'qdd': + seq += f"_{split_label[seq_idx+1]}_{split_label[seq_idx+2]}" + break + + if enter_else_lab is False: + continue + # once seq extracted, we can actually do analysis + else: + # make fname for data to save + fname = "rel_adv_to_min_xy4_max_seq_{}_pode_{}_offset_{}_count_{}" + # perform analysis state by state + state_data = {} + for pode in range(6): + # analyze non-XY4 sequence performance + pode_lab = f"pode_{pode}" + data.change_working_data_by_label(seq, 'reps_1', pode_lab) + seq_pdata = data.wrangle_fid_decay() + data.save_plot_data(f"{cwd}/LD-xy4/{fname.format(seq, pode, offset, count)}") + seq_fit = data.bootstrap_fit_aug_cosexp() + seq_tau = seq_fit[0][0] + seq_err = seq_fit[0][1] + seq_gam = seq_fit[2][0] + seq_gam_err = seq_fit[2][1] + + # now analyze XY4 sequence performance + data.change_working_data_by_label('xy4', 'tau_0dt', pode_lab) + xy4_pdata = data.wrangle_fid_decay() + data.save_plot_data(f"{cwd}/{seq}/{fname.format('xy4', pode, offset, count)}") + xy4_fit = data.bootstrap_fit_aug_cosexp() + xy4_tau = xy4_fit[0][0] + xy4_err = xy4_fit[0][1] + xy4_gam = xy4_fit[2][0] + xy4_gam_err = xy4_fit[2][1] + + # now get relative performance to XY4 + rel_adv = seq_tau / xy4_tau + rel_adv_err = np.sqrt((xy4_err / xy4_tau)**2 + (seq_err / seq_tau)**2) * rel_adv + # append ratio to list for that state + state_data[pode] = [rel_adv, rel_adv_err, seq_gam, seq_gam_err] + + if gen_plots is True: + title = fname.format(seq, pode, offset, count) + fig, ax = plot_cosfid_decay([seq_pdata, xy4_pdata], True, title=title.replace("_", "-"), + seqs=[f"LD {seq.replace('_', '-')}", 'SD xy4']) + fig.savefig(f"{cwd}/{seq}/{title}.png") + + # append state_data to total data set for this seq + if seq not in rel_adv_data: + rel_adv_data[seq] = [state_data] + else: + rel_adv_data[seq].append(state_data) + + count += 1 + + return rel_adv_data + + +def calc_med_with_err(values, errs): + """ + Given [values] +/- [errs], finds the median + and propogates the error if necessary. + """ + # handle odd case first + if len(values) % 2 == 1: + med_idx = np.argsort(values)[len(values)//2] + med = values[med_idx] + med_err = errs[med_idx] + else: + sort_idxs = np.argsort(values) + idx_of_med_idx = len(sort_idxs) // 2 + med_idx1 = sort_idxs[idx_of_med_idx] + med_idx0 = sort_idxs[idx_of_med_idx - 1] + med = (values[med_idx0] + values[med_idx1]) / 2 + weight = np.sqrt((errs[med_idx0]/values[med_idx0])**2 + (errs[med_idx1]/values[med_idx1])**2) + med_err = med * weight + + return med, med_err + +def make_rel_adv_summary(rel_adv_data, fname, pd=''): + """ + Takes [rel_adv_data] and summarizes results into + [fname].txt. + """ + # write header + with open (fname, 'w') as f: + header = "seq, " + for j in range(6): + header += f"p{j}_ra, p{j}_ra_2sigma, p{j}_gam, p{j}_gam_2sig " + header += "min, 2sig_min, max, 2sig_max, med, 2sig_med, mean, 2sig_mean\n" + f.write(header) + + # write lines for each sequence + for seq in rel_adv_data: + # set up ratio list of each pode + ratios = [[] for _ in range(6)] + errors = [[] for _ in range(6)] + gams = [[] for _ in range(6)] + gam_errs = [[] for _ in range(6)] + data = rel_adv_data[seq] + # iterate over trials + for trial in data: + # iterate over podes + for j in range(6): + ratios[j].append(trial[j][0]) + errors[j].append(trial[j][1]) + gams[j].append(np.abs(trial[j][2])) + gam_errs[j].append(trial[j][3]) + + + # set up list of means/ errs + means = [] + tot_errs = [] + gam_means = [] + gam_tot_errs = [] + for j in range(6): + # get mean/ error over trials for fidelity + # calculate mean + m = np.mean(ratios[j]) + means.append(m) + # calculate error + std = 2*np.std(ratios[j]) + prop_uncert = m * np.sqrt(np.sum((np.array(errors[j]) / np.array(ratios[j]))**2)) + tot_e = np.sqrt(std**2 + prop_uncert**2) + tot_errs.append(prop_uncert) + # get mean/ error over trials for gamma + g_m = np.mean(gams[j]) + gam_means.append(g_m) + # calculate error + std = 2*np.std(gams[j]) + prop_uncert = g_m * np.sqrt(np.sum((np.array(gam_errs[j]) / np.array(gams[j]))**2)) + tot_e = np.sqrt(std**2 + prop_uncert**2) + gam_tot_errs.append(tot_e) + + # calculate order statistics + # min + min_idx = np.argmin(means) + minn = means[min_idx] + minn_err = tot_errs[min_idx] + # max + max_idx = np.argmax(means) + maxx = means[max_idx] + maxx_err = tot_errs[max_idx] + # median + med, med_err = calc_med_with_err(means, tot_errs) + # mean + mean = np.mean(means) + weight = 0.0 + for j in range(6): + weight += (tot_errs[j] / means[j])**2 + mean_err = mean * np.sqrt(weight) + + with open(fname, 'a') as f: + line = f"{pd}{seq}," + for j in range(6): + line += f"{means[j]:0.4f},{tot_errs[j]:0.4f},{gam_means[j]:0.4f},{gam_tot_errs[j]:0.4f}," + line += f"{minn},{minn_err}," + line += f"{maxx},{maxx_err}," + line += f"{med},{med_err}," + line += f"{mean},{mean_err}\n" + f.write(line) + + return + + +def load_rel_adv_summary(fname): + """ + Loads .txt file summary of rel advantage experiment + into a list of lists. + """ + seq_list = [] + p0_data = [] + p1_data = [] + p2_data = [] + p3_data = [] + p4_data = [] + p5_data = [] + med_data = [] + with open(fname, 'r') as f: + # skip header + f.readline() + # get data for each sequence + for line in f.readlines(): + data = line.rstrip().split(',') + # add seq + seq_list.append(data[0].replace('_', '-')) + # add p0 data + p0_data.append([float(x) for x in data[1:5]]) + p1_data.append([float(x) for x in data[5:9]]) + p2_data.append([float(x) for x in data[9:13]]) + p3_data.append([float(x) for x in data[13:17]]) + p4_data.append([float(x) for x in data[17:21]]) + p5_data.append([float(x) for x in data[21:25]]) + med_data.append([float(data[-4]), float(data[-3])]) + + all_data = [seq_list, p0_data, p1_data, p2_data, p3_data, p4_data, p5_data, med_data] + return all_data + + +def combine_rel_adv_data(*data_sets): + """ + Combines relative advantage [data_sets] + into one large data set. + """ + big_dset = [[] for _ in range(8)] + for dset in data_sets: + for j in range(8): + big_dset[j].extend(dset[j]) + + return big_dset + +def argsort_seq(o_seq, u_seq): + """ + Given an ordered set [o_seq] of strings, + find indices that put unordered set [u_seq] + into same order. + """ + indices = [] + for seq in o_seq: + idx = u_seq.index(seq) + indices.append(idx) + + return np.array(indices) + +def get_data_subset(data, seqs): + """ + Extracts only data of [seqs]. + """ + sub_data = [[] for _ in range(len(data))] + + for j in range(len(data[0])): + if data[0][j] in seqs: + sub_data[0].append(data[0][j]) + for k in range(1, len(data)): + sub_data[k].append(data[k][j]) + + ord_data = [[] for _ in range(len(sub_data))] + # now re-order in accordance with seqs order + idx_order = argsort_seq(seqs, sub_data[0]) + for j in range(len(sub_data[0])): + for k in range(len(sub_data)): + ord_data[k].append(sub_data[k][idx_order[j]]) + + return ord_data + +def sep_data_with_osc(seqs, pj_data): + """ + Separates data into set of those with negligible oscillations + and those with apparent oscillations. + """ + x = [] + xg = [] + y = [] + y_err = [] + yg = [] + yg_err = [] + for idx, vals in enumerate(pj_data): + if np.abs(vals[2]) > 1000: + if np.abs(vals[0]) > 10: + x.append(seqs[idx]) + y.append(vals[0]) + y_err.append(0) + else: + x.append(seqs[idx]) + y.append(vals[0]) + y_err.append(vals[1]) + else: + xg.append(seqs[idx]) + yg.append(vals[0]) + yg_err.append(vals[1]) + + return x, y, y_err, xg, yg, yg_err + +def sep_med_data(med_data): + """ + Separates median data from tuple list of the form + [(med1, med_err1), ...] into [[med1, med2...], [err1, err2...]] + """ + y = [] + y_err = [] + for tup in med_data: + y.append(tup[0]) + y_err.append(tup[1]) + + return y, y_err + +def get_xticks_from_seq(seq, sub_seq): + """ + Create x-tick (i.e. plot x values) list + gives a sub_seq of seq that respects original + ordering/ spacing. + """ + x_ticks = [] + for j in range(len(seq)): + if seq[j] in sub_seq: + x_ticks.append(j) + + return np.array(x_ticks) + + + +############################################# +# Plotting Utilties +############################################# +def plot_podal_ra_results(data, figsize=1200, xmax=4): + """ + Plots average improvement over XY4 for each sequence + over the 6 Pauli states + the median performance. + """ + fig, ax = plt.subplots(figsize=set_size(figsize)) + #fig.set_size_inches(18.5, 10.5) + prop = ax._get_lines.prop_cycler + # first, "decompress" the data for convenience + seqs, p0, p1, p2, p3, p4, p5, avg = data + xvals = np.array(list(range(len(seqs)))) + datas = [p0, p1, p2, p3, p4, p5, avg] + for j, pj in enumerate(datas): + offset = 0.1*(j - (len(datas)/2)) + # create label + if j == 0: + label = '0' + elif j == 1: + label = '1' + elif j == 2: + label = '+' + elif j == 3: + label = '-' + elif j == 4: + label = '+i' + elif j == 5: + label = '-i' + else: + label = 'median' + c = next(prop)['color'] + if j < 6: + x, y, y_err, xg, yg, yg_err = sep_data_with_osc(seqs, pj) + x_ticks = get_xticks_from_seq(seqs, x) + xg_ticks = get_xticks_from_seq(seqs, xg) + ax.errorbar(x_ticks+offset, y, yerr=y_err, fmt='.', marker='o', color=c, label=label) + if xg != []: + ax.errorbar(xg_ticks+offset, yg, yerr=yg_err, fmt='.', marker='x', color=c) + else: + y, y_err = sep_med_data(pj) + ax.errorbar(xvals+offset, y, yerr=y_err, fmt='.', marker='v', color=c, label=label) + + ax.legend() + + ax.set_xticks(xvals) + for j in range(len(seqs)): + if 'super' in seqs[j]: + seqs[j] = seqs[j].replace('super', 's') + + ax.set_xticklabels(seqs) + ax.set_ylabel("Relative advantage to XY4") + ax.set_ylim(0, xmax) + c = next(prop)['color'] + ax.axhline(y=1, color=c, linestyle='-') + + return fig, ax + +def plot_podal_max_tau_ra_results(data, figsize=1200, xmax=4): + """ + Plots average improvement over XY4 for each sequence + over the 6 Pauli states + the median performance. + Here, sequences have maximum \tau and are repeated only + once, but XY4 has min \tau and is repeated many times. + """ + fig, ax = plt.subplots(figsize=set_size(figsize)) + #fig.set_size_inches(18.5, 10.5) + prop = ax._get_lines.prop_cycler + # first, "decompress" the data for convenience + seqs, p0, p1, p2, p3, p4, p5, avg = data + xvals = np.array(list(range(len(seqs)))) + datas = [p0, p1, p2, p3, p4, p5, avg] + for j, pj in enumerate(datas): + offset = 0.1*(j - (len(datas)/2)) + # create label + if j == 0: + label = '0' + elif j == 1: + label = '1' + elif j == 2: + label = '+' + elif j == 3: + label = '-' + elif j == 4: + label = '+i' + elif j == 5: + label = '-i' + else: + label = 'median' + c = next(prop)['color'] + if j < 6: + x, y, y_err, xg, yg, yg_err = sep_data_with_osc(seqs, pj) + x_ticks = get_xticks_from_seq(seqs, x) + xg_ticks = get_xticks_from_seq(seqs, xg) + ax.errorbar(x_ticks+offset, y, yerr=y_err, fmt='.', marker='o', color=c, label=label) + if xg != []: + ax.errorbar(xg_ticks+offset, yg, yerr=yg_err, fmt='.', marker='x', color=c) + else: + y, y_err = sep_med_data(pj) + ax.errorbar(xvals+offset, y, yerr=y_err, fmt='.', marker='v', color=c, label=label) + + ax.legend() + + ax.set_xticks(xvals) + for j in range(len(seqs)): + if 'super' in seqs[j]: + seqs[j] = seqs[j].replace('super', 's') + seqs[j] = f"LD {seqs[j]}" + + print(seqs) + + ax.set_xticklabels(seqs) + ax.set_ylabel("Relative advantage to XY4") + ax.set_ylim(0, xmax) + c = next(prop)['color'] + ax.axhline(y=1, color=c, linestyle='-') + + return fig, ax + +def gen_pub_conf_curve(a0_fit, lam_fit, gam_fit, smooth_t): + """ + Given fitting parameters and times of interest, + generates a 2 \sigma confidence band. + """ + # extract fitting params + a0, a0_err = a0_fit + lam, lam_err = lam_fit + gam, gam_err = gam_fit + + # set up data containers + func_vals = [[] for _ in range(len(smooth_t))] + + for _ in range(1000): + s_a0 = np.random.normal(a0, a0_err / 2) + s_lam = np.random.normal(lam, lam_err / 2) + s_gam = np.random.normal(gam, gam_err / 2) + + s_func_vals = pub_func([s_a0, s_lam, s_gam], smooth_t) +def gen_pub_conf_curve(a0_fit, lam_fit, gam_fit, smooth_t): + """ + Given fitting parameters and times of interest, + generates a 2 \sigma confidence band. + """ + # extract fitting params + a0, a0_err = a0_fit + lam, lam_err = lam_fit + gam, gam_err = gam_fit + + # set up data containers + func_vals = [[] for _ in range(len(smooth_t))] + + for _ in range(1000): + s_a0 = np.random.normal(a0, a0_err / 2) + s_lam = np.random.normal(lam, lam_err / 2) + s_gam = np.random.normal(gam, gam_err / 2) + + s_func_vals = pub_func([s_a0, s_lam, s_gam], smooth_t) + + for j in range(len(s_func_vals)): + func_vals[j].append(s_func_vals[j]) + + st_devs = map(np.std, func_vals) + st_devs = np.array([2 * sig for sig in st_devs]) + + # generate the min and max values of func + avg_vals = pub_func([a0, lam, gam], smooth_t) + min_vals = avg_vals - st_devs + max_vals = avg_vals + st_devs + + return min_vals, max_vals + +def plot_pub_decay(plot_datas, with_fit=False, title='', seqs=None, size=400): + """ + Plots [self.plot_data] assuming it was produced + from fidelity decay experiment. + """ + fig, ax = plt.subplots(figsize=set_size(size)) + prop = ax._get_lines.prop_cycler + mark_gen = itertools.cycle(('o', 's', 'v')) + if seqs is None: + seqs = ['' for _ in range(len(plot_datas))] + s_idx = 0 + legend = [] + for p_data in plot_datas: + times, fids, errs = p_data + time_ord = np.argsort(times) + times = times[time_ord] + fids = fids[time_ord] + errs = errs[time_ord] + color = next(prop)['color'] + marker = next(mark_gen) + # simple plot + if with_fit is False: + ax.errorbar(times, fids, yerr=errs, color=color, fmt=f"{marker}-") + ax.set_xlabel('time ($\\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + legend.append(seqs[s_idx]) + # plot with simple_exp fitting + else: + # get fit and plot it + a0_fit, lam_fit, gam_fit, _ = bootstrap_pub_fit(p_data) + a0, a0_err = a0_fit + lam, lam_err = lam_fit + gam, gam_err = gam_fit + ax.errorbar(times, fids, yerr=errs, color=color, fmt=f"{marker}") + smooth_t = np.linspace(min(times), max(times), 100) + ax.plot(smooth_t, pub_func([a0, lam, gam], smooth_t), color=color) + # format the legend with the decay constant and 2 std value + st_lam = f'{(1000 * lam):.2f}' + st_lam_err = f'{(1000*lam_err):.2f}' + if np.abs(gam) < 1/1000: + st_gam = "0" + legend.append(f'{seqs[s_idx]}, $\\lambda$ = {st_lam} $\pm$ {st_lam_err} (MHz), $\\gamma$ = {st_gam} (MHz)') + else: + st_gam = f'{(1000*gam):.2f}' + st_gam_err = f'{(1000*gam_err):.2f}' + legend.append(f'{seqs[s_idx]}, $\\lambda$ = {st_lam} $\pm$ {st_lam_err} (MHz), $\\gamma$ = {st_gam} $\pm$ {st_gam_err} (MHz)') + # add the 95% confidence intervals + min_vals, max_vals = gen_pub_conf_curve(a0_fit, lam_fit, gam_fit, smooth_t) + ax.fill_between(smooth_t, min_vals, max_vals, color=color, alpha=.2) + # add labels + ax.set_xlabel('time ($\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + s_idx += 1 + ax.legend(legend) + + return (fig, ax) + +def plot_cosfid_decay(plot_datas, with_fit=False, title='', seqs=None): + """ + Plots [self.plot_data] assuming it was produced + from fidelity decay experiment. + """ + fig, ax = plt.subplots(figsize=set_size(400)) + prop = ax._get_lines.prop_cycler + if seqs is None: + seqs = ['' for _ in range(len(plot_datas))] + s_idx = 0 + legend = [] + for p_data in plot_datas: + times, fids, errs = p_data + color = next(prop)['color'] + # simple plot + if with_fit is False: + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ax.set_xlabel('time ($\\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + legend.append(seqs[s_idx]) + # plot with simple_exp fitting + else: + # get fit and plot it + fit_params, par_params, gam_params, sum_res = bootstrap_fit_aug_cosexp(p_data) + tau = fit_params[0] + tau_err = fit_params[1] + a0 = par_params[0] + a0_err = par_params[1] + gam = gam_params[0] + gam_err = gam_params[1] + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ord_t = np.array(sorted(times)) + ax.plot(ord_t, aug_cosexp_func([a0, tau, gam], ord_t), color=color) + # format the legend with the decay constant and 2 std value + st_tau = f'{tau:.2f}' + st_tau_err = f'{tau_err:.2f}' + if np.abs(gam) > 1000: + st_gam = "$\\infty$" + st_gam_err = "0" + else: + st_gam = f'{gam:.2f}' + st_gam_err = f'{gam_err:.2f}' + legend.append(f'{seqs[s_idx]}, $\\lambda$ = {st_tau} $\pm$ {st_tau_err}, $\\gamma$ = {st_gam} $\pm$ {st_gam_err}') + # add the 95% confidence intervals + small = aug_cosexp_func([a0+a0_err, tau+tau_err, gam], ord_t) + big = aug_cosexp_func([a0-a0_err, tau-tau_err, gam], ord_t) + ax.fill_between(ord_t, aug_cosexp_func([a0+a0_err, tau+tau_err, gam], ord_t), aug_cosexp_func([a0-a0_err, tau-tau_err, gam], ord_t), + color=color, alpha=.2) + # add labels + ax.set_xlabel('time ($\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + s_idx += 1 + ax.legend(legend) + + return (fig, ax) + +def load_plot_data(fname): + """ + Given [fname], read file in and extract + plot data in desired format. + """ + times = [] + fids = [] + errs = [] + with open(fname, 'r') as f: + for line in f.readlines(): + T, fid, err = [float(x) for x in line.split(',')] + times.append(T) + fids.append(fid) + errs.append(err) + + return [np.array(times), np.array(fids), np.array(errs)] + +def set_size(width, fraction=1): + """ Set aesthetic figure dimensions to avoid scaling in latex. + + Parameters + ---------- + width: float + Width in pts + fraction: float + Fraction of the width which you wish the figure to occupy + + Returns + ------- + fig_dim: tuple + Dimensions of figure in inches + """ + # Width of figure + fig_width_pt = width * fraction + + # Convert from pt to inches + inches_per_pt = 1 / 72.27 + + # Golden ratio to set aesthetic figure height + golden_ratio = (5 ** 0.5 - 1) / 2 + + # Figure width in inches + fig_width_in = fig_width_pt * inches_per_pt + # Figure height in inches + fig_height_in = fig_width_in * golden_ratio + + return fig_width_in, fig_height_in + +def plot_fid_decay(plot_datas, with_fit=False, title='', legend=None): + """ + Plots [self.plot_data] assuming it was produced + from fidelity decay experiment. + """ + fig, ax = plt.subplots(figsize=set_size(400)) + prop = ax._get_lines.prop_cycler + for p_data in plot_datas: + times, fids, errs = p_data + color = next(prop)['color'] + if with_fit is True and legend is None: + legend = [] + # simple plot + if with_fit is False: + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ax.set_xlabel('time ($\\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + # plot with simple_exp fitting + else: + # get fit and plot it + fit_params, par_params, sum_res = bootstrap_fit_aug_exp(p_data) + tau = fit_params[0] + tau_err = fit_params[1] + a0 = par_params[0] + a0_err = par_params[1] + ax.errorbar(times, fids, yerr=errs, fmt='.', color=color) + ord_t = np.array(sorted(times)) + ax.plot(ord_t, aug_exp_func([a0, tau], ord_t), color=color) + # format the legend with the decay constant and 2 std value + st_tau = f'{tau:.2f}' + st_tau_err = f'{tau_err:.2f}' + legend.append(f'$\\lambda$ = {st_tau} $\pm$ {st_tau_err}') + # add the 95% confidence intervals + ax.fill_between(ord_t, aug_exp_func([a0+a0_err, tau+tau_err], ord_t), aug_exp_func([a0-a0_err, tau-tau_err], ord_t), + color=color, alpha=.2) + # add labels + ax.set_xlabel('time ($\mu$s)') + ax.set_ylabel('fidelity') + ax.set_title(title) + if legend is not None: + ax.legend(legend) + + return (fig, ax) + +############################################# +# Reproducibility Statistics Utilities +############################################# +def order_files(files): + """ + Orders files by job #. + """ + job_nums = [] + for f in files: + split_f = f.split('_') + job_idx = split_f.index('job') + job_n = int(split_f[job_idx + 1]) + job_nums.append(job_n) + + new_order = np.argsort(job_nums) + new_files = [] + for i in new_order: + new_files.append(files[i]) + + return new_files + +def get_data_over_calibs(file_list): + """ + Given a file list of data sets, organize the + data into a dictionary of the form + {'calib 1 date': [job0, job1, ..., jobn], + 'calib 2 date': [job0, job1, ..., jobn]} + """ + data_over_calib = {} + + for f in file_list: + # get job index + split_f = f.split('_') + job_idx = split_f.index('job') + job_n = int(split_f[job_idx + 1]) + data = IBMQData(name=f"job_{job_n}") + data.load_raw_data(f) + data.format_data() + + # get txt file and extract calib date + txt_f = f"{f[:f.find('_raw_data')]}_properties.txt" + with open(txt_f, 'r') as tf: + update = tf.readlines()[5] + date = update[18:] + if date not in data_over_calib: + data_over_calib[date] = [data] + else: + data_over_calib[date].append(data) + + return data_over_calib + +def perform_calib_fid_analysis(data_over_calib, file_list, label='free'): + """ + Given a dictionary with data over calibration cycles + and the files (in same order) from which it was made, + performs analysis to extract fidelities across each job. + Gives the results in the form of four dictionaries + 1. split_job_fid_dict + 2. per_calib_fid_dict + 3. job_len_dict + 4. complete_times_dict + """ + split_job_fid_dict = {} + per_calib_fid_dict = {} + job_len_dict = {} + complete_times_dict = {} + + tot_idx = 0 + for key in data_over_calib: + key_fid_list = [] + job_len_list = [] + complete_times = [] + for data in data_over_calib[key]: + dat_file = file_list[tot_idx] + tot_idx += 1 + job_idx = dat_file.split('_').index('job') + c_time = dat_file.split('_')[job_idx + 3][0:-3] + complete_times.append(c_time) + fid_list = [] + job_len = 0 + data.change_working_data_by_label(label) + for tup in data.working_data: + job_len += 1 + counts = tup[2] + fid = counts['0x0'] / (counts['0x0'] + counts['0x1']) + fid_list.append(fid) + + key_fid_list.append(fid_list) + job_len_list.append(job_len) + + split_job_fid_dict[key] = key_fid_list + flat_fid_list = [item for sublist in key_fid_list for item in sublist] + per_calib_fid_dict[key] = flat_fid_list + job_len_dict[key] = job_len_list + complete_times_dict[key] = complete_times + + return split_job_fid_dict, per_calib_fid_dict, job_len_dict, complete_times_dict + +def bootstrapci(data, n=1000, func=np.mean, ci=.95): + """ + Generate `n` bootstrap samples, evaluating `func` + at each resampling. Then computes error bar with + 'ci' confidence interval on data. + """ + simulations = np.zeros(n) + sample_size = len(data) + xbar = func(data) + for c in range(n): + itersample = np.random.choice(data, size=sample_size, replace=True) + simulations[c] = func(itersample) + + std = np.std(simulations) + return np.mean(data), 2*std + +def boot_fid(fid, shots=8000): + """ + Given a [fid], bootstraps the + error given that [shots] shots + were taken to obtain [fid]. + """ + n0 = int(np.ceil(shots * fid)) + n1 = shots - n0 + fake_data = [] + for j in range(n0): + fake_data.append(1) + for j in range(n1): + fake_data.append(0) + random.shuffle(fake_data) + + return bootstrapci(fake_data) + +############################################# +# Fitting Utilties +############################################# +########################## +# Exp + Cos fit +######################### +def gen_time_series_sample(plot_data): + """ + Given [plot_data] of the form [xdata, ydata, yerrs], + generates a new sample data set of the form + [xsamp, ysamp, yerrs] useful for bootstrapping. + + Assumes yerr is 2 \sigma. + """ + xs, ys, errs = plot_data + samp_ys = [] + # get 1 sigma errs instead for sampling + errs = errs / 2 + for i in range(len(ys)): + # generate sample y from normal dist + s_y = np.random.normal(ys[i], errs[i]) + samp_ys.append(s_y) + + return [xs, samp_ys, errs] + +########################################### +# pub(lication) fit +########################################### +def pub_func(p, t): + """ + F(t) = 1/2(p[0]*e^{-p[1] t} + 1)*Cos(p[2]*t) + """ + mid_val = p[0]*np.exp(-p[1]*t)*np.cos(p[2]*t) + return (1/2)*(mid_val + 1) + +def perform_pub_fit(plot_data, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = pub_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + t, fid, yerr = plot_data + ysig = (yerr / 2) + + def err_func(p, t, fid, err): + return sum(((fid - pub_func(p, t)) / err)**2) + + if seed is None: + p_init = [1.0, 1 / 10, 1 / 40] + else: + p_init = seed + + bnds = ((0.0, 1.0), (0.0, 10.0), (0.0, 10.0)) + out = scipy.optimize.minimize(err_func, p_init, args=(t, fid, ysig), bounds=bnds) + + # collect fitting parameters + a0, lam, gam = out.x + # get sum of squared residuals + res_sum = np.sum((pub_func([a0, lam, gam], t) - fid)**2) + red_chi_sq = res_sum / (len(fid) - 2) + + return a0, lam, gam, res_sum + +def bootstrap_pub_fit(plot_data, boot_samps=1000, seed=None): + """ + Bootstrap fit of [plots_data] to the aug_exp function. + """ + a0_list = [] + lam_list = [] + gam_list = [] + + # do fit on original data + a0, lam, gam, _ = perform_pub_fit(plot_data, seed) + a0_list.append(a0) + lam_list.append(lam) + gam_list.append(gam) + seed = [a0, lam, gam] + + for i in range(boot_samps): + # generate sample of plot_data + samp_plot_data = gen_time_series_sample(plot_data) + # fit this data and add to lists + a0, lam, gam, _ = perform_pub_fit(samp_plot_data, seed) + a0_list.append(a0) + lam_list.append(lam) + gam_list.append(gam) + + avg_a0 = np.mean(a0_list) + a0_std = 2*np.std(a0_list) + avg_lam = np.mean(lam_list) + lam_std = 2*np.std(lam_list) + avg_gam = np.mean(gam_list) + gam_std = 2*np.std(gam_list) + + # obtain sum of residuals squared + t, fid, _ = plot_data + rsq_sum = np.sum((pub_func([avg_a0, avg_lam, avg_gam], t) - fid)**2) + + return [avg_a0, a0_std], [avg_lam, lam_std], [avg_gam, gam_std], rsq_sum + +########################################### +# aug_cosexp fit +########################################### + +def aug_cosexp_func(p, t): + """ + F(t) = 1/2(p[0]*e^{-t / p[1]} + 1)*Cos(p[2]*t) + """ + mid_val = p[0]*np.exp(-t/p[1])*np.cos(t/p[2]) + return (1/2)*(mid_val + 1) + +def perform_aug_cosexp_fit(plot_data, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = aug_cosexp_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + t, fid, yerr = plot_data + ysig = (yerr / 2) + + def err_func(p, t, fid, err): + return ((fid - aug_cosexp_func(p, t)) / err)**2 + + if seed is None: + p_init = [1.0, 50.0, 50.0] + else: + p_init = seed + out = scipy.optimize.leastsq(err_func, p_init, + args=(t, fid, ysig), full_output=1) + + # collect fitting parameters + a0, tau, gam = out[0] + covar = out[1] + # get sum of squared residuals + res_sum = np.sum((aug_cosexp_func([a0, tau, gam], t) - fid)**2) + red_chi_sq = res_sum / (len(fid) - 2) + # get errors if not None + if covar is not None: + if covar[0][0] is not None: + a0_err = np.sqrt(covar[0][0] * red_chi_sq) + else: + a0_err = 'undetermined' + if covar[1][1] is not None: + tau_err = np.sqrt(covar[1][1] * red_chi_sq) + else: + tau_err = 'undetermined' + if covar[2][2] is not None: + gam_err = np.sqrt(covar[2][2] * red_chi_sq) + else: + gam_err = 'undetermined' + else: + a0_err = 'undetermined' + tau_err = 'undetermined' + gam_err = 'undetermined' + + return [tau, tau_err], [a0, a0_err], [gam, gam_err], res_sum + +def bootstrap_fit_aug_cosexp(plot_data, boot_samps=1000, seed=None): + """ + Bootstrap fit of [plots_data] to the aug_exp function. + """ + a0_list = [] + tau_list = [] + gam_list = [] + + # do fit on original data + tau_fit, a0_fit, gam_fit, _ = perform_aug_cosexp_fit(plot_data, seed) + a0 = a0_fit[0] + a0_list.append(a0) + tau = tau_fit[0] + tau_list.append(tau) + gam = gam_fit[0] + gam_list.append(gam) + seed = [a0, tau, gam] + + for i in range(boot_samps): + # generate sample of plot_data + samp_plot_data = gen_time_series_sample(plot_data) + # fit this data and add to lists + tau_fit, a0_fit, gam_fit, _ = perform_aug_cosexp_fit(samp_plot_data, seed) + a0_list.append(a0_fit[0]) + tau_list.append(tau_fit[0]) + gam_list.append(gam_fit[0]) + + avg_a0 = np.mean(a0_list) + a0_std = 2*np.std(a0_list) + avg_tau = np.mean(tau_list) + tau_std = 2*np.std(tau_list) + avg_gam = np.mean(gam_list) + gam_std = 2*np.std(gam_list) + + # obtain sum of residuals squared + t, fid, _ = plot_data + rsq_sum = np.sum((aug_cosexp_func([avg_a0, avg_tau, avg_gam], t) - fid)**2) + + return [avg_tau, tau_std], [avg_a0, a0_std], [avg_gam, gam_std], rsq_sum + +########################## +# Exp Fit +######################### +def aug_exp_func(p, t): + """ + F(t) = 1/2(p[0]*e^{-t / p[1]} + 1) + """ + # define f(t) + return (1/2)*(p[0]*np.exp(-t/p[1]) + 1) + +def perform_aug_exp_fit(plot_data, seed=None): + """ + Takes plot_data (x, y, yerr) & fits a function of the form + F(t) = aug_exp_func(p, t) + such that sum{[(F(t) - y)/yerr]^2} is minimized, i.e. + least squares weighted by errors. + """ + t, fid, yerr = plot_data + ysig = (yerr / 2) + + def err_func(p, t, fid, err): + return ((fid - aug_exp_func(p, t)) / err)**2 + + if seed is None: + p_init = [1.0, 50.0] + else: + p_init = seed + out = scipy.optimize.leastsq(err_func, p_init, + args=(t, fid, ysig), full_output=1) + + # collect fitting parameters + a0, tau = out[0] + covar = out[1] + # get sum of squared residuals + res_sum = np.sum((aug_exp_func([a0, tau], t) - fid)**2) + red_chi_sq = res_sum / (len(fid) - 2) + # get errors if not None + if covar is not None: + if covar[0][0] is not None: + a0_err = np.sqrt(covar[0][0] * red_chi_sq) + else: + a0_err = 'undetermined' + if covar[1][1] is not None: + tau_err = np.sqrt(covar[1][1] * red_chi_sq) + else: + tau_err = 'undetermined' + else: + a0_err = 'undetermined' + tau_err = 'undetermined' + + return [tau, tau_err], [a0, a0_err], res_sum + +def bootstrap_fit_aug_exp(plot_data, boot_samps=1000, seed=None): + """ + Bootstrap fit of [plots_data] to the aug_exp function. + """ + a0_list = [] + tau_list = [] + for i in range(boot_samps): + # generate sample of plot_data + samp_plot_data = gen_time_series_sample(plot_data) + # fit this data and add to lists + tau_fit, a0_fit, _ = perform_aug_exp_fit(samp_plot_data, seed) + a0_list.append(a0_fit[0]) + tau_list.append(tau_fit[0]) + + avg_a0 = np.mean(a0_list) + a0_std = 2*np.std(a0_list) + avg_tau = np.mean(tau_list) + tau_std = 2*np.std(tau_list) + + # obtain sum of residuals squared + t, fid, _ = plot_data + rsq_sum = np.sum((aug_exp_func([avg_a0, avg_tau], t) - fid)**2) + + return [avg_tau, tau_std], [avg_a0, a0_std], rsq_sum + + +############################################# +# Job Submission Utilties +############################################# +def get_max_tau(seq, max_T, backend, tau_min=0, tau_max=800, tol=1): + """ + Finds the maximum value of [tau] that a + sequence can have before it runs longer than + [max_T]. + """ + sched = IBMQDdSchedule(backend, 'c_basis') + # get mid point tau + tau_mid = int(np.floor((tau_max + tau_min) / 2)) + # time it + getattr(sched, f"add_{seq}")(0, 1, tau_mid) + T = sched.get_phys_time() / 1000 # get in micro-s + diff = max_T - T + if np.abs(diff) < tol: + return tau_mid + elif tau_mid == 1: + return tau_mid + elif T < max_T: + return get_max_tau(seq, max_T, backend, tau_mid, tau_max, tol=tol) + else: + return get_max_tau(seq, max_T, backend, tau_min, tau_mid, tol=tol) + + +def extract_T(experiment): + """ + Extract the T value for an experiment in nano seconds. + """ + split_label = experiment.name.split('_') + T_idx = split_label.index('T') + T_val = float(split_label[T_idx + 1][0:-2]) + + return T_val + +def chunk_experiments(experiments, chunk_size): + """ + Chunks list of [experiments] into lists of size [chunk_size]. + """ + num_lists = int(np.ceil(len(experiments) / chunk_size)) + + for j in range(num_lists): + yield experiments[chunk_size*j:chunk_size*(j+1)] + +def group_experiments_by_duration(experiments, num_bins): + """ + Bins experiments by DD sequence time. Chooses bins to + try and make all bins contain same number of elements. + """ + binned_experiments = [[] for _ in range(num_bins)] + # first, extract the times + times = [] + for exper in experiments: + T_val = extract_T(exper) + times.append(T_val) + + # bin the times (maintains order of data) + bin_info = pd.qcut(times, num_bins) + interval_list = bin_info.categories + + # iterate through the experiments and add them to correct bin + for (o_idx, bi) in enumerate(bin_info): + for (i_idx, interval) in enumerate(interval_list): + if bi.overlaps(interval): + binned_experiments[i_idx].append(experiments[o_idx]) + break + + return binned_experiments + +def gen_fds_on_fly_and_submit(backend, rep_dict, sched_settings, trials, + max_tries_on_fail=3, max_job_size=75): + """ + Generates fixed-delay schedules on the fly and then submits them. + """ + waittime = 1200 + # get data containers ready + back_name = backend.props['backend_name'] + result_list = [] + failed_experiments = [] + + # create "binned experiments list" which consists of list of list + # each sublist contains ~75 experiments to submit in one job + exps_to_run = [] + seqs = rep_dict.keys() + for _ in range(trials): + exps_to_run.extend(seqs) + + # unravel sched settings for schedule creation on the fly + offset, sym, delay, d_label, basis, encoding_qubit, dd_qubit = sched_settings + qubit = encoding_qubit + + # run first attempt of experiments + active_jobs = [] + for idx, seq in enumerate(exps_to_run): + print(f"job idx being tried: {idx} w/ seq {seq}") + job_attempted = False + while job_attempted is False: + # check if any queue slots are availible + avail_jobs = backend.backend.remaining_jobs_count() + print(f"avail jobs: {avail_jobs}") + if avail_jobs > 0: + job_attempted = True + try: + print(f"submitting job with idx: {idx} w/ seq {seq}") + # submit job with random ordering of individual circuits run + scheds_to_submit = [] + for reps in rep_dict[seq]: + desc, scheds = edde.pulse.pauli_pode_fid_decay_dd(offset, seq, sym, reps, delay, d_label, backend, basis, encoding_qubit, dd_qubit) + scheds_to_submit.extend(scheds) + error_mit_0 = IBMQDdSchedule(backend, basis, name='error_mitigate_0') + error_mit_0.add_error_mitigation_0(qubit) + scheds_to_submit.append(error_mit_0) + error_mit_1 = IBMQDdSchedule(backend, basis, name='error_mitigate_1') + error_mit_1.add_error_mitigation_1(qubit) + scheds_to_submit.append(error_mit_1) + job = backend.submit_job(scheds_to_submit, f"job_{idx}", num_shots=8192, shuffle=True) + # extract calibrated pules and add to active jobs list + x_pulse = str(error_mit_0.basis[f'X_{dd_qubit}']) + y_pulse = str(error_mit_0.basis[f'Y_{dd_qubit}']) + active_jobs.append((idx, job, x_pulse, y_pulse)) + except: + print(f"job with idx: {idx} failed on first try w/ seq {seq}") + failed_experiments.append((idx, seq)) + # otherwise, wait a few minutes and try again + else: + print(f"No availible queue slots for this account. Going to wait {waittime}s and try again.") + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # add info about pulses used and current pulses calibrated + sched = IBMQDdSchedule(backend, basis, name='get_pulses') + curr_xp = str(sched.basis[f'X_{dd_qubit}']) + curr_yp = str(sched.basis[f'Y_{dd_qubit}']) + with open(backend_fname + "properties.txt", 'a') as f: + f.write("\nPulse calibration information\n") + f.write("---\n") + f.write(f"used X: {a_job[2]}\n") + f.write(f"currently calibrated X: {curr_xp}\n") + f.write(f"used Y: {a_job[3]}\n") + f.write(f"currently calibrated Y: {curr_yp}\n") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + # run second attempt of experiments provided that they didn't succeed the first time + trial = 1 + while trial < max_tries_on_fail: + trial += 1 + if len(failed_experiments) > 0: + successful_idx = [] + for pos_idx, seq in enumerate(failed_experiments): + job_attempted = False + while job_attempted is False: + avail_jobs = backend.backend.remaining_jobs_count() + if avail_jobs > 0: + job_attempted = True + try: + print(f"submitting job with idx: {idx}") + # submit job with random ordering of individual circuits run + scheds_to_submit = [] + for reps in rep_dict[seq]: + desc, scheds = edde.pulse.pauli_pode_fid_decay_dd(offset, seq, sym, reps, delay, d_label, backend, basis, encoding_qubit, dd_qubit) + scheds_to_submit.extend(scheds) + error_mit_0 = IBMQDdSchedule(backend, basis, name='error_mitigate_0') + error_mit_0.add_error_mitigation_0(qubit) + scheds_to_submit.append(error_mit_0) + error_mit_1 = IBMQDdSchedule(backend, basis, name='error_mitigate_1') + error_mit_1.add_error_mitigation_1(qubit) + scheds_to_submit.append(error_mit_1) + job = backend.submit_job(scheds_to_submit, f"job_{idx}", num_shots=8192, shuffle=True) + # extract calibrated pules and add to active jobs list + x_pulse = str(error_mit_0.basis[f'X_{dd_qubit}']) + y_pulse = str(error_mit_0.basis[f'Y_{dd_qubit}']) + active_jobs.append((idx, job, x_pulse, y_pulse)) + successful_idx.append(idx) + except: + print(f"job with idx: {idx} failed on {pos_idx+1} try") + continue + # otherwise, wait a few minutes and try again + else: + print(f"No availible queue slots for this account. Going to wait {waittime}s and try again.") + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # add info about pulses used and current pulses calibrated + sched = IBMQDdSchedule(backend, basis, name='get_pulses') + curr_xp = str(sched.basis[f'X_{dd_qubit}']) + curr_yp = str(sched.basis[f'Y_{dd_qubit}']) + with open(backend_fname + "properties.txt", 'a') as f: + f.write("\nPulse calibration information\n") + f.write("---\n") + f.write(f"used X: {a_job[2]}\n") + f.write(f"currently calibrated X: {curr_xp}\n") + f.write(f"used Y: {a_job[3]}\n") + f.write(f"currently calibrated Y: {curr_yp}\n") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + # remove successful experiments (reverse order prevent in-place bugs) + for idx in sorted(successful_idx, reverse=True): + del failed_experiments[idx] + + # for remaining active jobs, wait on results to come in + count = 0 + while len(active_jobs) != 0: + print(f"All jobs submitted. Awaiting retrieval. (This message is sent every {waittime}s.)") + if count > 0: + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # add info about pulses used and current pulses calibrated + sched = IBMQDdSchedule(backend, basis, name='get_pulses') + curr_xp = str(sched.basis[f'X_{dd_qubit}']) + curr_yp = str(sched.basis[f'Y_{dd_qubit}']) + with open(backend_fname + "properties.txt", 'a') as f: + f.write("\nPulse calibration information\n") + f.write("---\n") + f.write(f"used X: {a_job[2]}\n") + f.write(f"currently calibrated X: {curr_xp}\n") + f.write(f"used Y: {a_job[3]}\n") + f.write(f"currently calibrated Y: {curr_yp}\n") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + count += 1 + + return result_list, failed_experiments + + +def gen_haar_fds_on_fly_and_submit(backend, rep_dict, seq_settings, + generic_settings, + trials, max_tries_on_fail=3, + max_job_size=75): + """ + Generates fixed-delay schedules on the fly and then submits them. + """ + waittime = 600 + # get data containers ready + back_name = backend.props['backend_name'] + result_list = [] + failed_experiments = [] + + # create "binned experiments list" which consists of list of list + # each sublist contains ~75 experiments to submit in one job + exps_to_run = [] + seqs = rep_dict.keys() + for _ in range(trials): + exps_to_run.extend(seqs) + + # unravel sched settings for schedule creation on the fly + N, d_label, encoding_qubits, dd_qubits = generic_settings + qubit = encoding_qubits + + # run first attempt of experiments + active_jobs = [] + for idx, seq in enumerate(exps_to_run): + print(f"job idx being tried: {idx} w/ seq {seq}") + job_attempted = False + while job_attempted is False: + # check if any queue slots are availible + avail_jobs = backend.backend.remaining_jobs_count() + print(f"avail jobs: {avail_jobs}") + if avail_jobs > 0: + job_attempted = True + try: + print(f"submitting job with idx: {idx} w/ seq {seq}") + # submit job with random ordering of individual circuits run + reps, pad = rep_dict[seq] + basis, sym, delay = seq_settings[seq] + desc, scheds = edde.pulse.haar_fid_decay_dd(N, seq, sym, reps, pad, delay, d_label, backend, basis, encoding_qubits, dd_qubits) + job = backend.submit_job(scheds, f"job_{idx}", num_shots='max', shuffle=True) + # extract calibrated pules and add to active jobs list + x_pulse = str(scheds[0].basis[f'X_{dd_qubits}']) + y_pulse = str(scheds[0].basis[f'Y_{dd_qubits}']) + active_jobs.append((idx, job, x_pulse, y_pulse)) + except: + print(f"job with idx: {idx} failed on first try w/ seq {seq}") + failed_experiments.append((idx, seq)) + # otherwise, wait a few minutes and try again + else: + print(f"No availible queue slots for this account. Going to wait {waittime}s and try again.") + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # add info about pulses used and current pulses calibrated + sched = IBMQDdSchedule(backend, basis, name='get_pulses') + curr_xp = str(sched.basis[f'X_{dd_qubits}']) + curr_yp = str(sched.basis[f'Y_{dd_qubits}']) + with open(backend_fname + "properties.txt", 'a') as f: + f.write("\nPulse calibration information\n") + f.write("---\n") + f.write(f"used X: {a_job[2]}\n") + f.write(f"currently calibrated X: {curr_xp}\n") + f.write(f"used Y: {a_job[3]}\n") + f.write(f"currently calibrated Y: {curr_yp}\n") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + # run second attempt of experiments provided that they didn't succeed the first time + trial = 1 + while trial < max_tries_on_fail: + trial += 1 + if len(failed_experiments) > 0: + successful_idx = [] + for pos_idx, seq in enumerate(failed_experiments): + job_attempted = False + while job_attempted is False: + avail_jobs = backend.backend.remaining_jobs_count() + if avail_jobs > 0: + job_attempted = True + try: + print(f"submitting job with idx: {idx}") + # submit job with random ordering of individual circuits run + reps, pad = rep_dict[seq] + basis, sym, delay = seq_settings[seq] + desc, scheds = edde.pulse.haar_fid_decay_dd(N, seq, sym, reps, pad, delay, d_label, backend, basis, encoding_qubits, dd_qubits) + job = backend.submit_job(scheds, f"job_{idx}", num_shots='max', shuffle=True) + # extract calibrated pules and add to active jobs list + x_pulse = str(scheds[0].basis[f'X_{dd_qubits}']) + y_pulse = str(scheds[0].basis[f'Y_{dd_qubits}']) + active_jobs.append((idx, job, x_pulse, y_pulse)) + successful_idx.append(idx) + except: + print(f"job with idx: {idx} failed on {pos_idx+1} try") + continue + # otherwise, wait a few minutes and try again + else: + print(f"No availible queue slots for this account. Going to wait {waittime}s and try again.") + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # add info about pulses used and current pulses calibrated + sched = IBMQDdSchedule(backend, basis, name='get_pulses') + curr_xp = str(sched.basis[f'X_{dd_qubits}']) + curr_yp = str(sched.basis[f'Y_{dd_qubits}']) + with open(backend_fname + "properties.txt", 'a') as f: + f.write("\nPulse calibration information\n") + f.write("---\n") + f.write(f"used X: {a_job[2]}\n") + f.write(f"currently calibrated X: {curr_xp}\n") + f.write(f"used Y: {a_job[3]}\n") + f.write(f"currently calibrated Y: {curr_yp}\n") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + # remove successful experiments (reverse order prevent in-place bugs) + for idx in sorted(successful_idx, reverse=True): + del failed_experiments[idx] + + # for remaining active jobs, wait on results to come in + count = 0 + while len(active_jobs) != 0: + print(f"All jobs submitted. Awaiting retrieval. (This message is sent every {waittime}s.)") + if count > 0: + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # add info about pulses used and current pulses calibrated + sched = IBMQDdSchedule(backend, basis, name='get_pulses') + curr_xp = str(sched.basis[f'X_{dd_qubits}']) + curr_yp = str(sched.basis[f'Y_{dd_qubits}']) + with open(backend_fname + "properties.txt", 'a') as f: + f.write("\nPulse calibration information\n") + f.write("---\n") + f.write(f"used X: {a_job[2]}\n") + f.write(f"currently calibrated X: {curr_xp}\n") + f.write(f"used Y: {a_job[3]}\n") + f.write(f"currently calibrated Y: {curr_yp}\n") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + count += 1 + + return result_list, failed_experiments + +def loop_binned_experiments(backend, binned_experiments, max_tries=3, max_job_size=75): + """ + Submits jobs that are binned together in same queue. At any given time, + can submit up to 5 jobs at once--but ensures that it's never the case + that a single job contains two different bin sets--even if this + isn't maximizing number of circuits per job. As a queue slot + frees up, automatically queues up new jobs. + """ + waittime = 600 + # get data containers ready + back_name = backend.props['backend_name'] + result_list = [] + failed_experiments = [] + + # randomly shuffle the bin order + #random.shuffle(binned_experiments) + + # reduce each job submission down to [max_job_size] circuits/schedules + exps_to_run = [] + for exp in binned_experiments: + chunked_exp = list(chunk_experiments(exp, max_job_size)) + exps_to_run.extend(chunked_exp) + + # run first attempt of experiments + active_jobs = [] + for idx, exp_group in enumerate(exps_to_run): + print(f"job idx being tried: {idx}") + job_attempted = False + while job_attempted is False: + # check if any queue slots are availible + avail_jobs = backend.backend.remaining_jobs_count() + print(f"avail jobs: {avail_jobs}") + if avail_jobs > 0: + job_attempted = True + try: + print(f"submitting job with idx: {idx}") + # submit job with random ordering of individual circuits run + job = backend.submit_job(exp_group, f"job_{idx}", num_shots=8000, shuffle=True) + active_jobs.append((idx, job)) + except: + print(f"job with idx: {idx} failed on first try") + failed_experiments.append((idx, exp)) + # otherwise, wait a few minutes and try again + else: + print(f"No availible queue slots for this account. Going to wait {waittime}s and try again.") + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + # run second attempt of experiments provided that they didn't succeed the first time + trial = 1 + while trial < max_tries: + trial += 1 + if len(failed_experiments) > 0: + successful_idx = [] + for pos_idx, exp_tup in enumerate(failed_experiments): + job_attempted = False + while job_attempted is False: + avail_jobs = backend.backend.remaining_jobs_count() + if avail_jobs > 0: + job_attempted = True + try: + print(f"submitting job with idx: {idx}") + # submit job with random ordering of individual circuits run + job = backend.submit_job(exp_group, f"job_{idx}", num_shots=8000, shuffle=True) + active_jobs.append((idx, job)) + successful_idx.append(idx) + except: + print(f"job with idx: {idx} failed on {pos_idx+1} try") + continue + # otherwise, wait a few minutes and try again + else: + print(f"No availible queue slots for this account. Going to wait {waittime}s and try again.") + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + # remove successful experiments (reverse order prevent in-place bugs) + for idx in sorted(successful_idx, reverse=True): + del failed_experiments[idx] + + # for remaining active jobs, wait on results to come in + count = 0 + while len(active_jobs) != 0: + print(f"All jobs submitted. Awaiting retrieval. (This message is sent every {waittime}s.)") + if count > 0: + time.sleep(waittime) + # check on status of jobs and extract/save results if applicable + idxs_to_remove = [] + for (a_idx, a_job) in enumerate(active_jobs): + if a_job[1].done() is True: + print(f"Data for job with idx {a_job[0]} retrieved and will be saved.") + # first, load in backend to extract most recent "data sheet" + backend.change_backend(back_name) + # save useful job info + now = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backend_fname = back_name + "_" + f"job_{a_job[0]}" + "_" + now + "_" + backend.save_backend_props(backend_fname + "properties.txt") + # save result + result = backend.backend.retrieve_job(a_job[1].job_id()).result() + result_list.append(result) + data = IBMQData(result) + data.save_raw_data(backend_fname + "raw_data") + idxs_to_remove.append(a_idx) + # remove the finished jobs from active_job list + for j in sorted(idxs_to_remove, reverse=True): + del active_jobs[j] + + count += 1 + + return result_list, failed_experiments + +def cancel_most_recent_job(ibmq_backend): + """ + Cancels the most recent job sent to [ibmq_backend]. + """ + ibmq_backend.backend.active_jobs()[0].cancel() + return + +def cancel_all_jobs(ibmq_backend): + """ + Cancels all jobs on [ibmq_backend]. + """ + while ibmq_backend.backend.remaining_jobs_count() < 5: + cancel_most_recent_job(ibmq_backend) + return +