From ca8d9a24b9ec298afb46ffea0af7e3e57e5f9ea7 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Fri, 5 Mar 2021 04:35:31 +0530 Subject: [PATCH 1/5] Fix release scripts (#3876) - Updates pytest version because existing version has a bug where --ignore= doesn't work. - Fixed verify-published-package script to install pip dev dependencies. - Updated setup.py to include `cirq.google.json_test_data` --- dev_tools/conf/pip-list-dev-tools.txt | 2 +- dev_tools/packaging/verify-published-package.sh | 5 ++--- setup.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev_tools/conf/pip-list-dev-tools.txt b/dev_tools/conf/pip-list-dev-tools.txt index 9c4edfce489..6e5c054e843 100644 --- a/dev_tools/conf/pip-list-dev-tools.txt +++ b/dev_tools/conf/pip-list-dev-tools.txt @@ -5,7 +5,7 @@ virtualenv black==20.8b1 mypy~=0.782.0 pylint~=2.6.0 -pytest~=5.4.1 +pytest~=6.2.2 pytest-asyncio~=0.12.0 pytest-cov~=2.5.0 filelock~=3.0.12 diff --git a/dev_tools/packaging/verify-published-package.sh b/dev_tools/packaging/verify-published-package.sh index 32d2a89abec..610c707819e 100755 --- a/dev_tools/packaging/verify-published-package.sh +++ b/dev_tools/packaging/verify-published-package.sh @@ -74,6 +74,7 @@ for PYTHON_VERSION in python3; do # Prepare. RUNTIME_DEPS_FILE="${REPO_ROOT}/requirements.txt" CONTRIB_DEPS_FILE="${REPO_ROOT}/cirq/contrib/contrib-requirements.txt" + DEV_DEPS_FILE="${REPO_ROOT}/dev_tools/conf/pip-list-dev-tools.txt" echo -e "\n\033[32m${PYTHON_VERSION}\033[0m" echo "Working in a fresh virtualenv at ${tmp_dir}/${PYTHON_VERSION}" @@ -82,7 +83,7 @@ for PYTHON_VERSION in python3; do # Install package. if [ "${PYPI_REPO_NAME}" == "TEST" ]; then echo "Pre-installing dependencies since they don't all exist in TEST pypi" - "${tmp_dir}/${PYTHON_VERSION}/bin/pip" install --quiet -r "${RUNTIME_DEPS_FILE}" + "${tmp_dir}/${PYTHON_VERSION}/bin/pip" install --quiet -r "${RUNTIME_DEPS_FILE}" -r "${DEV_DEPS_FILE}" fi echo Installing "${PYPI_PROJECT_NAME}==${PROJECT_VERSION} from ${PYPI_REPO_NAME} pypi" "${tmp_dir}/${PYTHON_VERSION}/bin/pip" install --quiet ${PIP_FLAGS} "${PYPI_PROJECT_NAME}==${PROJECT_VERSION}" @@ -93,8 +94,6 @@ for PYTHON_VERSION in python3; do "${tmp_dir}/${PYTHON_VERSION}/bin/python" -c "import cirq; print(cirq.Circuit(cirq.CZ(*cirq.LineQubit.range(2))))" # Run tests. - echo Installing pytest requirements - "${tmp_dir}/${PYTHON_VERSION}/bin/pip" install --quiet pytest pytest-asyncio PY_VER=$(ls "${tmp_dir}/${PYTHON_VERSION}/lib") echo Running cirq tests cirq_dir="${tmp_dir}/${PYTHON_VERSION}/lib/${PY_VER}/site-packages/${PROJECT_NAME}" diff --git a/setup.py b/setup.py index ef26e6ea428..ed124d59c77 100644 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ 'cirq': ['py.typed'], 'cirq.google.api.v1': ['*.proto', '*.pyi'], 'cirq.google.api.v2': ['*.proto', '*.pyi'], + 'cirq.google.json_test_data': ['*'], 'cirq.protocols.json_test_data': ['*'], }, ) From af6500e468ea804f7e95f0e323770161391510a3 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Thu, 4 Mar 2021 23:43:24 +0000 Subject: [PATCH 2/5] Add @mrwojtek to CODEOWNERS for cirq/docs/qcvv (#3877) --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4ab9b298f69..6b31d0ee080 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,4 +8,5 @@ **/pasqal/**/*.* @HGSilveri @quantumlib/cirq-maintainers @vtomole @cduck -cirq/experiments/**/*.* @mrwojtek @quantumlib/cirq-maintainers @vtomole @cduck \ No newline at end of file +cirq/experiments/**/*.* @mrwojtek @quantumlib/cirq-maintainers @vtomole @cduck +cirq/docs/qcvv/**/*.* @mrwojtek @quantumlib/cirq-maintainers @vtomole @cduck From 0b04d9d4e0e808332d7b32a34cb4ccfde8dcb932 Mon Sep 17 00:00:00 2001 From: Dave Bacon Date: Thu, 4 Mar 2021 16:46:28 -0800 Subject: [PATCH 3/5] Add IonQ getting started notebook (#3758) After #3755 Part of #3479 Like other service notebooks not tested due to auth. --- dev_tools/notebooks/isolated_notebook_test.py | 3 +- dev_tools/notebooks/notebook_test.py | 3 +- docs/_book.yaml | 5 + docs/tutorials/ionq/getting_started.ipynb | 367 ++++++++++++++++++ 4 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 docs/tutorials/ionq/getting_started.ipynb diff --git a/dev_tools/notebooks/isolated_notebook_test.py b/dev_tools/notebooks/isolated_notebook_test.py index d1f440f844f..0c68399b49e 100644 --- a/dev_tools/notebooks/isolated_notebook_test.py +++ b/dev_tools/notebooks/isolated_notebook_test.py @@ -44,9 +44,10 @@ # please always add a reason for skipping. SKIP_NOTEBOOKS = [ # skipping vendor notebooks as we don't have auth sorted out + "**/aqt/*.ipynb", "**/google/*.ipynb", + "**/ionq/*.ipynb", "**/pasqal/*.ipynb", - "**/aqt/*.ipynb", # skipping fidelity estimation due to # https://github.com/quantumlib/Cirq/issues/3502 "examples/*fidelity*", diff --git a/dev_tools/notebooks/notebook_test.py b/dev_tools/notebooks/notebook_test.py index 69d4e462a98..7dde1a33d45 100644 --- a/dev_tools/notebooks/notebook_test.py +++ b/dev_tools/notebooks/notebook_test.py @@ -31,9 +31,10 @@ SKIP_NOTEBOOKS = [ # skipping vendor notebooks as we don't have auth sorted out + "**/aqt/*.ipynb", + "**/ionq/*.ipynb", "**/google/*.ipynb", "**/pasqal/*.ipynb", - "**/aqt/*.ipynb", # skipping fidelity estimation due to # https://github.com/quantumlib/Cirq/issues/3502 "examples/*fidelity*", diff --git a/docs/_book.yaml b/docs/_book.yaml index 76a65a3a910..115c424696f 100644 --- a/docs/_book.yaml +++ b/docs/_book.yaml @@ -174,6 +174,11 @@ upper_tabs: - title: "Getting started with AQT hardware" path: /cirq/tutorials/aqt/getting_started + #### IONQ HARDWARE #### + - heading: "IonQ hardware" + - title: "Getting started with IonQ hardware" + path: /cirq/tutorials/ionq/getting_started + #### PASQAL HARDWARE #### - heading: "Pasqal hardware" - title: "Getting started with Pasqal hardware" diff --git a/docs/tutorials/ionq/getting_started.ipynb b/docs/tutorials/ionq/getting_started.ipynb new file mode 100644 index 00000000000..2d9d351356e --- /dev/null +++ b/docs/tutorials/ionq/getting_started.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "6996a2c78dba" + }, + "source": [ + "# Getting Start with IonQ and Cirq" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f4af1602b1e6" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " View on QuantumAI\n", + " \n", + " Run in Google Colab\n", + " \n", + " View source on GitHub\n", + " \n", + " Download notebook\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "34625186c8a4" + }, + "source": [ + "This notebook shows how to get up and running with the IonQ API. As of February 2021, this API is limited to partners only. More information about partnerships can be found at [ionq.com/get-started](https://ionq.com/get-started).\n", + "\n", + "To get started, first you must install Cirq. \n", + "\n", + "> NOTE: Currently this notebook requires the development version of Cirq 0.10 or higher." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "f5008c9c33b4" + }, + "outputs": [], + "source": [ + "try:\n", + " import cirq\n", + " version = cirq.__version__.split(\".\")\n", + " assert int(version[0]) > 0 or int(version[1]) > 9, \"Cirq version >0.9 required\"\n", + "except (ImportError, AssertionError):\n", + " print(\"Installing Cirq...\")\n", + " !pip install --quiet --pre cirq\n", + " print(\"Cirq installed.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1bdedbe68161" + }, + "source": [ + "Given that the above cell runs, you have installed Cirq and imported it. To simplify using the ionq api it is also suggested that you import ionq. Notice how nice it is that cirq and ionq are four letter words ending in \"q\"." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "22bc38124ede" + }, + "outputs": [], + "source": [ + "import cirq\n", + "import cirq.ionq as ionq" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "81d55416de71" + }, + "source": [ + "## Constructing an IonQ Service object" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "11b21e6193ae" + }, + "source": [ + "The main object that you use to access the IonQ API is an instance of the `cirq.ionq.Service` class. To construct this you need two pieces of information: the location of the remote host for accessing the API and also an API key. Both should be supplied to partners. Please be careful when using notebooks and version control to not save your API key in a public location!\n", + "\n", + "Given these bits of information you get a service object by simply running" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "800002580d0d" + }, + "outputs": [], + "source": [ + "REMOTE_HOST = 'https://example.com/' # Replace with the IonQ host.\n", + "API_KEY = 'tomyheart' # Replace with your IonQ API key\n", + "\n", + "service = ionq.Service(remote_host=REMOTE_HOST, \n", + " api_key=API_KEY, \n", + " default_target='simulator')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "042b4395ec4e" + }, + "source": [ + "Note that we have set the `default_target` to `simulator`. The other option would be to set it to `qpu`.\n", + "\n", + "## Running a simple circuit\n", + "\n", + "The IonQ API supports a limited set of gates natively. Circuit built with these gates do not need any modification and can be run directly against the API. For a list of the API supported gates see [circuit documentation](../../ionq/circuit.md). One supported gate is the square root of not gate, which we use here in conjunction with a controlled-not gate. The following cell will run the circuit below, blocking until the program has run and results have been returned:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "1ffa7a842d3c" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b=0100110001000110100000000110101111100110100001110110111010010011001011001100001001100110100011001111, 0100110001000110100000000110101111100110100001110110111010010011001011001100001001100110100011001111\n" + ] + } + ], + "source": [ + "q0, q1 = cirq.LineQubit.range(2)\n", + "circuit = cirq.Circuit(\n", + " cirq.X(q0)**0.5, # Square root of X\n", + " cirq.CX(q0, q1), # CNOT\n", + " cirq.measure(q0, q1, key='b') # Measure both qubits\n", + ")\n", + "result = service.run(circuit, repetitions=100)\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d5d62daa9d1e" + }, + "source": [ + "Because we did not specify a `target` and we ran this against a service with `default_target='simulator'`, this ran against the simulator. To run against the QPU simply add the target to the `run` method (note that this may take a while since the queue for the qpu is much longer than that for the simulator):" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "019fdd573787" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b=1101101100001000011111111011110001111011100011101001000110000001001001011100111001000110000110111110, 1101101100001000011111111011110001111011100011101001000110000001001001011100111001000110000110111110\n" + ] + } + ], + "source": [ + "result = service.run(circuit, repetitions=100, target='qpu')\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6f18ed2cc703" + }, + "source": [ + "## Jobs\n", + "\n", + "In the above section, the `run` method blocked on awaiting the program to run and return results. A different pattern is to asynchronously create jobs, which return an id that they can be used to identify the job, and fetch the results at a later time." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "id": "7353114a3b17" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job id: 34be7d9b-1a27-4974-b8ca-2b5941e52cd8\n" + ] + } + ], + "source": [ + "job = service.create_job(circuit, repetitions=100)\n", + "job_id = job.job_id()\n", + "print(f'Job id: {job_id}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "68dea73d380c" + }, + "source": [ + "Given the `job` object above, you can check on the status of the job" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "9b24b14182c9" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Status: completed\n" + ] + } + ], + "source": [ + "print(f'Status: {job.status()}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8ac6188f9053" + }, + "source": [ + "Or if you only have the job id, you can use this to get the job and create a new `cirq.ionq.Job` object to query." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "id": "a3b60c874b76" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Status: completed\n" + ] + } + ], + "source": [ + "same_job = service.get_job(job_id=job_id)\n", + "print(f'Status: {same_job.status()}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9c18cf2b3e62" + }, + "source": [ + "To get the results from the job, you can get the results of the job using the `results()` method. Note, however that this will block if the job is not completed, polling until the status is `completed`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "61ea7a4d3287" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "00: 0.5000000000000001\n", + "11: 0.4999999999999999\n" + ] + } + ], + "source": [ + "results = same_job.results()\n", + "print(results)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2583244eef7f" + }, + "source": [ + "Note that the results are not `cirq.Result`. To convert these to a `cirq.Result`, you can use `to_cirq_result()`" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "5051f3116d52" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b=0100001001111001110001111100101001010100010011100101011000111100110101100000110010110010001101111101, 0100001001111001110001111100101001010100010011100101011000111100110101100000110010110010001101111101\n" + ] + } + ], + "source": [ + "print(results.to_cirq_result())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "9cc9d9b62c2d" + }, + "source": [ + "## Next steps\n", + "\n", + "Check out the documentation on fully using the Cirq IonQ integration\n", + "\n", + "[Learn how to build circuits for the API](../../ionq/circuits.md)\n", + "\n", + "[How to use the service API](../../ionq/jobs.md)\n", + "\n", + "[Learn how to query the performace of a processor by accessing IonQ calibrations](../../ionq/calibrations.md)" + ] + } + ], + "metadata": { + "colab": { + "name": "getting_started.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From 11eff3f558bcc9234bbe3c7e18ebe7e761f30364 Mon Sep 17 00:00:00 2001 From: Matthew Harrigan Date: Fri, 5 Mar 2021 01:15:27 +0000 Subject: [PATCH 4/5] [XEB] Sundry practical improvements (#3871) - Shuffle order of task execution - Save data as you generate it - Move more functionality up the xxOptions Hierarchy - More robust fitting - Return std.dev. of fit parameters - Bugfix in notebook when running on device - Allow benchmarking the same pair in different layers and keep them distinct - Add note about characterizing the same pair in different layers: they will not be kept distinct. This can be relaxed, but requires changing the datatype of XEBCalibrationResult and keeping around (layer_i, pair_i) indices which are likely more troublesome to work with than actual qubit pairs. --- cirq/experiments/xeb_fitting.py | 85 +++++++++++++++++++-------- cirq/experiments/xeb_fitting_test.py | 16 ++++- cirq/experiments/xeb_sampling.py | 27 ++++++++- cirq/experiments/xeb_sampling_test.py | 33 ++++++++++- docs/qcvv/parallel_xeb.ipynb | 13 ++-- 5 files changed, 137 insertions(+), 37 deletions(-) diff --git a/cirq/experiments/xeb_fitting.py b/cirq/experiments/xeb_fitting.py index 6a1973c40be..ff69a6e2709 100644 --- a/cirq/experiments/xeb_fitting.py +++ b/cirq/experiments/xeb_fitting.py @@ -206,6 +206,14 @@ def get_initial_simplex_and_names( return initial_simplex, names + def get_parameterized_gate(self): + theta = THETA_SYMBOL if self.characterize_theta else self.theta_default + zeta = ZETA_SYMBOL if self.characterize_zeta else self.zeta_default + chi = CHI_SYMBOL if self.characterize_chi else self.chi_default + gamma = GAMMA_SYMBOL if self.characterize_gamma else self.gamma_default + phi = PHI_SYMBOL if self.characterize_phi else self.phi_default + return ops.PhasedFSimGate(theta=theta, zeta=zeta, chi=chi, gamma=gamma, phi=phi) + @dataclass(frozen=True) class SqrtISwapXEBOptions(XEBPhasedFSimCharacterizationOptions): @@ -221,14 +229,6 @@ class SqrtISwapXEBOptions(XEBPhasedFSimCharacterizationOptions): def should_parameterize(op: 'cirq.Operation') -> bool: return op.gate == SQRT_ISWAP - def get_parameterized_gate(self): - theta = THETA_SYMBOL if self.characterize_theta else self.theta_default - zeta = ZETA_SYMBOL if self.characterize_zeta else self.zeta_default - chi = CHI_SYMBOL if self.characterize_chi else self.chi_default - gamma = GAMMA_SYMBOL if self.characterize_gamma else self.gamma_default - phi = PHI_SYMBOL if self.characterize_phi else self.phi_default - return ops.PhasedFSimGate(theta=theta, zeta=zeta, chi=chi, gamma=gamma, phi=phi) - def parameterize_circuit( circuit: 'cirq.Circuit', @@ -378,6 +378,10 @@ def characterize_phased_fsim_parameters_with_xeb_by_pair( This is appropriate if you have run parallel XEB on multiple pairs of qubits. + The optimization is done per-pair. If you have the same pair in e.g. two different + layers the characterization optimization will lump the data together. This is in contrast + with the benchmarking functionality, which will always index on `(layer_i, pair_i, pair)`. + Args: sampled_df: The DataFrame of sampled two-qubit probability distributions returned from `sample_2q_xeb_circuits`. @@ -438,7 +442,9 @@ def exponential_decay(cycle_depths: np.ndarray, a: float, layer_fid: float) -> n return a * layer_fid ** cycle_depths -def _fit_exponential_decay(cycle_depths: np.ndarray, fidelities: np.ndarray) -> Tuple[float, float]: +def _fit_exponential_decay( + cycle_depths: np.ndarray, fidelities: np.ndarray +) -> Tuple[float, float, float, float]: """Fit an exponential model fidelity = a * layer_fid**x using nonlinear least squares. This uses `exponential_decay` as the function to fit with parameters `a` and `layer_fid`. @@ -453,22 +459,40 @@ def _fit_exponential_decay(cycle_depths: np.ndarray, fidelities: np.ndarray) -> a: The first fit parameter that scales the exponential function, perhaps accounting for state prep and measurement (SPAM) error. layer_fid: The second fit parameters which serves as the base of the exponential. + a_std: The standard deviation of the `a` parameter estimate. + layer_fid_std: The standard deviation of the `layer_fid` parameter estimate. """ cycle_depths = np.asarray(cycle_depths) fidelities = np.asarray(fidelities) - # Get initial guess by linear least squares with logarithm of model + # Get initial guess by linear least squares with logarithm of model. + # This only works for positive fidelities. We use numpy fancy indexing + # with `positives` (an ndarray of bools). positives = fidelities > 0 + if np.sum(positives) <= 1: + # The sum of the boolean array is the number of `True` entries. + # For one or fewer positive values, we cannot perform the linear fit. + return 0, 0, np.inf, np.inf cycle_depths_pos = cycle_depths[positives] log_fidelities = np.log(fidelities[positives]) slope, intercept, _, _, _ = scipy.stats.linregress(cycle_depths_pos, log_fidelities) layer_fid_0 = np.clip(np.exp(slope), 0, 1) a_0 = np.clip(np.exp(intercept), 0, 1) - (a, layer_fid), _ = scipy.optimize.curve_fit( - exponential_decay, cycle_depths, fidelities, p0=(a_0, layer_fid_0), bounds=((0, 0), (1, 1)) - ) - return a, layer_fid + try: + (a, layer_fid), pcov = scipy.optimize.curve_fit( + exponential_decay, + cycle_depths, + fidelities, + p0=(a_0, layer_fid_0), + bounds=((0, 0), (1, 1)), + ) + except ValueError: # coverage: ignore + # coverage: ignore + return 0, 0, np.inf, np.inf + + a_std, layer_fid_std = np.sqrt(np.diag(pcov)) + return a, layer_fid, a_std, layer_fid_std def _one_unique(df, name, default): @@ -494,21 +518,26 @@ def fit_exponential_decays(fidelities_df: pd.DataFrame) -> pd.DataFrame: for the fit parameters "a" and "layer_fid"; and nested "cycles_depths" and "fidelities" lists (now grouped by pair). """ - records = [] - for pair in fidelities_df['pair'].unique(): - f1 = fidelities_df[fidelities_df['pair'] == pair] - a, layer_fid = _fit_exponential_decay(f1['cycle_depth'], f1['fidelity']) + + def _per_pair(f1): + a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay( + f1['cycle_depth'], f1['fidelity'] + ) record = { - 'pair': pair, 'a': a, 'layer_fid': layer_fid, 'cycle_depths': f1['cycle_depth'].values, 'fidelities': f1['fidelity'].values, - 'layer_i': _one_unique(f1, 'layer_i', default=0), - 'pair_i': _one_unique(f1, 'pair_i', default=0), + 'a_std': a_std, + 'layer_fid_std': layer_fid_std, } - records.append(record) - return pd.DataFrame(records).set_index(['pair', 'layer_i', 'pair_i']) + return pd.Series(record) + + if 'layer_i' in fidelities_df.columns: + groupby = ['layer_i', 'pair_i', 'pair'] + else: + groupby = ['pair'] + return fidelities_df.groupby(groupby).apply(_per_pair) def before_and_after_characterization( @@ -531,13 +560,19 @@ def before_and_after_characterization( fit_decay_df_c = fit_exponential_decays(characterization_result.fidelities_df) joined_df = fit_decay_df_0.join(fit_decay_df_c, how='outer', lsuffix='_0', rsuffix='_c') + # Remove (layer_i, pair_i) from the index. While we keep this for `fit_exponential_decays` + # so the same pair can be benchmarked in different contexts, the multi-pair characterization + # function only keys on the pair identity. This can be seen acutely by the + # `characterization_result.final_params` dictionary being keyed only by the pair. + joined_df = joined_df.reset_index().set_index('pair') + joined_df['characterized_angles'] = [ - characterization_result.final_params[pair] for pair, _, _ in joined_df.index + characterization_result.final_params[pair] for pair in joined_df.index ] # Take any `final_params` (for any pair). We just need the angle names. fp, *_ = characterization_result.final_params.values() for angle_name in fp.keys(): joined_df[angle_name] = [ - characterization_result.final_params[pair][angle_name] for pair, _, _ in joined_df.index + characterization_result.final_params[pair][angle_name] for pair in joined_df.index ] return joined_df diff --git a/cirq/experiments/xeb_fitting_test.py b/cirq/experiments/xeb_fitting_test.py index ebd22bb08b4..30f7ad31d3f 100644 --- a/cirq/experiments/xeb_fitting_test.py +++ b/cirq/experiments/xeb_fitting_test.py @@ -300,5 +300,19 @@ def test_fit_exponential_decays(): rs = np.random.RandomState(999) cycle_depths = np.arange(3, 100, 11) fidelities = 0.95 * 0.98 ** cycle_depths + rs.normal(0, 0.2) - a, layer_fid = _fit_exponential_decay(cycle_depths, fidelities) + a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay(cycle_depths, fidelities) np.testing.assert_allclose([a, layer_fid], [0.95, 0.98], atol=0.02) + assert 0 < a_std < 0.2 / len(cycle_depths) + assert 0 < layer_fid_std < 1e-3 + + +def test_fit_exponential_decays_negative_fids(): + rs = np.random.RandomState(999) + cycle_depths = np.arange(3, 100, 11) + fidelities = 0.5 * 0.5 ** cycle_depths + rs.normal(0, 0.2) - 0.5 + assert np.sum(fidelities > 0) <= 1, 'they go negative' + a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay(cycle_depths, fidelities) + assert a == 0 + assert layer_fid == 0 + assert a_std == np.inf + assert layer_fid_std == np.inf diff --git a/cirq/experiments/xeb_sampling.py b/cirq/experiments/xeb_sampling.py index 6baaebb36cb..e45aa781ef7 100644 --- a/cirq/experiments/xeb_sampling.py +++ b/cirq/experiments/xeb_sampling.py @@ -13,6 +13,9 @@ # limitations under the License. """Estimation of fidelity associated with experimental circuit executions.""" import concurrent +import os +import time +import uuid from concurrent.futures.thread import ThreadPoolExecutor from dataclasses import dataclass from typing import ( @@ -32,7 +35,7 @@ import pandas as pd import tqdm -from cirq import ops, devices +from cirq import ops, devices, value, protocols from cirq.circuits import Circuit from cirq.experiments.random_quantum_circuit_generation import CircuitLibraryCombination @@ -78,6 +81,7 @@ def __init__( def __call__(self, tasks: List[_Sample2qXEBTask]) -> List[Dict[str, Any]]: prepared_circuits = [task.prepared_circuit for task in tasks] results = self.sampler.run_batch(prepared_circuits, repetitions=self.repetitions) + timestamp = time.time() assert len(results) == len(tasks) records = [] for task, nested_result in zip(tasks, results): @@ -93,6 +97,7 @@ def __call__(self, tasks: List[_Sample2qXEBTask]) -> List[Dict[str, Any]]: 'circuit_i': circuit_i, 'cycle_depth': task.cycle_depth, 'sampled_probs': sampled_probs, + 'timestamp': timestamp, # Additional metadata to track *how* this circuit # was zipped and executed. 'layer_i': task.layer_i, @@ -266,6 +271,7 @@ def _execute_sample_2q_xeb_tasks_in_batches( repetitions: int, batch_size: int, progress_bar: Callable[..., ContextManager], + dataset_directory: Optional[str] = None, ) -> List[Dict[str, Any]]: """Helper function used in `sample_2q_xeb_circuits` to batch and execute sampling tasks.""" n_tasks = len(tasks) @@ -278,9 +284,13 @@ def _execute_sample_2q_xeb_tasks_in_batches( futures = [pool.submit(run_batch, task_batch) for task_batch in batched_tasks] records = [] - with progress_bar(total=n_tasks) as progress: + with progress_bar(total=len(batched_tasks) * batch_size) as progress: for future in concurrent.futures.as_completed(futures): - records += future.result() + new_records = future.result() + if dataset_directory is not None: + os.makedirs(f'{dataset_directory}', exist_ok=True) + protocols.to_json(new_records, f'{dataset_directory}/xeb.{uuid.uuid4()}.json') + records.extend(new_records) progress.update(batch_size) return records @@ -294,6 +304,8 @@ def sample_2q_xeb_circuits( batch_size: int = 9, progress_bar: Optional[Callable[..., ContextManager]] = tqdm.tqdm, combinations_by_layer: Optional[List[CircuitLibraryCombination]] = None, + shuffle: Optional['cirq.RANDOM_STATE_OR_SEED_LIKE'] = None, + dataset_directory: Optional[str] = None, ): """Sample two-qubit XEB circuits given a sampler. @@ -314,6 +326,11 @@ def sample_2q_xeb_circuits( by `circuits` will be sampled verbatim, resulting in isolated XEB characterization. Otherwise, this contains all the random combinations and metadata required to combine the circuits in `circuits` into wide, parallel-XEB-style circuits for execution. + shuffle: If provided, use this random state or seed to shuffle the order in which tasks + are executed. + dataset_directory: If provided, save each batch of sampled results to a file + `{dataset_directory}/xeb.{uuid4()}.json` where uuid4() is a random string. This can be + used to incrementally save results to be analyzed later. Returns: A pandas dataframe with index given by ['circuit_i', 'cycle_depth']. @@ -338,6 +355,9 @@ def sample_2q_xeb_circuits( # Construct truncated-with-measurement circuits to run. tasks = _generate_sample_2q_xeb_tasks(zipped_circuits, cycle_depths) + if shuffle is not None: + shuffle = value.parse_random_state(shuffle) + shuffle.shuffle(tasks) # Batch and run tasks. records = _execute_sample_2q_xeb_tasks_in_batches( @@ -347,6 +367,7 @@ def sample_2q_xeb_circuits( repetitions=repetitions, batch_size=batch_size, progress_bar=progress_bar, + dataset_directory=dataset_directory, ) # Set up the dataframe. diff --git a/cirq/experiments/xeb_sampling_test.py b/cirq/experiments/xeb_sampling_test.py index 3d567d93091..6f76e940305 100644 --- a/cirq/experiments/xeb_sampling_test.py +++ b/cirq/experiments/xeb_sampling_test.py @@ -11,11 +11,13 @@ # 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 glob import itertools from typing import Iterable import networkx as nx import numpy as np +import pandas as pd import pytest import cirq @@ -43,6 +45,7 @@ def test_sample_2q_xeb_circuits(): sampler=cirq.Simulator(), circuits=circuits, cycle_depths=cycle_depths, + shuffle=np.random.RandomState(10), ) assert len(df) == len(cycle_depths) * len(circuits) for (circuit_i, cycle_depth), row in df.iterrows(): @@ -89,11 +92,24 @@ def _manhattan_distance(qubit1: cirq.GridQubit, qubit2: cirq.GridQubit) -> int: ) -def test_sample_2q_parallel_xeb_circuits(): +def _assert_frame_approx_equal(df, df2, *, atol): + assert len(df) == len(df2) + for (i1, row1), (i2, row2) in zip(df.sort_index().iterrows(), df2.sort_index().iterrows()): + assert i1 == i2 + for k in set(row1.keys()) | set(row2.keys()): + v1 = row1[k] + v2 = row2[k] + if isinstance(v1, (float, np.ndarray)): + np.testing.assert_allclose(v1, v2, atol=atol) + else: + assert v1 == v2, k + + +def test_sample_2q_parallel_xeb_circuits(tmpdir): circuits = rqcg.generate_library_of_2q_circuits( n_library_circuits=5, two_qubit_gate=cirq.ISWAP ** 0.5, max_cycle_depth=10 ) - cycle_depths = [10] + cycle_depths = [5, 10] graph = _gridqubits_to_graph_device(cirq.GridQubit.rect(3, 2)) combs = rqcg.get_random_combinations_for_device( n_library_circuits=len(circuits), @@ -107,7 +123,9 @@ def test_sample_2q_parallel_xeb_circuits(): circuits=circuits, cycle_depths=cycle_depths, combinations_by_layer=combs, + dataset_directory=f'{tmpdir}/my_dataset', ) + n_pairs = sum(len(c.pairs) for c in combs) assert len(df) == len(cycle_depths) * len(circuits) * n_pairs for (circuit_i, cycle_depth), row in df.iterrows(): @@ -119,6 +137,17 @@ def test_sample_2q_parallel_xeb_circuits(): assert 0 <= row['pair_i'] < 2 # in 3x2 graph, there's a max of 2 pairs per layer assert len(df['pair'].unique()) == 7 # seven pairs in 3x2 graph + # Test loading from dataset + chunks = [record for fn in glob.glob(f'{tmpdir}/my_dataset/*') for record in cirq.read_json(fn)] + df2 = pd.DataFrame(chunks).set_index(['circuit_i', 'cycle_depth']) + df2['pair'] = [tuple(row['pair']) for _, row in df2.iterrows()] + actual_index_names = ['layer_i', 'pair_i', 'combination_i', 'cycle_depth'] + _assert_frame_approx_equal( + df.reset_index().set_index(actual_index_names), + df2.reset_index().set_index(actual_index_names), + atol=1e-5, + ) + def test_sample_2q_parallel_xeb_circuits_bad_circuit_library(): circuits = rqcg.generate_library_of_2q_circuits( diff --git a/docs/qcvv/parallel_xeb.ipynb b/docs/qcvv/parallel_xeb.ipynb index 773ff599235..99b08431ef1 100644 --- a/docs/qcvv/parallel_xeb.ipynb +++ b/docs/qcvv/parallel_xeb.ipynb @@ -113,7 +113,7 @@ "from cirq.experiments import random_quantum_circuit_generation as rqcg\n", "\n", "circuit_library = rqcg.generate_library_of_2q_circuits(\n", - " n_library_circuits=10, \n", + " n_library_circuits=20, \n", " two_qubit_gate=cirq.ISWAP**0.5,\n", " random_state=52,\n", ")\n", @@ -167,8 +167,8 @@ "else:\n", " import cirq.google as cg\n", " sampler = cg.get_engine_sampler(device_name, gate_set_name='sqrt_iswap')\n", - " device = cg.get_engine_device(device)\n", - " \n", + " device = cg.get_engine_device(device_name)\n", + " qubits = sorted(device.qubits)\n", " graph = ccr.xmon_device_to_graph(device)\n", "\n", "\n", @@ -198,7 +198,7 @@ "source": [ "combs_by_layer = rqcg.get_random_combinations_for_device(\n", " n_library_circuits=len(circuit_library),\n", - " n_combinations=9,\n", + " n_combinations=10,\n", " device_graph=graph,\n", " random_state=53,\n", ")\n", @@ -260,7 +260,8 @@ " sampler=sampler,\n", " circuits=circuit_library,\n", " cycle_depths=cycle_depths,\n", - " combinations_by_layer=combs_by_layer\n", + " combinations_by_layer=combs_by_layer,\n", + " shuffle=np.random.RandomState(52),\n", ")\n", "sampled_df" ] @@ -406,7 +407,7 @@ " options,\n", " pool=pool,\n", " # ease tolerance so it converges faster:\n", - " fatol=5e-3, xatol=5e-3\n", + " fatol=1e-2, xatol=1e-2\n", ")" ] }, From b66bb9f4abbf0780662c282c9542fa27045fddc0 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Fri, 5 Mar 2021 21:18:59 +0530 Subject: [PATCH 5/5] Minor improvements in calibration heatmap api (#3879) - Adds a default title in calibration heatmaps. - Return self from update_config to support chaining of operations. Eg: heatmap.update_config(title='a').plot() --- cirq/google/engine/calibration.py | 4 ++-- cirq/google/engine/calibration_test.py | 10 ++++++---- cirq/vis/heatmap.py | 3 ++- cirq/vis/heatmap_test.py | 5 ++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cirq/google/engine/calibration.py b/cirq/google/engine/calibration.py index f6256caa207..26d253ef131 100644 --- a/cirq/google/engine/calibration.py +++ b/cirq/google/engine/calibration.py @@ -245,9 +245,9 @@ def heatmap(self, key: str) -> vis.Heatmap: ) value_map = {self.key_to_qubits(k): self.value_to_float(v) for k, v in metrics.items()} if all(len(k) == 1 for k in value_map.keys()): - return vis.Heatmap(value_map) + return vis.Heatmap(value_map, title=key.replace('_', ' ').title()) elif all(len(k) == 2 for k in value_map.keys()): - return vis.TwoQubitInteractionHeatmap(value_map) + return vis.TwoQubitInteractionHeatmap(value_map, title=key.replace('_', ' ').title()) raise ValueError( 'Heatmaps are only supported if all the targets in a metric are one or two qubits.' + f'{key} has target qubits {value_map.keys()}' diff --git a/cirq/google/engine/calibration_test.py b/cirq/google/engine/calibration_test.py index b52668d5328..73042e2304c 100644 --- a/cirq/google/engine/calibration_test.py +++ b/cirq/google/engine/calibration_test.py @@ -26,13 +26,13 @@ """ timestamp_ms: 1562544000021, metrics: [{ - name: 'xeb', + name: 'two_qubit_xeb', targets: ['0_0', '0_1'], values: [{ double_val: .9999 }] }, { - name: 'xeb', + name: 'two_qubit_xeb', targets: ['0_0', '1_0'], values: [{ double_val: .9998 @@ -92,7 +92,7 @@ def test_calibration_metrics_dictionary(): def test_calibration_str(): calibration = cg.Calibration(_CALIBRATION_DATA) - assert str(calibration) == "Calibration(keys=['globalMetric', 't1', 'xeb'])" + assert str(calibration) == "Calibration(keys=['globalMetric', 't1', 'two_qubit_xeb'])" def test_calibration_repr(): @@ -163,11 +163,13 @@ def test_calibration_heatmap(): figure = mpl.figure.Figure() axes = figure.add_subplot(111) heatmap.plot(axes) + assert axes.get_title() == 'T1' - heatmap = calibration.heatmap('xeb') + heatmap = calibration.heatmap('two_qubit_xeb') figure = mpl.figure.Figure() axes = figure.add_subplot(999) heatmap.plot(axes) + assert axes.get_title() == 'Two Qubit Xeb' with pytest.raises(ValueError, match="one or two qubits.*multi_qubit"): multi_qubit_data = Merge( diff --git a/cirq/vis/heatmap.py b/cirq/vis/heatmap.py index 57704f74032..ac9eaa9122b 100644 --- a/cirq/vis/heatmap.py +++ b/cirq/vis/heatmap.py @@ -151,10 +151,11 @@ def _validate_kwargs(self, kwargs) -> None: invalid_args = ", ".join([k for k in kwargs if k not in valid_kwargs]) raise ValueError(f"Received invalid argument(s): {invalid_args}") - def update_config(self, **kwargs) -> None: + def update_config(self, **kwargs) -> 'Heatmap': """Add/Modify **kwargs args passed during initialisation.""" self._validate_kwargs(kwargs) self._config.update(kwargs) + return self def _qubits_to_polygon(self, qubits: QubitTuple) -> Tuple[Polygon, Point]: qubit = qubits[0] diff --git a/cirq/vis/heatmap_test.py b/cirq/vis/heatmap_test.py index 29e78e0fe81..3b7c8b073a5 100644 --- a/cirq/vis/heatmap_test.py +++ b/cirq/vis/heatmap_test.py @@ -262,11 +262,10 @@ def test_colorbar(ax, position, size, pad): random_heatmap = heatmap.Heatmap(test_value_map, plot_colorbar=False) fig1, ax1 = plt.subplots() random_heatmap.plot(ax1) + fig2, ax2 = plt.subplots() random_heatmap.update_config( plot_colorbar=True, colorbar_position=position, colorbar_size=size, colorbar_pad=pad - ) - fig2, ax2 = plt.subplots() - random_heatmap.plot(ax2) + ).plot(ax2) # We need to call savefig() explicitly for updating axes position since the figure # object has been altered in the HeatMap._plot_colorbar function.