diff --git a/examples/creating_a_dds.ipynb b/examples/creating_a_dds.ipynb index e1e738ce..0f2d9b75 100644 --- a/examples/creating_a_dds.ipynb +++ b/examples/creating_a_dds.ipynb @@ -39,16 +39,16 @@ "\n", "Q-CTRL Open Controls can create DDSs according to the following protocols:\n", "\n", - "1. Ramsey\n", - "2. Spin Echo (SE)\n", - "3. Carr-Purcell (CP)\n", - "4. Carr-Purcell-Meiboom-Gill (CPMG)\n", - "5. Uhrig\n", - "6. Periodic\n", - "7. Walsh Single-axis\n", - "8. Quadratic\n", - "9. X-concatenated\n", - "10. XY-concatenated\n", + "1. `Ramsey`\n", + "2. `spin echo`\n", + "3. `Carr-Purcell`\n", + "4. `Carr-Purcell-Meiboom-Gill`\n", + "5. `Uhrig`\n", + "6. `periodic`\n", + "7. `Walsh single-axis`\n", + "8. `quadratic`\n", + "9. `X concatenated`\n", + "10. `XY concatenated`\n", "\n", "See the [technical documentation](https://docs.q-ctrl.com/control-formats#dynamical-decoupling-sequences) for details." ] @@ -253,14 +253,12 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -278,19 +276,19 @@ "rabi_plot_axis.plot(times, rabi_rotations)\n", "rabi_plot_axis.ticklabel_format(style='sci', axis='x', scilimits=(0, 2))\n", "rabi_plot_axis.set_xlim([0, max(times)])\n", - "rabi_plot_axis.set_xlabel('Time (sec)')\n", + "rabi_plot_axis.set_xlabel('Time (s)')\n", "rabi_plot_axis.set_ylabel('Rabi Rotations (rad)')\n", "\n", "azimuth_plot_axis.plot(times, azimuthal_angles)\n", "azimuth_plot_axis.ticklabel_format(style='sci', axis='x', scilimits=(0, 2))\n", "azimuth_plot_axis.set_xlim([0, max(times)])\n", - "azimuth_plot_axis.set_xlabel('Time (sec)')\n", + "azimuth_plot_axis.set_xlabel('Time (s)')\n", "azimuth_plot_axis.set_ylabel('Azimuthal Angle (rad)')\n", "\n", "detuning_plot_axis.plot(times, detuning_rotations)\n", "detuning_plot_axis.ticklabel_format(style='sci', axis='x', scilimits=(0, 2))\n", "detuning_plot_axis.set_xlim([0, max(times)])\n", - "detuning_plot_axis.set_xlabel('Time (sec)')\n", + "detuning_plot_axis.set_xlabel('Time (s)')\n", "detuning_plot_axis.set_ylabel('Detuning Rotation (rad)')\n" ] }, @@ -401,7 +399,7 @@ "source": [ "## Custom Definition of Dynamic Decoupling Sequence\n", "\n", - "`DynamicDecouplingSequence`, defined in Q-CTRL Open Controls, accepts `duration`, `rabi_rotations`, `azimuthal_angles` and `detuning_rotations` as parameters to define any arbitrary DDS. This is the most generalized way to create any custom DDS." + "An arbitrary `DynamicDecouplingSequence` can be created by providing a `duration` along with arrays for the `rabi_rotations`, `azimuthal_angles`, `detuning_rotations` and offsets." ] }, { @@ -426,14 +424,11 @@ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], "source": [ - "# defining the required parameters\n", "_duration = 1.50\n", "_rabi_rotations = [0., np.pi, np.pi, 0., np.pi, np.pi, 0.]\n", "_azimuthal_angles = [0., np.pi/2, 0., 0., 0., np.pi/2, 0.]\n", @@ -441,24 +436,20 @@ "_offsets = [0., 0.25, 0.50, 0.75, 1.00, 1.25, 1.50]\n", "_name = 'Custom DDS'\n", "\n", - "custom_dds = DynamicDecouplingSequence(duration=_duration,\\\n", - " rabi_rotations=_rabi_rotations,\\\n", + "custom_dds = DynamicDecouplingSequence(duration=_duration,\n", + " rabi_rotations=_rabi_rotations,\n", " azimuthal_angles=_azimuthal_angles,\n", " detuning_rotations=_detuning_rotations,\n", " offsets=_offsets,\n", " name=_name)\n", "\n", - "## this specific pulse, as defined above consists of (Y_pi, X_pi, Z_pi, X_pi, Y_pi) pulses\n", - "## at the offsets (0.25, 0.50, 0.75, 1.00, 1.25)\n", "## let us plot and verify\n", - "\n", "formatted_plot_data = custom_dds.get_plot_formatted_arrays()\n", "rabi_rotations, azimuthal_angles, detuning_rotations, times = (formatted_plot_data['rabi_rotations'],\n", " formatted_plot_data['azimuthal_angles'],\n", " formatted_plot_data['detuning_rotations'],\n", " formatted_plot_data['times'])\n", "\n", - "# prepare the axes\n", "figure, (rabi_plot_axis, azimuth_plot_axis, detuning_plot_axis) = plt.subplots(\n", " 1, 3, figsize=(20,5))\n", "\n", diff --git a/examples/creating_a_driven_control.ipynb b/examples/creating_a_driven_control.ipynb new file mode 100644 index 00000000..66df69f1 --- /dev/null +++ b/examples/creating_a_driven_control.ipynb @@ -0,0 +1,427 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating a Driven Control\n", + "\n", + "This notebook illustrates how to use Q-CTRL Open Controls to create a driven control.\n", + "\n", + "A driven control represents a continuous drive on a the transition of a qubit with a tunable detuning. The Open Controls package allows you to generate driven controls that enact dynamically corrected gates (DCG). These dynamically corrected gates are able to achieve an arbitrary rotation of the bloch sphere (around any point on the equator), in a manner that is robust to dephasing and/or control noise. DCGs can be used as drop-in gate replacements to suppress errors in a quantum computation.\n", + "\n", + "Q-CTRL Open Controls can be used to create a driven control from a library of well-known control schemes. Once created, it can be printed, plotted, exported in CSV or JSON format for use on a quantum computer or any of [Q-CTRL's products](https://q-ctrl.com/products/)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from qctrlopencontrols import new_predefined_driven_control, DrivenControl" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Predefined Driven Control Schemes\n", + "\n", + "Q-CTRL Open Controls can create driven controls according to the following dynamically corrected gate protocols:\n", + "\n", + "1. `primitive`\n", + "2. `BB1`\n", + "3. `SK1`\n", + "4. `CORPSE`\n", + "5. `WAMF1`\n", + "6. `SCROFULOUS`\n", + "7. `COPRSE in BB1`\n", + "8. `CORPSE in SK1`\n", + "9. `CORPSE in SCROFULOUS`\n", + "\n", + "See the [technical documentation](https://docs.q-ctrl.com/control-library) for details." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating and Printing a Driven Control\n", + "\n", + "A driven control is made of a continuous drive on the qubits transition with a tunable detuning. The continuous drive is described by a piecewise constant function made of a set of segments. Each drive segment has a `rabi_rate` applied at a `azimuthal_angle` for a `duration`, with a `detuning`. The mathematical definition of a driven control is explained in the [technical documentation](http://docs.q-ctrl.com/control-library#dynamical-decoupling-sequences).\n", + "\n", + "Q-CTRL Open controls can generate a driven control from a library of dynamically corrected gate schemes, mathematically defined in the [technical documentation](https://docs.q-ctrl.com/control-formats#dynamical-decoupling-sequences). All dynamically corrected gates are derived from three quantities: \n", + "\n", + "* `maximum_rabi_rate` the maximum achievable rabi rate.\n", + "* `rabi_rotation` the total rotation of the bloch sphere.\n", + "* `azimuthal_angle` the angle to the center point of the rotation on the equator. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Primitive X-pi:\n", + "Rabi Rates = [1.0] x 6.283185307179586\n", + "Azimuthal Angles = [0.0] x pi\n", + "Detunings = [0] x 0.0\n", + "Durations = [1.0] x 0.5\n" + ] + } + ], + "source": [ + "## Primitive Pi pulse in X\n", + "prim = new_predefined_driven_control(\n", + " rabi_rotation=np.pi,\n", + " azimuthal_angle=0,\n", + " maximum_rabi_rate=2 * np.pi,\n", + " scheme='primitive',\n", + " name='Primitive X-pi'\n", + ")\n", + "print(prim)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BB1 X-pi:\n", + "Rabi Rates = [1.0,1.0,1.0,1.0] x 6.283185307179586\n", + "Azimuthal Angles = [0.0,0.5804306232551663,1.7412918697654987,0.5804306232551663] x pi\n", + "Detunings = [0,0,0,0] x 0.0\n", + "Durations = [0.2,0.2,0.4,0.2] x 2.5\n" + ] + } + ], + "source": [ + "## BB1 Pi pulse in X (implements the same effective operation as above)\n", + "bb1_x = new_predefined_driven_control(\n", + " rabi_rotation=np.pi,\n", + " azimuthal_angle=0,\n", + " maximum_rabi_rate=2 * np.pi,\n", + " scheme='BB1',\n", + " name='BB1 X-pi'\n", + ")\n", + "print(bb1_x)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BB1 Y-pi/2:\n", + "Rabi Rates = [1.0,1.0,1.0,1.0] x 6.283185307179586\n", + "Azimuthal Angles = [0.5,1.0398930876747683,2.1196792630243046,1.0398930876747683] x pi\n", + "Detunings = [0,0,0,0] x 0.0\n", + "Durations = [0.1111111111111111,0.2222222222222222,0.4444444444444444,0.2222222222222222] x 2.25\n" + ] + } + ], + "source": [ + "## BB1 Pi/2 pulse in Y\n", + "bb1_y = new_predefined_driven_control(\n", + " rabi_rotation=np.pi/2,\n", + " azimuthal_angle=np.pi/2,\n", + " maximum_rabi_rate=2 * np.pi,\n", + " scheme='BB1',\n", + " name='BB1 Y-pi/2'\n", + ")\n", + "print(bb1_y)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SK1 Y-pi/2:\n", + "Rabi Rates = [1.0,1.0,1.0] x 6.283185307179586\n", + "Azimuthal Angles = [0.5,-0.03989308767476825,1.0398930876747683] x pi\n", + "Detunings = [0,0,0] x 0.0\n", + "Durations = [0.1111111111111111,0.4444444444444444,0.4444444444444444] x 2.25\n" + ] + } + ], + "source": [ + "## SK1 Pi/2 pulse in Y\n", + "sk1 = new_predefined_driven_control(\n", + " rabi_rotation=np.pi/2,\n", + " azimuthal_angle=np.pi/2,\n", + " maximum_rabi_rate=2 * np.pi,\n", + " scheme='SK1',\n", + " name='SK1 Y-pi/2'\n", + ")\n", + "print(sk1)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CORPSE X-pi/2:\n", + "Rabi Rates = [1.0,1.0,1.0] x 6.283185307179586\n", + "Azimuthal Angles = [0.0,1.0,0.0] x pi\n", + "Detunings = [0,0,0] x 0.0\n", + "Durations = [0.5284727158825664,0.43811717424831853,0.033410109869114954] x 2.0199465438373845\n" + ] + } + ], + "source": [ + "## CORPSE Pi/2 pulse in X\n", + "corpse = new_predefined_driven_control(\n", + " rabi_rotation=np.pi/2,\n", + " azimuthal_angle=0,\n", + " maximum_rabi_rate=2 * np.pi,\n", + " scheme='CORPSE',\n", + " name='CORPSE X-pi/2'\n", + ")\n", + "print(corpse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plotting a driven control\n", + "\n", + "Once created, Q-CTRL Open Controls provides the method `get_plot_formatted_arrays` to create a set of formatted arrays ready to be immediately plotted with Matplotlib. We use the `BB1` as a driven control to generate plots of the `rabi_rates`, `azimuthal_angles` and `detunings`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "formatted_plot_data = bb1_x.get_plot_formatted_arrays(coordinates='cylindrical')\n", + "rabi_rates, azimuthal_angles, detunings, times = (formatted_plot_data['rabi_rates'],\n", + " formatted_plot_data['azimuthal_angles'],\n", + " formatted_plot_data['detunings'],\n", + " formatted_plot_data['times'])\n", + "# prepare the axes\n", + "figure, (x_axis, y_axis, z_axis) = plt.subplots(1, 3, figsize=(20,5))\n", + "\n", + "x_axis.fill_between(times, rabi_rates, 0, alpha=0.15, color='#680cea')\n", + "x_axis.plot(times, rabi_rates, color='#680cea')\n", + "x_axis.set_xlabel('Time (s)')\n", + "x_axis.set_ylabel('Rabi Rate (radHz)')\n", + "\n", + "y_axis.fill_between(times, azimuthal_angles, 0, alpha=0.15, color='#680cea')\n", + "y_axis.plot(times, azimuthal_angles, color='#680cea')\n", + "y_axis.set_xlabel('Time (s)')\n", + "y_axis.set_ylabel('Azimuthal Angle (rad)')\n", + "\n", + "z_axis.fill_between(times, detunings, 0, alpha=0.15, color='#680cea')\n", + "z_axis.plot(times, detunings, color='#680cea')\n", + "z_axis.set_xlabel('Time (s)')\n", + "z_axis.set_ylabel('Detuning (radHz)')\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exporting the Driven Control\n", + "\n", + "Q-CTRL Open Controls enables exporting driven controls in CSV or JSON format. An exported driven control is [formatted](https://docs.q-ctrl.com/control-formats) to be compatible with [Q-CTRL BLACK OPAL](https://app.q-ctrl.com).\n", + "\n", + "Q-CTRL Open Controls can export a driven control in either `cartesian` or `cylindrical` coordinates. For details, consult the [technical documentation](https://docs.q-ctrl.com/output-data-formats#q-ctrl-hardware).\n", + "\n", + "In the example below, we chose the `bb1_x` control (created above) for exporting to a CSV file." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "file_type='CSV'\n", + "filename='example_driven_control.csv'\n", + "\n", + "bb1_x.export_to_file(\n", + " filename=filename, \n", + " file_type=file_type,\n", + " coordinates='cartesian')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "amplitude_x,amplitude_y,detuning,duration,maximum_rabi_rate\n", + "\n", + "6.283185307179586,0.0,0.0,0.5,6.283185307179586\n", + "\n", + "-1.570796326794896,6.083668013960418,0.0,0.5,6.283185307179586\n", + "\n", + "4.319689898685962,-4.562751010470316,0.0,1.0,6.283185307179586\n", + "\n", + "-1.570796326794896,6.083668013960418,0.0,0.5,6.283185307179586\n" + ] + } + ], + "source": [ + "## Reload the file and check its content to better understand the format\n", + "with open(filename, 'rt') as handle:\n", + " file_content = handle.readlines()\n", + "for line in file_content:\n", + " print(line)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Opening the Exported Sequence in Q-CTRL BLACK OPAL" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The exported CSV files are compatible for analysis by a suite of tools offered by [Q-CTRL BLACK OPAL](https://app.q-ctrl.com). For example, you can upload the exported file in the [1-QUBIT Workspace](https://app.q-ctrl.com/oneQubit) for further analysis. The process to upload a custom control is described in [Uploading and Evaluating Custom Controls](https://help.q-ctrl.com/black-opal/guides/uploading-and-evaluating-custom-controls). For a full capability of BLACK OPAL, consult [Q-CTRL Help](https://help.q-ctrl.com/black-opal)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom Definition of a Driven Control\n", + "\n", + "An arbitrary `DrivenControl` can be defined defined using arrays of `rabi_rotations`, `azimuthal_angles`, `detuning_rotations` and `durations`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABJkAAAFACAYAAAAfw61rAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzs3Xu0XXV97/33JwEEFUSbIJcQQZragwrK2AZUtFCrBY5K\ntaLgrRU0xUqP1aet1J56O+cZwz5Wn6qgMVoE2yrVo2DUKIoPFdRaExC506YUJSk2EblfDXyfP9YM\nLDZ776ydtdeaa+/9fo2xxp63NeeHyU5+Wd/1m79fqgpJkiRJkiSpHwvaDiBJkiRJkqTZzyKTJEmS\nJEmS+maRSZIkSZIkSX2zyCRJkiRJkqS+WWSSJEmSJElS3ywySZIkSZIkqW8WmSRJkiRJktQ3i0yS\nJEmSJEnqm0UmSZIkSZIk9W2HtgPMpEWLFtV+++3XdgxJGjkXX3zxz6tqcds52mQbIUmTs52wnZCk\nqfTaTsypItN+++3HunXr2o4hSSMnyU/aztA22whJmpzthO2EJE2l13bCx+UkSZIkSZLUN4tMkiRJ\nkiRJ6ptFJkmSJEmSJPXNIpMkSZIkSZL6ZpFJkiRJkiRJfbPIJEmSJEmSpL5ZZJIkSZIkSVLfLDJJ\nkkZOkjOSbEpyxST7k+QjSdYnuSzJIcPOKEmSJOnhLDJJkkbRmcBRU+w/GljWvFYAHx9CJkmSJElT\n2KHtAJI0ai46427WfuHetmM8wrOOexTPO3GXtmMMRVVdmGS/KQ45FvhMVRXwgyS7J9mrqm4cSkBJ\n89aothEwv9qJUeLvhCQ9xCKTJI2z9gv38tNLt7DHAaPT2XPT+vu5767yH4oP2Qe4oWt9Q7PNIpOk\ngRrFNgJsJ9rk74QkPcQikyRNYI8DFnDSp3drO8aDPv2m23lgS7UdY9ZJsoLO43QsXbq05TSS5opR\nayPAdqJt/k5IUsdoldslSerNRmDfrvUlzbaHqapVVTVWVWOLFy8eWjhJkiRpPrLIJEmajVYDr29m\nmTsMuNXxmCRpfuhhBtIjktya5NLm9a5hZ5Sk+crH5SRJIyfJ54AjgEVJNgDvBnYEqKqVwBrgGGA9\ncBfwhnaSSpJacCZwGvCZKY65qKpePJw4kqStLDJJkkZOVZ2wjf0FvGVIcSRJI6SHGUglSS3xcTlJ\nkiRJc81zklyW5OtJnjrZQUlWJFmXZN3mzZuHmU+S5iSLTJIkSZLmkkuApVV1EPBR4NzJDnSCCEma\nWRaZJEmSJM0ZVXVbVd3RLK8BdkyyqOVYkjQvWGSSJEmSNGck2TNJmuXldD7z3NRuKkmaHxz4W5Ik\nSdKs0cMMpK8A3pxkC3A3cHwzYYQkacAsMkmSJEmaNXqYgfQ04LQhxZEkdRlYkSnJGcCLgU1V9bQJ\n9v8p8JquHP8NWFxVv0hyPXA7cD+wparGBpVTkiRJkiRJ/RvkmExnAkdNtrOqPlBVz6iqZwB/Dnyn\nqn7RdciRzX4LTJIkSZIkSSNuYEWmqroQ+MU2D+w4AfjcoLJIkiRJkiRpsFqfXS7Jo+n0ePpi1+YC\nzk9ycZIV23j/iiTrkqzbvHnzIKNKkiRJkiRpEq0XmYCXAN8b96jc4c1jdEcDb0ny/MneXFWrqmqs\nqsYWL1486KySJEmSJEmawCgUmY5n3KNyVbWx+bkJOAdY3kIuSZIkSZIk9ajVIlOSxwG/AXy5a9tj\nkuy6dRl4EXBFOwklSZIkSZLUix0GdeIknwOOABYl2QC8G9gRoKpWNoe9DPhmVd3Z9dYnAuck2Zrv\ns1X1jUHllCRJkiRJUv8GVmSqqhN6OOZM4Mxx264DDh5MKkmSJEmSJA3CKIzJJEmSJEmSpFnOIpMk\nSZIkSZL6ZpFJkiRJkiRJfbPIJEmSJEmSpL5ZZJIkSZIkSVLfLDJJkiRJkiSpbxaZJEmSJEmS1Lcd\n2g6giV10xt2s/cK9bceYFZ513KN43om7tB1DkiRJkqR5zSLTiFr7hXv56aVb2OMAO5tNZdP6+7nv\nrrLIJEmSJElSyywyjbA9DljASZ/ere0YI+3Tb7qdB7ZU2zEkSZIkSZr37CYjSZIkSZKkvllkkiRJ\nkiRJUt8sMkmSJEmSJKlvFpkkSZIkSZLUN4tMkiRJkiRJ6ptFJkmSJEmSJPXNIpMkSZIkSZL6ZpFJ\nkiRJkiRJfbPIJEmSJEmSpL5ZZJIkSZIkSVLfLDJJkiRJkiSpbxaZJEmSJM0aSc5IsinJFZPsT5KP\nJFmf5LIkhww7oyTNVxaZJEmSJM0mZwJHTbH/aGBZ81oBfHwImSRJWGSSJEmSNItU1YXAL6Y45Fjg\nM9XxA2D3JHsNJ50kzW8DKzL10I31iCS3Jrm0eb2ra99RSa5turieOqiMkiRJkuacfYAbutY3NNse\nIcmKJOuSrNu8efNQwknSXDbInkxnMnU3VoCLquoZzet9AEkWAqfT6eZ6IHBCkgMHmFOSJEnSPFRV\n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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "_rabi_rates = [np.pi/2,np.pi/2,np.pi,np.pi]\n", + "_azimuthal_angles = [0., -np.pi/2, 0., np.pi/2]\n", + "_detunings = [np.pi/2, 0, -np.pi/2, 0]\n", + "_durations = [0.5, 1, 2, 0.5]\n", + "_name = 'Custon Driven Control'\n", + "\n", + "custom_driven_control = DrivenControl(rabi_rates=_rabi_rates, \n", + " azimuthal_angles=_azimuthal_angles,\n", + " detunings=_detunings,\n", + " durations=_durations,\n", + " name=_name)\n", + "\n", + "## let us plot and verify\n", + "formatted_plot_data = custom_driven_control.get_plot_formatted_arrays(coordinates='cylindrical')\n", + "rabi_rates, azimuthal_angles, detunings, times = (formatted_plot_data['rabi_rates'],\n", + " formatted_plot_data['azimuthal_angles'],\n", + " formatted_plot_data['detunings'],\n", + " formatted_plot_data['times'])\n", + "\n", + "figure, (x_axis, y_axis, z_axis) = plt.subplots(1, 3, figsize=(20,5))\n", + "\n", + "x_axis.fill_between(times, rabi_rates, 0, alpha=0.15, color='#680cea')\n", + "x_axis.plot(times, rabi_rates, color='#680cea')\n", + "x_axis.set_xlabel('Time (s)')\n", + "x_axis.set_ylabel('Rabi Rate (radHz)')\n", + "\n", + "y_axis.fill_between(times, azimuthal_angles, 0, alpha=0.15, color='#680cea')\n", + "y_axis.plot(times, azimuthal_angles, color='#680cea')\n", + "y_axis.set_xlabel('Time (s)')\n", + "y_axis.set_ylabel('Azimuthal Angle (rad)')\n", + "\n", + "z_axis.fill_between(times, detunings, 0, alpha=0.15, color='#680cea')\n", + "z_axis.plot(times, detunings, color='#680cea')\n", + "z_axis.set_xlabel('Time (s)')\n", + "z_axis.set_ylabel('Detuning (radHz)')\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/qctrlopencontrols/__init__.py b/qctrlopencontrols/__init__.py index 694090dc..b7268b97 100644 --- a/qctrlopencontrols/__init__.py +++ b/qctrlopencontrols/__init__.py @@ -21,5 +21,5 @@ from .dynamic_decoupling_sequences import (DynamicDecouplingSequence, new_predefined_dds, convert_dds_to_driven_controls) -from .driven_controls import DrivenControls +from .driven_controls import DrivenControl, new_predefined_driven_control from .qiskit import convert_dds_to_quantum_circuit diff --git a/qctrlopencontrols/driven_controls/__init__.py b/qctrlopencontrols/driven_controls/__init__.py index ef6755d0..779ff29a 100644 --- a/qctrlopencontrols/driven_controls/__init__.py +++ b/qctrlopencontrols/driven_controls/__init__.py @@ -14,12 +14,29 @@ """ ============= -Pulses module +driven_controls module ============= """ -from .driven_controls import DrivenControls +from .driven_control import DrivenControl from .constants import ( UPPER_BOUND_RABI_RATE, UPPER_BOUND_DETUNING_RATE, - UPPER_BOUND_DURATION, LOWER_BOUND_DURATION, UPPER_BOUND_SEGMENTS) + UPPER_BOUND_DURATION, LOWER_BOUND_DURATION, UPPER_BOUND_SEGMENTS, + PRIMITIVE, BB1, SK1, + WAMF1, + CORPSE, + CORPSE_IN_SK1, + CORPSE_IN_BB1, + SCROFULOUS, + CORPSE_IN_SCROFULOUS) + +from .predefined import ( + new_predefined_driven_control, + new_primitive_control, new_wimperis_1_control, new_solovay_kitaev_1_control, + new_compensating_for_off_resonance_with_a_pulse_sequence_control, + new_compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev_control, + new_compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis_control, + new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control, + new_walsh_amplitude_modulated_filter_1_control, + new_corpse_in_scrofulous_control) diff --git a/qctrlopencontrols/driven_controls/constants.py b/qctrlopencontrols/driven_controls/constants.py index bac69616..5ef657a8 100644 --- a/qctrlopencontrols/driven_controls/constants.py +++ b/qctrlopencontrols/driven_controls/constants.py @@ -14,7 +14,7 @@ """ ================ -pulses.constants +driven_controls.constants ================ """ @@ -38,3 +38,41 @@ UPPER_BOUND_SEGMENTS = 10000 """Maximum number of segments allowed in a control """ + +#Driven control types +PRIMITIVE = 'primitive' +"""Primitive control +""" + +BB1 = 'BB1' +"""First-order Wimperis control, also known as BB1 +""" + +SK1 = 'SK1' +"""First-order Solovay-Kitaev control +""" + +WAMF1 = 'WAMF1' +"""First-order Walsh sequence control +""" + +CORPSE = 'CORPSE' +"""Dynamically corrected control - Compensating for Off-Resonance with a Pulse Sequence (COPRSE) +""" + +CORPSE_IN_BB1 = 'CORPSE in BB1' +"""Concatenated dynamically corrected control - BB1 inside COPRSE +""" + +CORPSE_IN_SK1 = 'CORPSE in SK1' +"""Concatenated dynamically corrected control - First order Solovay-Kitaev inside COPRSE +""" + +SCROFULOUS = 'SCROFULOUS' +"""Dynamically corrected control - + Short Composite Rotation For Undoing Length Over and Under Shoot (SCROFULOUS) +""" + +CORPSE_IN_SCROFULOUS = 'CORPSE in SCROFULOUS' +"""Concatenated dynamically corrected control - CORPSE inside SCROFULOUS +""" diff --git a/qctrlopencontrols/driven_controls/driven_control.py b/qctrlopencontrols/driven_controls/driven_control.py new file mode 100644 index 00000000..7a593537 --- /dev/null +++ b/qctrlopencontrols/driven_controls/driven_control.py @@ -0,0 +1,626 @@ +# Copyright 2019 Q-CTRL Pty Ltd & Q-CTRL Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +====================== +driven_controls.driven_controls +====================== +""" +import json +import numpy as np + +from qctrlopencontrols.exceptions import ArgumentsValueError +from qctrlopencontrols.base import QctrlObject + +from qctrlopencontrols.globals import ( + QCTRL_EXPANDED, CSV, JSON, CARTESIAN, CYLINDRICAL) + +from .constants import ( + UPPER_BOUND_SEGMENTS, UPPER_BOUND_RABI_RATE, UPPER_BOUND_DETUNING_RATE, + UPPER_BOUND_DURATION, LOWER_BOUND_DURATION) + + +def get_plot_data_from_segments(segments): + """ + Generates arrays that can be used to produce a plot representing the shape of the driven control + constructed from the segments. + + Parameters + ---------- + segments : list + List of segments formatted as described in qctrlopencontrols.driven_controls.DrivenControl + + Returns + ------- + tuple + Tuple made up of arrays for plotting formatted as + (amplitude_x,amplitude_y,amplitude_z,time) where: + - amplitude_k is the amplitude values. + - times the time corresponding to each amplitude_k coordinate. + Note that plot will have repeated times and for amplitudes, this is because it is + expected that these coordinates are to be used with plotting software that 'joins + the dots' with linear lines between each coordinate. The time array gives the x + values for all the amplitude arrays, which give the y values. + + """ + segment_times = np.insert(np.cumsum(segments[:, 3]), 0, 0.) + coords = len(segment_times) + coord_amplitude_x = np.concatenate([[0.], segments[:, 0], [0.]]) + coord_amplitude_y = np.concatenate([[0.], segments[:, 1], [0.]]) + coord_amplitude_z = np.concatenate([[0.], segments[:, 2], [0.]]) + plot_time = [] + plot_amplitude_x = [] + plot_amplitude_y = [] + plot_amplitude_z = [] + for i in range(coords): + plot_time.append(segment_times[i]) + plot_time.append(segment_times[i]) + plot_amplitude_x.append(coord_amplitude_x[i]) + plot_amplitude_x.append(coord_amplitude_x[i + 1]) + plot_amplitude_y.append(coord_amplitude_y[i]) + plot_amplitude_y.append(coord_amplitude_y[i + 1]) + plot_amplitude_z.append(coord_amplitude_z[i]) + plot_amplitude_z.append(coord_amplitude_z[i + 1]) + + return (np.array(plot_amplitude_x), + np.array(plot_amplitude_y), + np.array(plot_amplitude_z), + np.array(plot_time)) + + +class DrivenControl(QctrlObject): #pylint: disable=too-few-public-methods + """ + Creates a driven control. A driven is a set of segments made up of amplitude vectors + and corresponding durations. + + Parameters + ---------- + rabi_rates : numpy.ndarray, optional + 1-D array of size nx1 where n is number of segments; + Each entry is the rabi rate for the segment. Defaults to None + azimuthal_angles : numpy.ndarray, optional + 1-D array of size nx1 where n is the number of segments; + Each entry is the azimuthal angle for the segment; Defaults to None + detunings : numpy.ndarray, optional + 1-D array of size nx1 where n is the number of segments; + Each entry is the detuning angle for the segment; Defaults to None + durations : numpy.ndarray, optional + 1-D array of size nx1 where n is the number of segments; + Each entry is the duration of the segment (in seconds); Defaults to None + name : string, optional + An optional string to name the driven control. Defaults to None. + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + + def __init__(self, + rabi_rates=None, + azimuthal_angles=None, + detunings=None, + durations=None, + name=None): + + self.name = name + if self.name is not None: + self.name = str(self.name) + + check_none_values = [(rabi_rates is None), (azimuthal_angles is None), + (detunings is None), (durations is None)] + all_are_none = all(value is True for value in check_none_values) + if all_are_none: + rabi_rates = np.array([np.pi]) + azimuthal_angles = np.array([0.]) + detunings = np.array([0.]) + durations = np.array([1.]) + else: + # some may be None while others are not + input_array_lengths = [] + if not check_none_values[0]: + rabi_rates = np.array(rabi_rates, dtype=np.float).reshape((-1,)) + input_array_lengths.append(rabi_rates.shape[0]) + + if not check_none_values[1]: + azimuthal_angles = np.array(azimuthal_angles, dtype=np.float).reshape((-1,)) + input_array_lengths.append(len(azimuthal_angles)) + + if not check_none_values[2]: + detunings = np.array(detunings, dtype=np.float).reshape((-1,)) + input_array_lengths.append(len(detunings)) + + if not check_none_values[3]: + durations = np.array(durations, dtype=np.float).reshape((-1,)) + input_array_lengths.append(len(durations)) + + # check all valid array lengths are equal + if max(input_array_lengths) != min(input_array_lengths): + raise ArgumentsValueError('Rabi rates, Azimuthal angles, Detunings and Durations ' + 'must be of same length', + {'rabi_rates': rabi_rates, + 'azimuthal_angles': azimuthal_angles, + 'detunings': detunings, + 'durations': durations}) + + valid_input_length = max(input_array_lengths) + if check_none_values[0]: + rabi_rates = np.zeros((valid_input_length,)) + if check_none_values[1]: + azimuthal_angles = np.zeros((valid_input_length,)) + if check_none_values[2]: + detunings = np.zeros((valid_input_length,)) + if check_none_values[3]: + durations = np.ones((valid_input_length,)) + + self.rabi_rates = rabi_rates + self.azimuthal_angles = azimuthal_angles + self.detunings = detunings + self.durations = durations + + # check if all the rabi_rates are greater than zero + if np.any(rabi_rates < 0.): + raise ArgumentsValueError('All rabi rates must be greater than zero.', + {'rabi_rates': rabi_rates}, + extras={ + 'azimuthal_angles': azimuthal_angles, + 'detunings': detunings, + 'durations': durations}) + + # check if all the durations are greater than zero + if np.any(durations <= 0): + raise ArgumentsValueError('Duration of driven control segments must all be greater' + + ' than zero.', + {'durations': self.durations}) + + self.number_of_segments = rabi_rates.shape[0] + if self.number_of_segments > UPPER_BOUND_SEGMENTS: + raise ArgumentsValueError( + 'The number of segments must be smaller than the upper bound:' + + str(UPPER_BOUND_SEGMENTS), + {'number_of_segments': self.number_of_segments}) + + super(DrivenControl, self).__init__( + base_attributes=['rabi_rates', 'azimuthal_angles', 'detunings', + 'durations', 'name']) + + if self.maximum_rabi_rate > UPPER_BOUND_RABI_RATE: + raise ArgumentsValueError( + 'Maximum rabi rate of segments must be smaller than the upper bound: ' + + str(UPPER_BOUND_RABI_RATE), + {'maximum_rabi_rate': self.maximum_rabi_rate}) + + if self.maximum_detuning > UPPER_BOUND_DETUNING_RATE: + raise ArgumentsValueError( + 'Maximum detuning of segments must be smaller than the upper bound: ' + + str(UPPER_BOUND_DETUNING_RATE), + {'maximum_detuning': self.maximum_detuning}) + if self.maximum_duration > UPPER_BOUND_DURATION: + raise ArgumentsValueError( + 'Maximum duration of segments must be smaller than the upper bound: ' + + str(UPPER_BOUND_DURATION), + {'maximum_duration': self.maximum_duration}) + if self.minimum_duration < LOWER_BOUND_DURATION: + raise ArgumentsValueError( + 'Minimum duration of segments must be larger than the lower bound: ' + + str(LOWER_BOUND_DURATION), + {'minimum_duration': self.minimum_duration}) + + @property + def maximum_rabi_rate(self): + """Returns the maximum rabi rate of the control + + Returns + ------- + float + The maximum rabi rate of the control + """ + + return np.amax(self.rabi_rates) + + @property + def maximum_detuning(self): + """Returns the maximum detuning of the control + + Returns + ------- + float + The maximum detuning of the control + """ + return np.amax(self.detunings) + + @property + def amplitude_x(self): + """Return the X-Amplitude + + Returns + ------- + numpy.ndarray + X-Amplitude of each segment + """ + + return self.rabi_rates * np.cos(self.azimuthal_angles) + + @property + def amplitude_y(self): + """Return the Y-Amplitude + + Returns + ------- + numpy.ndarray + Y-Amplitude of each segment + """ + + return self.rabi_rates * np.sin(self.azimuthal_angles) + + @property + def angles(self): + """Returns the angles + + Returns + ------- + numpy.darray + Angles as 1-D array of floats + """ + + amplitudes = np.sqrt(self.amplitude_x ** 2 + + self.amplitude_y ** 2 + + self.detunings ** 2) + angles = amplitudes * self.durations + + return angles + + @property + def directions(self): + + """Returns the directions + + Returns + ------- + numpy.ndarray + Directions as 1-D array of floats + """ + amplitudes = np.sqrt(self.amplitude_x ** 2 + + self.amplitude_y ** 2 + + self.detunings ** 2) + normalized_amplitude_x = self.amplitude_x/amplitudes + normalized_amplitude_y = self.amplitude_y/amplitudes + normalized_detunings = self.detunings/amplitudes + + normalized_amplitudes = np.hstack((normalized_amplitude_x[:, np.newaxis], + normalized_amplitude_y[:, np.newaxis], + normalized_detunings[:, np.newaxis])) + + directions = np.array([normalized_amplitudes if amplitudes[i] != 0. else + np.zeros([3, ]) for i in range(self.number_of_segments)]) + + return directions + + @property + def times(self): + """Returns the time of each segment within the duration + of the control + + Returns + ------ + numpy.ndarray + Segment times as 1-D array of floats + """ + + return np.insert(np.cumsum(self.durations), 0, 0.) + + @property + def maximum_duration(self): + """Returns the maximum duration of all the control segments + + Returns + ------- + float + The maximum duration of all the control segments + """ + + return np.amax(self.durations) + + @property + def minimum_duration(self): + """Returns the minimum duration of all the control segments + + Returns + ------- + float + The minimum duration of all the controls segments + """ + + return np.amin(self.durations) + + @property + def duration(self): + """Returns the total duration of the control + + Returns + ------- + float + Total duration of the control + """ + + return np.sum(self.durations) + + def _qctrl_expanded_export_content(self, file_type, coordinates): + + """Private method to prepare the content to be saved in Q-CTRL expanded format + + Parameters + ---------- + file_type : str, optional + One of 'csv' or 'json'; defaults to 'csv'. + coordinates : str, optional + Indicates the co-ordinate system requested. Must be one of + 'cylindrical', 'cartesian' or 'polar'; defaults to 'cylindrical' + + Returns + ------- + list or dict + Based on file_type; list if 'csv', dict if 'json' + """ + control_info = None + amplitude_x = self.amplitude_x + amplitude_y = self.amplitude_y + if coordinates == CARTESIAN: + if file_type == CSV: + + control_info = list() + control_info.append('amplitude_x,amplitude_y,detuning,duration,maximum_rabi_rate') + for segment_idx in range(self.number_of_segments): + control_info.append('{},{},{},{},{}'.format( + amplitude_x[segment_idx], + amplitude_y[segment_idx], + self.detunings[segment_idx], + self.durations[segment_idx], + self.maximum_rabi_rate + )) + else: + control_info = dict() + if self.name is not None: + control_info['name'] = self.name + control_info['maximum_rabi_rate'] = self.maximum_rabi_rate + control_info['amplitude_x'] = list(amplitude_x) + control_info['amplitude_y'] = list(amplitude_y) + control_info['detuning'] = list(self.detunings) + control_info['duration'] = list(self.durations) + + else: + + if file_type == CSV: + control_info = list() + control_info.append('rabi_rate,azimuthal_angle,detuning,duration,maximum_rabi_rate') + for segment_idx in range(self.number_of_segments): + control_info.append('{},{},{},{},{}'.format( + self.rabi_rates[segment_idx]/self.maximum_rabi_rate, + np.arctan2(amplitude_y[segment_idx], + amplitude_x[segment_idx]), + self.detunings[segment_idx], + self.durations[segment_idx], + self.maximum_rabi_rate + )) + + else: + control_info = dict() + if self.name is not None: + control_info['name'] = self.name + control_info['maximum_rabi_rate'] = self.maximum_rabi_rate + control_info['rabi_rates'] = list(self.rabi_rates / self.maximum_rabi_rate) + control_info['azimuthal_angles'] = list(np.arctan2( + amplitude_y, amplitude_x)) + control_info['detuning'] = list(self.detunings) + control_info['duration'] = list(self.durations) + + return control_info + + def _export_to_qctrl_expanded_format(self, filename=None, + file_type=CSV, + coordinates=CYLINDRICAL): + + """Private method to save control in qctrl_expanded_format + + Parameters + ---------- + filename : str, optional + Name and path of the file to save the control into. + Defaults to None + file_type : str, optional + One of 'CSV' or 'JSON'; defaults to 'CSV'. + coordinates : str, optional + Indicates the co-ordinate system requested. Must be one of + 'Cylindrical', 'Cartesian'; defaults to 'Cylindrical' + """ + + control_info = self._qctrl_expanded_export_content(file_type=file_type, + coordinates=coordinates) + if file_type == CSV: + with open(filename, 'wt') as handle: + + control_info = '\n'.join(control_info) + handle.write(control_info) + else: + with open(filename, 'wt') as handle: + json.dump(control_info, handle, sort_keys=True, indent=4) + + def export_to_file(self, filename=None, + file_format=QCTRL_EXPANDED, + file_type=CSV, + coordinates=CYLINDRICAL): + + """Prepares and saves the driven control in a file. + + Parameters + ---------- + filename : str, optional + Name and path of the file to save the control into. + Defaults to None + file_format : str + Specified file format for saving the control. Defaults to + 'Q-CTRL expanded'; Currently it does not support any other format. + For detail of the `Q-CTRL Expanded Format` consult + `Q-CTRL Control Data Format + ` _. + file_type : str, optional + One of 'CSV' or 'JSON'; defaults to 'CSV'. + coordinates : str, optional + Indicates the co-ordinate system requested. Must be one of + 'Cylindrical', 'Cartesian'; defaults to 'Cylindrical' + + References + ---------- + `Q-CTRL Control Data Format + ` _. + + Raises + ------ + ArgumentsValueError + Raised if some of the parameters are invalid. + """ + + if filename is None: + raise ArgumentsValueError('Invalid filename provided.', + {'filename': filename}) + + if file_format not in [QCTRL_EXPANDED]: + raise ArgumentsValueError('Requested file format is not supported. Please use ' + 'one of {}'.format([QCTRL_EXPANDED]), + {'file_format': file_format}) + + if file_type not in [CSV, JSON]: + raise ArgumentsValueError('Requested file type is not supported. Please use ' + 'one of {}'.format([CSV, JSON]), + {'file_type': file_type}) + + if coordinates not in [CYLINDRICAL, CARTESIAN]: + raise ArgumentsValueError('Requested coordinate type is not supported. Please use ' + 'one of {}'.format([CARTESIAN, CYLINDRICAL]), + {'coordinates': coordinates}) + + if file_format == QCTRL_EXPANDED: + self._export_to_qctrl_expanded_format(filename=filename, + file_type=file_type, + coordinates=coordinates) + + def get_plot_formatted_arrays(self, coordinates=CARTESIAN, dimensionless_rabi_rate=True): + """ Gets arrays for plotting a driven control. + + Parameters + ---------- + dimensionless_rabi_rate: boolean + If True, calculates the dimensionless values for segments + coordinates : string + Indicated the type of segments that need to be transformed can be 'cartesian' or + 'cylindrical'. + + Returns + ------- + dict + A dict with keywords depending on the chosen coordinates. For 'cylindrical', we have + 'rabi_rate', 'azimuthal_angle', 'detuning' and 'times', and for 'cartesian' we have + 'amplitude_x', 'amplitude_y', 'detuning' and 'times'. + + Notes + ----- + The plot data can have repeated times and for amplitudes, because it is expected + that these coordinates are to be used with plotting software that 'joins the dots' with + linear lines between each coordinate. The time array gives the x values for all the + amplitude arrays, which give the y values. + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + if dimensionless_rabi_rate: + normalizer = self.maximum_rabi_rate + else: + normalizer = 1 + + if coordinates == CARTESIAN: + (x_amplitudes, y_amplitudes, detunings, times) = get_plot_data_from_segments( + np.vstack((self.amplitude_x / normalizer, self.amplitude_y / normalizer, + self.detunings, self.durations)).T + ) + plot_dictionary = { + 'amplitudes_x': x_amplitudes, + 'amplitudes_y': y_amplitudes, + 'detunings': detunings, + 'times': times + } + + elif coordinates == CYLINDRICAL: + (x_plot, y_plot, detunings, times) = get_plot_data_from_segments( + np.vstack((self.rabi_rates / normalizer, self.azimuthal_angles, + self.detunings, self.durations)).T + ) + x_plot[np.equal(x_plot, -0.0)] = 0. + y_plot[np.equal(y_plot, -0.0)] = 0. + azimuthal_angles_plot = np.arctan2(y_plot, x_plot) + amplitudes_plot = np.sqrt(np.abs(x_plot**2 + y_plot**2)) + plot_dictionary = { + 'rabi_rates': amplitudes_plot, + 'azimuthal_angles': azimuthal_angles_plot, + 'detunings': detunings, + 'times': times + } + else: + raise ArgumentsValueError( + 'Unsupported coordinates provided: ', + arguments={'coordinates': coordinates}) + + return plot_dictionary + + def __str__(self): + """Prepares a friendly string format for a Driven Control + """ + driven_control_string = list() + + if self.name is not None: + driven_control_string.append('{}:'.format(self.name)) + + pretty_rabi_rates = [str(rabi_rate/self.maximum_rabi_rate) + if self.maximum_rabi_rate != 0 else '0' + for rabi_rate in list(self.rabi_rates)] + pretty_rabi_rates = ','.join(pretty_rabi_rates) + pretty_azimuthal_angles = [str(azimuthal_angle/np.pi) + for azimuthal_angle in self.azimuthal_angles] + pretty_azimuthal_angles = ','.join(pretty_azimuthal_angles) + pretty_detuning = [str(detuning/self.maximum_detuning) + if self.maximum_detuning != 0 else '0' + for detuning in list(self.detunings)] + pretty_detuning = ','.join(pretty_detuning) + + pretty_durations = [str(duration/self.duration) for duration in self.durations] + pretty_durations = ','.join(pretty_durations) + + driven_control_string.append( + 'Rabi Rates = [{}] x {}'.format(pretty_rabi_rates, + self.maximum_rabi_rate)) + driven_control_string.append( + 'Azimuthal Angles = [{}] x pi'.format(pretty_azimuthal_angles)) + driven_control_string.append( + 'Detunings = [{}] x {}'.format(pretty_detuning, + self.maximum_detuning)) + driven_control_string.append('Durations = [{}] x {}'.format(pretty_durations, + self.duration)) + driven_control_string = '\n'.join(driven_control_string) + + return driven_control_string + + +if __name__ == '__main__': + pass diff --git a/qctrlopencontrols/driven_controls/driven_controls.py b/qctrlopencontrols/driven_controls/driven_controls.py deleted file mode 100644 index 983da0ab..00000000 --- a/qctrlopencontrols/driven_controls/driven_controls.py +++ /dev/null @@ -1,296 +0,0 @@ -# Copyright 2019 Q-CTRL Pty Ltd & Q-CTRL Inc -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -====================== -pulses.driven_controls -====================== -""" -import json -import numpy as np - -from qctrlopencontrols.exceptions import ArgumentsValueError -from qctrlopencontrols.base import QctrlObject - -from qctrlopencontrols.globals import ( - QCTRL_EXPANDED, CSV, JSON, CARTESIAN, CYLINDRICAL) - -from .constants import ( - UPPER_BOUND_SEGMENTS, UPPER_BOUND_RABI_RATE, UPPER_BOUND_DETUNING_RATE, - UPPER_BOUND_DURATION, LOWER_BOUND_DURATION) - - -class DrivenControls(QctrlObject): #pylint: disable=too-few-public-methods - """Creates a pulse. A pulse is a set of segments made up of amplitude vectors and durations. - - Parameters - ---------- - segments : list, optional - Defaults to None. A list of amplitude vector components - and durations. Each element of the list should be formatted as - [amplitude_x,amplitude_y,amplitude_z,duration] where amplitude_i is the angular - rabi frequency to be multiplied by the - corresponding pauli matrix, i.e. amplitude_x would correspond to sigma_x. - The duration is the time of that segment. - If None, defaults to a square pi pulse [[np.pi, 0, 0, 1], ]. - name : string, optional - Defaults to None. An optional string to name the pulse. - - Raises - ------ - ArgumentsValueError - Raised when an argument is invalid. - """ - - def __init__(self, - segments=None, - name=None): - - self.name = name - if self.name is not None: - self.name = str(self.name) - - if segments is None: - segments = [[np.pi, 0, 0, 1], ] - - self.segments = np.array(segments, dtype=np.float) - self.number_of_segments = len(self.segments) - if self.segments.shape != (self.number_of_segments, 4): - raise ArgumentsValueError('Segments must be of shape (number_of_segments,4).', - {'segments': self.segments}, - extras={'number_of_segments': self.number_of_segments}) - if self.number_of_segments > UPPER_BOUND_SEGMENTS: - raise ArgumentsValueError( - 'The number of segments must be smaller than the upper bound:' - + str(UPPER_BOUND_SEGMENTS), - {'segments': self.segments}, - extras={'number_of_segments': self.number_of_segments}) - - self.amplitudes = np.sqrt(np.sum(self.segments[:, 0:3] ** 2, axis=1)) - - self.segment_durations = self.segments[:, 3] - if np.any(self.segment_durations <= 0): - raise ArgumentsValueError('Duration of pulse segments must all be greater' - + ' than zero.', - {'segments': self.segments}, - extras={'segment_durations': self.segment_durations}) - - super(DrivenControls, self).__init__( - base_attributes=['segments', 'name']) - - self.angles = self.amplitudes * self.segment_durations - self.directions = np.array([self.segments[i, 0:3] / self.amplitudes[i] - if self.amplitudes[i] != 0. else np.zeros([3, ]) - for i in range(self.number_of_segments)]) - - self.segment_times = np.insert( - np.cumsum(self.segment_durations), 0, 0.) - self.duration = self.segment_times[-1] - - self.rabi_rates = np.sqrt(np.sum(self.segments[:, 0:2]**2, axis=1)) - - self.maximum_rabi_rate = np.amax(self.rabi_rates) - self.maximum_detuning = np.amax(np.abs(self.segments[:, 2])) - self.maximum_amplitude = np.amax(self.amplitudes) - self.minimum_duration = np.amin(self.segment_durations) - self.maximum_duration = np.amax(self.segment_durations) - - if self.maximum_rabi_rate > UPPER_BOUND_RABI_RATE: - raise ArgumentsValueError( - 'Maximum rabi rate of segments must be smaller than the upper bound: ' - + str(UPPER_BOUND_RABI_RATE), - {'segments': self.segments}, - extras={'maximum_rabi_rate': self.maximum_rabi_rate}) - - if self.maximum_detuning > UPPER_BOUND_DETUNING_RATE: - raise ArgumentsValueError( - 'Maximum detuning of segments must be smaller than the upper bound: ' - + str(UPPER_BOUND_DETUNING_RATE), - {'segments': self.segments}, - extras={'maximum_detuning': self.maximum_detuning}) - if self.maximum_duration > UPPER_BOUND_DURATION: - raise ArgumentsValueError( - 'Maximum duration of segments must be smaller than the upper bound: ' - + str(UPPER_BOUND_DURATION), - {'segments': self.segments}, - extras={'maximum_duration': self.maximum_duration}) - if self.minimum_duration < LOWER_BOUND_DURATION: - raise ArgumentsValueError( - 'Minimum duration of segments must be larger than the lower bound: ' - + str(LOWER_BOUND_DURATION), - {'segments': self.segments}, - extras={'minimum_duration'}) - - def _qctrl_expanded_export_content(self, file_type, coordinates): - - """Private method to prepare the content to be saved in Q-CTRL expanded format - - Parameters - ---------- - file_type : str, optional - One of 'csv' or 'json'; defaults to 'csv'. - coordinates : str, optional - Indicates the co-ordinate system requested. Must be one of - 'cylindrical', 'cartesian' or 'polar'; defaults to 'cylindrical' - - Returns - ------- - list or dict - Based on file_type; list if 'csv', dict if 'json' - """ - - control_info = None - if coordinates == CARTESIAN: - - if file_type == CSV: - - control_info = list() - control_info.append('amplitude_x,amplitude_y,detuning,duration,maximum_rabi_rate') - for segment_idx in range(self.segments.shape[0]): - control_info.append('{},{},{},{},{}'.format( - self.segments[segment_idx, 0] / self.maximum_rabi_rate, - self.segments[segment_idx, 1] / self.maximum_rabi_rate, - self.segments[segment_idx, 2], - self.segments[segment_idx, 3], - self.maximum_rabi_rate - )) - else: - control_info = dict() - if self.name is not None: - control_info['name'] = self.name - control_info['maximum_rabi_rate'] = self.maximum_rabi_rate - control_info['amplitude_x'] = list(self.segments[:, 0]/self.maximum_rabi_rate) - control_info['amplitude_y'] = list(self.segments[:, 1] / self.maximum_rabi_rate) - control_info['detuning'] = list(self.segments[:, 2]) - control_info['duration'] = list(self.segments[:, 3]) - - else: - - if file_type == CSV: - control_info = list() - control_info.append('rabi_rate,azimuthal_angle,detuning,duration,maximum_rabi_rate') - for segment_idx in range(self.segments.shape[0]): - control_info.append('{},{},{},{},{}'.format( - self.rabi_rates[segment_idx]/self.maximum_rabi_rate, - np.arctan2(self.segments[segment_idx, 1], - self.segments[segment_idx, 0]), - self.segments[segment_idx, 2], - self.segments[segment_idx, 3], - self.maximum_rabi_rate - )) - - else: - control_info = dict() - if self.name is not None: - control_info['name'] = self.name - control_info['maximum_rabi_rate'] = self.maximum_rabi_rate - control_info['rabi_rates'] = list(self.rabi_rates / self.maximum_rabi_rate) - control_info['azimuthal_angles'] = list(np.arctan2( - self.segments[:, 1], self.segments[:, 0])) - control_info['detuning'] = list(self.segments[:, 2]) - control_info['duration'] = list(self.segments[:, 3]) - - return control_info - - def _export_to_qctrl_expanded_format(self, filename=None, - file_type=CSV, - coordinates=CYLINDRICAL): - - """Private method to save control in qctrl_expanded_format - - Parameters - ---------- - filename : str, optional - Name and path of the file to save the control into. - Defaults to None - file_type : str, optional - One of 'CSV' or 'JSON'; defaults to 'CSV'. - coordinates : str, optional - Indicates the co-ordinate system requested. Must be one of - 'Cylindrical', 'Cartesian'; defaults to 'Cylindrical' - """ - - control_info = self._qctrl_expanded_export_content(file_type=file_type, - coordinates=coordinates) - if file_type == CSV: - with open(filename, 'wt') as handle: - - control_info = '\n'.join(control_info) - handle.write(control_info) - else: - with open(filename, 'wt') as handle: - json.dump(control_info, handle, sort_keys=True, indent=4) - - def export_to_file(self, filename=None, - file_format=QCTRL_EXPANDED, - file_type=CSV, - coordinates=CYLINDRICAL): - - """Prepares and saves the driven control in a file. - - Parameters - ---------- - filename : str, optional - Name and path of the file to save the control into. - Defaults to None - file_format : str - Specified file format for saving the control. Defaults to - 'Q-CTRL expanded'; Currently it does not support any other format. - For detail of the `Q-CTRL Expanded Format` consult - `Q-CTRL Control Data Format - ` _. - file_type : str, optional - One of 'CSV' or 'JSON'; defaults to 'CSV'. - coordinates : str, optional - Indicates the co-ordinate system requested. Must be one of - 'Cylindrical', 'Cartesian'; defaults to 'Cylindrical' - - References - ---------- - `Q-CTRL Control Data Format - ` _. - - Raises - ------ - ArgumentsValueError - Raised if some of the parameters are invalid. - """ - - if filename is None: - raise ArgumentsValueError('Invalid filename provided.', - {'filename': filename}) - - if file_format not in [QCTRL_EXPANDED]: - raise ArgumentsValueError('Requested file format is not supported. Please use ' - 'one of {}'.format([QCTRL_EXPANDED]), - {'file_format': file_format}) - - if file_type not in [CSV, JSON]: - raise ArgumentsValueError('Requested file type is not supported. Please use ' - 'one of {}'.format([CSV, JSON]), - {'file_type': file_type}) - - if coordinates not in [CYLINDRICAL, CARTESIAN]: - raise ArgumentsValueError('Requested coordinate type is not supported. Please use ' - 'one of {}'.format([CARTESIAN, CYLINDRICAL]), - {'coordinates': coordinates}) - - if file_format == QCTRL_EXPANDED: - self._export_to_qctrl_expanded_format(filename=filename, - file_type=file_type, - coordinates=coordinates) - - -if __name__ == '__main__': - pass diff --git a/qctrlopencontrols/driven_controls/predefined.py b/qctrlopencontrols/driven_controls/predefined.py new file mode 100644 index 00000000..c47d9b6e --- /dev/null +++ b/qctrlopencontrols/driven_controls/predefined.py @@ -0,0 +1,709 @@ +# Copyright 2019 Q-CTRL Pty Ltd & Q-CTRL Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +========================== +driven_controls.predefined +========================== + +More information and publication references to all driven controls defined here +can be found at https://docs.q-ctrl.com/control-library +""" + +import numpy as np + +from qctrlopencontrols.exceptions import ArgumentsValueError +from .driven_control import DrivenControl + +from .constants import ( + PRIMITIVE, BB1, SK1, + WAMF1, + CORPSE, + CORPSE_IN_SK1, + CORPSE_IN_BB1, + SCROFULOUS, + CORPSE_IN_SCROFULOUS) + + +def new_predefined_driven_control( + scheme=PRIMITIVE, + **kwargs): + """ + Create a new driven control + + Parameters + ---------- + scheme : string, optional + Defaults to None. The name of the driven control type, + supported options are: + - 'primitive' + - 'wimperis_1' + - 'solovay_kitaev_1' + - 'compensating_for_off_resonance_with_a_pulse_sequence' + - 'compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis' + - 'compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev' + - 'walsh_amplitude_modulated_filter_1' + - 'short_composite_rotation_for_undoing_length_over_and_under_shoot' + - 'corpse_in_scrofulous' + kwargs : dict, optional + options to make the corresponding control type. + + Returns + ------- + qctrlopencontrols.DrivenControls + Returns a driven control corresponding to the driven_control_type. + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + + # Forced to import here to avoid cyclic imports, need to review + # Raise error if the input driven_control_type is not known + if scheme == PRIMITIVE: + driven_control = new_primitive_control(**kwargs) + elif scheme == BB1: + driven_control = new_wimperis_1_control(**kwargs) + elif scheme == SK1: + driven_control = new_solovay_kitaev_1_control(**kwargs) + elif scheme == WAMF1: + driven_control = new_walsh_amplitude_modulated_filter_1_control(**kwargs) + elif scheme == CORPSE: + driven_control = new_compensating_for_off_resonance_with_a_pulse_sequence_control( + **kwargs) + elif scheme == CORPSE_IN_BB1: + driven_control = \ + new_compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis_control( + **kwargs) + elif scheme == \ + CORPSE_IN_SK1: + driven_control = \ + new_compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev_control( + **kwargs) + elif scheme == SCROFULOUS: + driven_control = \ + new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control(**kwargs) + elif scheme == CORPSE_IN_SCROFULOUS: + driven_control = new_corpse_in_scrofulous_control(**kwargs) + else: + raise ArgumentsValueError( + 'Unknown predefined pulse type. See help(new_predefined_driven_control) to display all' + + ' allowed inputs.', + {'scheme': scheme}) + return driven_control + +def _predefined_common_attributes(maximum_rabi_rate, + rabi_rotation, + azimuthal_angle): + """ + Adds some checks etc for all the predefined pulses + + Parameters + ---------- + rabi_rotation : float + The total polar angle to be performed by the pulse. + Defined in polar coordinates. + maximum_rabi_rate : float + Defaults to 2.*np.pi + The maximum rabi frequency for the pulse. + azimuthal_angle : float + The azimuthal position of the pulse. + + Returns + ------- + tuple + Tuple of floats made of: + (rabi_rate, rabi_rotation, azimuthal) + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + + maximum_rabi_rate = float(maximum_rabi_rate) + if maximum_rabi_rate <= 0: + raise ArgumentsValueError( + 'Maximum rabi angular frequency should be greater than zero.', + {'maximum_rabi_rate': maximum_rabi_rate}) + + rabi_rotation = float(rabi_rotation) + if rabi_rotation == 0: + raise ArgumentsValueError( + 'The rabi rotation must be non zero.', + {'rabi_rotation': rabi_rotation} + ) + + azimuthal_angle = float(azimuthal_angle) + + return (maximum_rabi_rate, rabi_rotation, azimuthal_angle) + +def _get_transformed_rabi_rotation_wimperis(rabi_rotation): + """ + Calculates the Rabi rotation angle as required by Wimperis 1 (BB1) + and Solovay-Kitaev driven controls. + + Parameters + ---------- + rabi_rotation : float + Rotation angle of the operation + + Returns + ------- + float + The transformed angle as per definition for the Wimperis 1 (BB1) control + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + # Raise error if the polar angle is incorrect + if rabi_rotation > 4 * np.pi: + raise ArgumentsValueError( + 'The polar angle must be between -4 pi and 4 pi (inclusive).', + {'rabi_rotation': rabi_rotation}) + return np.arccos(-rabi_rotation / (4 * np.pi)) + +def _derive_segments(angles, amplitude=2. * np.pi): + """ + Derive the driven control segments from a set of rabi_rotations defined in terms of the + spherical polar angles + + Parameters + ---------- + angles : numpy.array + angles is made of a list polar angle 2-lists formatted + as [polar_angle, azimuthal_angle]. + All angles should be greater or equal to 0, and the polar_angles + must be greater than zero. + amplitude : float, optional + Defaults to 1. The total amplitude of each segment in + rad Hz. + + Returns + ------- + list + Segments for the driven control. + + """ + segments = [[amplitude * np.cos(phi), amplitude * np.sin(phi), 0., theta / amplitude] + for (theta, phi) in angles] + return segments + + +def new_primitive_control( + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + Primitive driven control. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the driven control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the driven control. + azimuthal_angle : float, optional + The azimuthal position of the driven control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + return DrivenControl( + rabi_rates=[maximum_rabi_rate], + azimuthal_angles=[azimuthal_angle], + detunings=[0], + durations=[rabi_rotation/maximum_rabi_rate], + **kwargs) + + +def new_wimperis_1_control( + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + Wimperis or BB1 control. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + phi_p = _get_transformed_rabi_rotation_wimperis(rabi_rotation) + + rabi_rotations = [rabi_rotation, np.pi, 2 * np.pi, np.pi] + + rabi_rates = [maximum_rabi_rate] * 4 + azimuthal_angles = [azimuthal_angle, azimuthal_angle + phi_p, + azimuthal_angle + 3 * phi_p, azimuthal_angle + phi_p] + detunings = [0] * 4 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + +def new_solovay_kitaev_1_control( + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + First-order Solovay-Kitaev control, also known as SK1 + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + phi_p = _get_transformed_rabi_rotation_wimperis(rabi_rotation) + + rabi_rotations = [rabi_rotation, 2 * np.pi, 2 * np.pi] + + rabi_rates = [maximum_rabi_rate] * 3 + azimuthal_angles = [azimuthal_angle, azimuthal_angle - phi_p, azimuthal_angle + phi_p] + detunings = [0] * 3 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + + +def new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control( # pylint: disable=invalid-name + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + SCROFULOUS control to compensate for pulse length errors + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + # Create a lookup table for rabi rotation and phase angles, taken from the official paper. + # Note: values in the paper are in degrees. + def degrees_to_radians(angle_in_degrees): + return angle_in_degrees / 180 * np.pi + + if np.isclose(rabi_rotation, np.pi): + theta_1 = degrees_to_radians(180.) + phi_1 = np.arccos( + -np.pi * np.cos(theta_1) / 2 / theta_1 / np.sin(rabi_rotation / 2) + ) + phi_2 = phi_1 - np.arccos(- np.pi / 2 / theta_1) + elif np.isclose(rabi_rotation, 0.5 * np.pi): + theta_1 = degrees_to_radians(115.2) + phi_1 = np.arccos( + -np.pi * np.cos(theta_1) / 2 / theta_1 / np.sin(rabi_rotation / 2) + ) + phi_2 = phi_1 - np.arccos(- np.pi / 2 / theta_1) + elif np.isclose(rabi_rotation, 0.25 * np.pi): + theta_1 = degrees_to_radians(96.7) + phi_1 = np.arccos( + -np.pi * np.cos(theta_1) / 2 / theta_1 / np.sin(rabi_rotation / 2) + ) + phi_2 = phi_1 - np.arccos(- np.pi / 2 / theta_1) + else: + raise ArgumentsValueError( + 'rabi_rotation angle must be either pi, pi/2 or pi/4', + {'rabi_rotation': rabi_rotation}) + + theta_3 = theta_1 + phi_3 = phi_1 + theta_2 = np.pi + + rabi_rotations = [theta_1, theta_2, theta_3] + + rabi_rates = [maximum_rabi_rate] * 3 + azimuthal_angles = [azimuthal_angle + phi_1, azimuthal_angle + phi_2, azimuthal_angle + phi_3] + detunings = [0] * 3 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + + +def new_compensating_for_off_resonance_with_a_pulse_sequence_control( # pylint: disable=invalid-name + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + Compensating for off resonance with a pulse sequence, often abbreviated as CORPSE. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + k = np.arcsin(np.sin(rabi_rotation / 2.) / 2.) + + rabi_rotations = [rabi_rotation / 2. + 2 * np.pi - k, 2 * np.pi - 2 * k, rabi_rotation / 2. - k] + + rabi_rates = [maximum_rabi_rate] * 3 + azimuthal_angles = [azimuthal_angle, azimuthal_angle + np.pi, azimuthal_angle] + detunings = [0] * 3 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + + +def new_compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis_control( # pylint: disable=invalid-name + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + Compensating for off resonance with a pulse sequence with an embedded + Wimperis (or BB1) control, also known as CinBB. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + phi_p = _get_transformed_rabi_rotation_wimperis(rabi_rotation) + k = np.arcsin(np.sin(rabi_rotation / 2.) / 2.) + + rabi_rotations = [2 * np.pi + rabi_rotation / 2. - k, 2 * np.pi - 2 * k, + rabi_rotation / 2. - k, np.pi, 2 * np.pi, np.pi] + + rabi_rates = [maximum_rabi_rate] * 6 + azimuthal_angles = [azimuthal_angle, azimuthal_angle + np.pi, azimuthal_angle, + azimuthal_angle + phi_p, azimuthal_angle + 3 * phi_p, + azimuthal_angle + phi_p] + detunings = [0] * 6 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + + +def new_compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev_control( # pylint: disable=invalid-name + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + Compensating for off resonance with a pulse sequence with an + embedded Solovay Kitaev (or SK1) control, also knowns as CinSK. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + phi_p = _get_transformed_rabi_rotation_wimperis(rabi_rotation) + k = np.arcsin(np.sin(rabi_rotation / 2.) / 2.) + + rabi_rotations = [2 * np.pi + rabi_rotation / 2. - k, 2 * np.pi - 2 * k, + rabi_rotation / 2. - k, 2 * np.pi, 2 * np.pi] + + rabi_rates = [maximum_rabi_rate] * 5 + azimuthal_angles = [azimuthal_angle, azimuthal_angle + np.pi, azimuthal_angle, + azimuthal_angle - phi_p, azimuthal_angle + phi_p] + detunings = [0] * 5 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + + +def new_corpse_in_scrofulous_control( # pylint: disable=invalid-name + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + CORPSE (Compensating for Off Resonance with a Pulse SEquence) embedded within a + SCROFULOUS (Short Composite ROtation For Undoing Length Over and Under Shoot) control, + also knowns as CinS. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControl + The driven control. + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + # Create a lookup table for rabi rotation and phase angles, taken from + # the Cummings paper. Note: values in the paper are in degrees. + def degrees_to_radians(angle_in_degrees): + return angle_in_degrees / 180 * np.pi + + if np.isclose(rabi_rotation, np.pi): + theta_1 = theta_3 = degrees_to_radians(180.) + phi_1 = phi_3 = np.arccos( + -np.pi * np.cos(theta_1) / 2 / theta_1 / np.sin(rabi_rotation / 2) + ) + phi_2 = phi_1 - np.arccos(- np.pi / 2 / theta_1) + elif np.isclose(rabi_rotation, 0.5 * np.pi): + theta_1 = theta_3 = degrees_to_radians(115.2) + phi_1 = phi_3 = np.arccos( + -np.pi * np.cos(theta_1) / 2 / theta_1 / np.sin(rabi_rotation / 2) + ) + phi_2 = phi_1 - np.arccos(- np.pi / 2 / theta_1) + elif np.isclose(rabi_rotation, 0.25 * np.pi): + theta_1 = theta_3 = degrees_to_radians(96.7) + phi_1 = phi_3 = np.arccos( + -np.pi * np.cos(theta_1) / 2 / theta_1 / np.sin(rabi_rotation / 2) + ) + phi_2 = phi_1 - np.arccos(- np.pi / 2 / theta_1) + else: + raise ArgumentsValueError( + 'rabi_rotation angle must be either pi, pi/2 or pi/4', + {'rabi_rotation': rabi_rotation}) + + theta_2 = np.pi + + total_angles = [] + # Loop over all SCROFULOUS Rabi rotations (theta) and azimuthal angles (phi) + # And make CORPSEs with those. + for theta, phi in zip([theta_1, theta_2, theta_3], [phi_1, phi_2, phi_3]): + k = np.arcsin(np.sin(theta / 2.) / 2.) + angles = np.array([ + [2. * np.pi + theta / 2. - k, phi + azimuthal_angle], + [2. * np.pi - 2. * k, np.pi + phi + azimuthal_angle], + [theta / 2. - k, phi + azimuthal_angle]]) + total_angles.append(angles) + + total_angles = np.vstack(total_angles) + + rabi_rotations = total_angles[:, 0] + + rabi_rates = [maximum_rabi_rate] * 9 + azimuthal_angles = total_angles[:, 1] + detunings = [0] * 9 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) + + +def new_walsh_amplitude_modulated_filter_1_control( # pylint: disable=invalid-name + rabi_rotation=None, + azimuthal_angle=0., + maximum_rabi_rate=2. * np.pi, + **kwargs): + """ + First order Walsh control with amplitude modulation. + + Parameters + ---------- + rabi_rotation : float, optional + The total rabi rotation to be performed by the control. + maximum_rabi_rate : float, optional + Defaults to 2.*np.pi + The maximum rabi frequency for the control. + azimuthal_angle : float, optional + The azimuthal position of the control. + kwargs : dict + Other keywords required to make a qctrlopencontrols.DrivenControls. + + Returns + ------- + qctrlopencontrols.DrivenControls + The driven control. + + Raises + ------ + ArgumentsValueError + Raised when an argument is invalid. + """ + (maximum_rabi_rate, rabi_rotation, azimuthal_angle) = _predefined_common_attributes( + maximum_rabi_rate, rabi_rotation, azimuthal_angle) + + if np.isclose(rabi_rotation, np.pi): + theta_plus = np.pi + theta_minus = np.pi / 2. + elif np.isclose(rabi_rotation, 0.5 * np.pi): + theta_plus = np.pi * (2.5 + 0.65667825) / 4. + theta_minus = np.pi * (2.5 - 0.65667825) / 4. + elif np.isclose(rabi_rotation, 0.25 * np.pi): + theta_plus = np.pi * (2.25 + 0.36256159) / 4. + theta_minus = np.pi * (2.25 - 0.36256159) / 4. + else: + raise ArgumentsValueError( + 'rabi_rotation angle must be either pi, pi/2 or pi/4', + {'rabi_rotation': rabi_rotation}) + + rabi_rotations = [theta_plus, theta_minus, theta_minus, theta_plus] + + rabi_rates = [maximum_rabi_rate] * 4 + azimuthal_angles = [azimuthal_angle] * 4 + detunings = [0] * 4 + durations = [rabi_rotation_ / maximum_rabi_rate for rabi_rotation_ in rabi_rotations] + + return DrivenControl( + rabi_rates=rabi_rates, + azimuthal_angles=azimuthal_angles, + detunings=detunings, + durations=durations, + **kwargs) diff --git a/qctrlopencontrols/dynamic_decoupling_sequences/driven_controls.py b/qctrlopencontrols/dynamic_decoupling_sequences/driven_controls.py index cfcf3c7e..efec0122 100644 --- a/qctrlopencontrols/dynamic_decoupling_sequences/driven_controls.py +++ b/qctrlopencontrols/dynamic_decoupling_sequences/driven_controls.py @@ -22,7 +22,7 @@ from qctrlopencontrols.exceptions import ArgumentsValueError from qctrlopencontrols.driven_controls import ( - UPPER_BOUND_RABI_RATE, UPPER_BOUND_DETUNING_RATE, DrivenControls) + UPPER_BOUND_RABI_RATE, UPPER_BOUND_DETUNING_RATE, DrivenControl) def _check_valid_operation(rabi_rotations, detuning_rotations): @@ -258,36 +258,50 @@ def convert_dds_to_driven_controls( if np.allclose(pulse_start_ends, 0.0): # the original sequence should be a free evolution - control_segments = np.reshape( - np.array([0., 0., 0., sequence_duration]), (1, 4)) - return DrivenControls(segments=control_segments, **kwargs) + return DrivenControl(rabi_rates=[0.], + azimuthal_angles=[0.], + detunings=[0.], + durations=[sequence_duration], + **kwargs) + + control_rabi_rates = np.zeros((operations.shape[1]*2,)) + control_azimuthal_angles = np.zeros((operations.shape[1] * 2,)) + control_detunings = np.zeros((operations.shape[1] * 2,)) + control_durations = np.zeros((operations.shape[1] * 2,)) - control_segments = np.zeros((operations.shape[1]*2, 4)) pulse_segment_idx = 0 for op_idx in range(0, operations.shape[1]): if operations[3, op_idx] == 0.0: - control_segments[pulse_segment_idx, 0:3] = [ - maximum_rabi_rate * np.cos(operations[2, op_idx]), - maximum_rabi_rate * np.sin(operations[2, op_idx]), - 0.] - control_segments[pulse_segment_idx, 3] = (pulse_start_ends[op_idx, 1] - - pulse_start_ends[op_idx, 0]) + control_rabi_rates[pulse_segment_idx] = maximum_rabi_rate + control_azimuthal_angles[pulse_segment_idx] = operations[2, op_idx] + control_durations[pulse_segment_idx] = (pulse_start_ends[op_idx, 1] - + pulse_start_ends[op_idx, 0]) else: - control_segments[pulse_segment_idx, 0:3] = [0., 0., operations[3, op_idx]] - control_segments[pulse_segment_idx, 3] = (pulse_start_ends[op_idx, 1] - - pulse_start_ends[op_idx, 0]) + control_detunings[pulse_segment_idx] = operations[3, op_idx] + control_durations[pulse_segment_idx] = (pulse_start_ends[op_idx, 1] - + pulse_start_ends[op_idx, 0]) if op_idx != (operations.shape[1]-1): - control_segments[pulse_segment_idx+1, 0:3] = np.array([0, 0, 0]) - control_segments[pulse_segment_idx+1, 3] = (pulse_start_ends[op_idx+1, 0] - - pulse_start_ends[op_idx, 1]) + control_rabi_rates[pulse_segment_idx+1] = 0. + control_azimuthal_angles[pulse_segment_idx+1] = 0. + control_detunings[pulse_segment_idx+1] = 0. + control_durations[pulse_segment_idx+1] = (pulse_start_ends[op_idx+1, 0] - + pulse_start_ends[op_idx, 1]) + pulse_segment_idx += 2 # almost there; let us check if there is any segments with durations = 0 - segment_durations = control_segments[:, 3] - control_segments = control_segments[segment_durations != 0] - return DrivenControls(segments=control_segments, **kwargs) + control_rabi_rates = control_rabi_rates[control_durations > 0.] + control_azimuthal_angles = control_azimuthal_angles[control_durations > 0.] + control_detunings = control_detunings[control_durations > 0.] + control_durations = control_durations[control_durations > 0.] + + return DrivenControl(rabi_rates=control_rabi_rates, + azimuthal_angles=control_azimuthal_angles, + detunings=control_detunings, + durations=control_durations, + **kwargs) if __name__ == '__main__': diff --git a/qctrlopencontrols/globals/__init__.py b/qctrlopencontrols/globals/__init__.py index 3e07bab5..78ccdc81 100644 --- a/qctrlopencontrols/globals/__init__.py +++ b/qctrlopencontrols/globals/__init__.py @@ -37,5 +37,5 @@ """ CYLINDRICAL = 'cylindrical' -"""Defined Cylindrical coordinate system +"""Defines Cylindrical coordinate system """ diff --git a/tests/test_driven_controls.py b/tests/test_driven_controls.py index 4c148c84..3782c495 100644 --- a/tests/test_driven_controls.py +++ b/tests/test_driven_controls.py @@ -22,7 +22,10 @@ import pytest from qctrlopencontrols.exceptions import ArgumentsValueError -from qctrlopencontrols import DrivenControls +from qctrlopencontrols import DrivenControl + +from qctrlopencontrols.driven_controls.constants import ( + UPPER_BOUND_SEGMENTS, UPPER_BOUND_RABI_RATE, UPPER_BOUND_DETUNING_RATE) def _remove_file(filename): @@ -40,43 +43,110 @@ def test_driven_controls(): """Tests the construction of driven controls """ - _segments = [[np.pi, 0., 0., 1.], - [np.pi, np.pi/2, 0., 2.], - [0., 0., np.pi, 3.]] + _rabi_rates = [np.pi, np.pi, 0] + _azimuthal_angles = [np.pi/2, 0, -np.pi] + _detunings = [0, 0, 0] + _durations = [1, 2, 3] _name = 'driven_control' - driven_control = DrivenControls( - segments=_segments, name=_name) + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations, + name=_name) - assert np.allclose(driven_control.segments, _segments) - assert driven_control.number_of_segments == 3 - assert np.allclose(driven_control.segment_durations, np.array( - [1., 2., 3.])) + assert np.allclose(driven_control.rabi_rates, _rabi_rates) + assert np.allclose(driven_control.durations, _durations) + assert np.allclose(driven_control.detunings, _detunings) + assert np.allclose(driven_control.azimuthal_angles, _azimuthal_angles) assert driven_control.name == _name - with pytest.raises(ArgumentsValueError): + driven_control = DrivenControl( + rabi_rates=None, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations, + name=_name) + + assert np.allclose(driven_control.rabi_rates, np.array([0., 0., 0.])) + assert np.allclose(driven_control.durations, _durations) + assert np.allclose(driven_control.detunings, _detunings) + assert np.allclose(driven_control.azimuthal_angles, _azimuthal_angles) + + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=None, + detunings=_detunings, + durations=_durations, + name=_name) + + assert np.allclose(driven_control.rabi_rates, _rabi_rates) + assert np.allclose(driven_control.durations, _durations) + assert np.allclose(driven_control.detunings, _detunings) + assert np.allclose(driven_control.azimuthal_angles, np.array([0., 0., 0.])) - _ = DrivenControls(segments=[[1e12, 0., 3, 1.]]) - _ = DrivenControls(segments=[[3., 0., 1e12, 1.]]) - _ = DrivenControls(segments=[[3., 0., 1e12, -1.]]) - _ = DrivenControls(segments=[[0., 0., 0., 0.]]) + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=None, + durations=_durations, + name=_name) + assert np.allclose(driven_control.rabi_rates, _rabi_rates) + assert np.allclose(driven_control.durations, _durations) + assert np.allclose(driven_control.detunings, np.array([0., 0., 0.])) + assert np.allclose(driven_control.azimuthal_angles, _azimuthal_angles) + + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=None, + name=_name) + + assert np.allclose(driven_control.rabi_rates, _rabi_rates) + assert np.allclose(driven_control.durations, np.array([1., 1., 1.])) + assert np.allclose(driven_control.detunings, _detunings) + assert np.allclose(driven_control.azimuthal_angles, _azimuthal_angles) + + driven_control = DrivenControl() + assert np.allclose(driven_control.rabi_rates, np.array([np.pi])) + assert np.allclose(driven_control.durations, np.array([1.])) + assert np.allclose(driven_control.detunings, np.array([0.])) + assert np.allclose(driven_control.azimuthal_angles, np.array([0.])) + + with pytest.raises(ArgumentsValueError): + _ = DrivenControl(rabi_rates=[-1]) + _ = DrivenControl(detunings=[-1]) + _ = DrivenControl(durations=[0]) + _ = DrivenControl(rabi_rates=[1.1 * UPPER_BOUND_RABI_RATE]) + _ = DrivenControl(detunings=[1.1 * UPPER_BOUND_DETUNING_RATE]) + _ = DrivenControl(rabi_rates=[1] * UPPER_BOUND_SEGMENTS + [1]) + _ = DrivenControl() + _ = DrivenControl(rabi_rates=[1, 2], azimuthal_angles=[1, 2, 3], + detunings=None, durations=None) def test_control_export(): """Tests exporting the control to a file """ + _rabi_rates = [5 * np.pi, 4 * np.pi, 3 * np.pi] + _azimuthal_angles = [np.pi / 4, np.pi / 3, 0] + _detunings = [0, 0, np.pi] + _durations = [2, 2, 1] - _maximum_rabi_rate = 5*np.pi - _segments = [[_maximum_rabi_rate*np.cos(np.pi/4), _maximum_rabi_rate*np.sin(np.pi/4), 0., 2.], - [_maximum_rabi_rate*np.cos(np.pi/3), _maximum_rabi_rate*np.sin(np.pi/3), 0., 2.], - [0., 0., np.pi, 1.]] - _name = 'driven_controls' + _name = 'driven_control' - driven_control = DrivenControls( - segments=_segments, name=_name) + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations, + name=_name + ) _filename = 'driven_control_qctrl_cylindrical.csv' driven_control.export_to_file( @@ -110,3 +180,156 @@ def test_control_export(): _remove_file('driven_control_qctrl_cartesian.csv') _remove_file('driven_control_qctrl_cylindrical.json') _remove_file('driven_control_qctrl_cartesian.json') + +def test_plot_data(): + """ + Test the plot data produced for a driven control. + """ + _rabi_rates = [np.pi, 2 * np.pi, np.pi] + _azimuthal_angles = [0, np.pi/2, -np.pi/2] + _detunings = [0, 1, 0] + _durations = [1, 1.25, 1.5] + + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations + ) + + x_amplitude = [0., np.pi, np.pi, 0., 0., 0., 0., 0.] + y_amplitude = [0., 0., 0., 2*np.pi, 2*np.pi, -np.pi, -np.pi, 0.] + z_amplitude = [0., 0., 0., 1., 1., 0., 0., 0.] + times = [0., 0., 1., 1., 2.25, 2.25, 3.75, 3.75] + + plot_data = driven_control.get_plot_formatted_arrays( + dimensionless_rabi_rate=False, coordinates='cartesian' + ) + + assert np.allclose(plot_data['times'], times) + assert np.allclose(plot_data['amplitudes_x'], x_amplitude) + assert np.allclose(plot_data['amplitudes_y'], y_amplitude) + assert np.allclose(plot_data['detunings'], z_amplitude) + +def test_pretty_print(): + + """Tests pretty output of driven control + """ + + _maximum_rabi_rate = 2*np.pi + _maximum_detuning = 1.0 + _rabi_rates = [np.pi, 2 * np.pi, np.pi] + _azimuthal_angles = [0, np.pi / 2, -np.pi / 2] + _detunings = [0, 1, 0] + _durations = [1., 1., 1.] + + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations + ) + + _pretty_rabi_rates = [str(_rabi_rate/_maximum_rabi_rate) + for _rabi_rate in _rabi_rates] + _pretty_azimuthal_angles = [str(azimuthal_angle/np.pi) + for azimuthal_angle in _azimuthal_angles] + _pretty_detunings = [str(detuning/_maximum_detuning) + for detuning in _detunings] + _pretty_durations = [str(duration/3.) for duration in _durations] + _pretty_rabi_rates = ','.join(_pretty_rabi_rates) + _pretty_azimuthal_angles = ','.join(_pretty_azimuthal_angles) + _pretty_detunings = ','.join(_pretty_detunings) + _pretty_durations = ','.join(_pretty_durations) + + _pretty_string = [] + _pretty_string.append('Rabi Rates = [{}] x {}'.format( + _pretty_rabi_rates, _maximum_rabi_rate)) + _pretty_string.append('Azimuthal Angles = [{}] x pi'.format( + _pretty_azimuthal_angles)) + _pretty_string.append('Detunings = [{}] x {}'.format( + _pretty_detunings, _maximum_detuning)) + _pretty_string.append('Durations = [{}] x 3.0'.format( + _pretty_durations)) + + _pretty_string = '\n'.join(_pretty_string) + + assert str(driven_control) == _pretty_string + + _maximum_rabi_rate = 0. + _maximum_detuning = 1.0 + _rabi_rates = [0., 0., 0.] + _azimuthal_angles = [0, np.pi / 2, -np.pi / 2] + _detunings = [0, 1, 0] + _durations = [1., 1., 1.] + + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations + ) + + _pretty_rabi_rates = ['0', '0', '0'] + _pretty_azimuthal_angles = [str(azimuthal_angle / np.pi) + for azimuthal_angle in _azimuthal_angles] + _pretty_detunings = [str(detuning / _maximum_detuning) + for detuning in _detunings] + _pretty_durations = [str(duration / 3.) for duration in _durations] + _pretty_rabi_rates = ','.join(_pretty_rabi_rates) + _pretty_azimuthal_angles = ','.join(_pretty_azimuthal_angles) + _pretty_detunings = ','.join(_pretty_detunings) + _pretty_durations = ','.join(_pretty_durations) + + _pretty_string = [] + _pretty_string.append('Rabi Rates = [{}] x {}'.format( + _pretty_rabi_rates, _maximum_rabi_rate)) + _pretty_string.append('Azimuthal Angles = [{}] x pi'.format( + _pretty_azimuthal_angles)) + _pretty_string.append('Detunings = [{}] x {}'.format( + _pretty_detunings, _maximum_detuning)) + _pretty_string.append('Durations = [{}] x 3.0'.format( + _pretty_durations)) + + _pretty_string = '\n'.join(_pretty_string) + + assert str(driven_control) == _pretty_string + + _maximum_rabi_rate = 2 * np.pi + _maximum_detuning = 0. + _rabi_rates = [np.pi, 2 * np.pi, np.pi] + _azimuthal_angles = [0, np.pi / 2, -np.pi / 2] + _detunings = [0, 0., 0] + _durations = [1., 1., 1.] + + driven_control = DrivenControl( + rabi_rates=_rabi_rates, + azimuthal_angles=_azimuthal_angles, + detunings=_detunings, + durations=_durations + ) + + _pretty_rabi_rates = [str(_rabi_rate / _maximum_rabi_rate) + for _rabi_rate in _rabi_rates] + _pretty_azimuthal_angles = [str(azimuthal_angle / np.pi) + for azimuthal_angle in _azimuthal_angles] + _pretty_detunings = ['0', '0', '0'] + _pretty_durations = [str(duration / 3.) for duration in _durations] + _pretty_rabi_rates = ','.join(_pretty_rabi_rates) + _pretty_azimuthal_angles = ','.join(_pretty_azimuthal_angles) + _pretty_detunings = ','.join(_pretty_detunings) + _pretty_durations = ','.join(_pretty_durations) + + _pretty_string = [] + _pretty_string.append('Rabi Rates = [{}] x {}'.format( + _pretty_rabi_rates, _maximum_rabi_rate)) + _pretty_string.append('Azimuthal Angles = [{}] x pi'.format( + _pretty_azimuthal_angles)) + _pretty_string.append('Detunings = [{}] x {}'.format( + _pretty_detunings, _maximum_detuning)) + _pretty_string.append('Durations = [{}] x 3.0'.format( + _pretty_durations)) + + _pretty_string = '\n'.join(_pretty_string) + + assert str(driven_control) == _pretty_string diff --git a/tests/test_dynamical_decoupling.py b/tests/test_dynamical_decoupling.py index 9479d20d..da785404 100644 --- a/tests/test_dynamical_decoupling.py +++ b/tests/test_dynamical_decoupling.py @@ -387,24 +387,18 @@ def test_conversion_to_driven_controls(): maximum_rabi_rate=_maximum_rabi_rate, maximum_detuning_rate=_maximum_detuning_rate, name=_name) - assert np.sum(driven_control.segments[:, 3]) == _duration - assert np.allclose(driven_control.segments[:, 0], np.array( - [0., _maximum_rabi_rate*np.cos(_azimuthal_angles[1]), 0., 0., 0., - _maximum_rabi_rate*np.cos(_azimuthal_angles[3]), 0.])) - assert np.allclose(driven_control.segments[:, 1], np.array( - [0., _maximum_rabi_rate*np.sin(_azimuthal_angles[1]), 0., 0., 0., 0., 0])) - assert np.allclose(driven_control.segments[:, 2], np.array( + assert np.sum(driven_control.durations) == _duration + assert np.allclose(driven_control.rabi_rates, np.array( + [0., _maximum_rabi_rate, 0., 0., 0., + _maximum_rabi_rate, 0.])) + assert np.allclose(driven_control.azimuthal_angles, np.array( + [0., _azimuthal_angles[1], 0., 0., 0., + _azimuthal_angles[3], 0.])) + assert np.allclose(driven_control.detunings, np.array( [0., 0., 0., np.pi, 0., 0., 0])) - assert np.allclose(driven_control.segments[:, 3], np.array( + assert np.allclose(driven_control.durations, np.array( [4.75e-1, 5e-2, 4.5e-1, 5e-2, 4.5e-1, 5e-2, 4.75e-1])) - _duration = 2. - _offsets = 2 * np.array([0., 0.25, 0.5, 0.75, 1.]) - _rabi_rotations = np.array([0., np.pi, 0., np.pi, 0.]) - _azimuthal_angles = np.array([0., np.pi / 2, 0., 0., 0.]) - _detuning_rotations = np.array([0., 0., np.pi, 0., 0.]) - _name = 'test_sequence' - def test_free_evolution_conversion(): @@ -433,8 +427,14 @@ def test_free_evolution_conversion(): maximum_detuning_rate=_maximum_detuning_rate, name=_name) - _segments = np.reshape(np.array([0., 0., 0., _duration]), (1, 4)) - assert np.allclose(driven_control.segments, _segments) + _rabi_rates = np.array([0.]) + _azimuthal_angles = np.array([0.]) + _detunings = np.array([0.]) + _durations = np.array([_duration]) + assert np.allclose(driven_control.rabi_rates, _rabi_rates) + assert np.allclose(driven_control.azimuthal_angles, _azimuthal_angles) + assert np.allclose(driven_control.detunings, _detunings) + assert np.allclose(driven_control.durations, _durations) def test_export_to_file(): @@ -501,5 +501,6 @@ def test_export_to_file(): _remove_file('dds_qctrl_cartesian.json') + if __name__ == '__main__': pass diff --git a/tests/test_predefined_driven_controls.py b/tests/test_predefined_driven_controls.py new file mode 100644 index 00000000..11b440cf --- /dev/null +++ b/tests/test_predefined_driven_controls.py @@ -0,0 +1,464 @@ +# Copyright 2019 Q-CTRL Pty Ltd & Q-CTRL Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +==================================== +Tests for Predefined Driven Controls +==================================== +""" +import numpy as np +import pytest + +from qctrlopencontrols.exceptions import ArgumentsValueError + +from qctrlopencontrols.driven_controls import ( + new_predefined_driven_control, + new_primitive_control, new_wimperis_1_control, new_solovay_kitaev_1_control, + PRIMITIVE, BB1, SK1, CORPSE, + new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control, + new_corpse_in_scrofulous_control, + new_compensating_for_off_resonance_with_a_pulse_sequence_control, + new_compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev_control, + new_compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis_control, + new_walsh_amplitude_modulated_filter_1_control +) + + +def test_new_predefined_driven_control(): + """Test the new_predefined_driven_control function in + qctrlopencontrols.driven_controls.predefined + """ + # Test that an error is raised if supplied with an unknown scheme + with pytest.raises(ArgumentsValueError): + _ = new_predefined_driven_control(scheme='nil') + +def test_predefined_common_attributes(): + """Test that expected exceptions are raised correctly for invalid parameters + """ + # Test negative maximum Rabi rate + with pytest.raises(ArgumentsValueError): + _ = new_predefined_driven_control( + maximum_rabi_rate=-1, shape='PRIMITIVE', rabi_rotation=1, azimuthal_angle=0) + # Test zero Rabi rotation + with pytest.raises(ArgumentsValueError): + _ = new_predefined_driven_control( + maximum_rabi_rate=1, shape='PRIMITIVE', rabi_rotation=0, azimuthal_angle=0) + + +def test_primitive_control_segments(): + """Test the segments of the predefined primitive driven control + """ + _rabi_rate = 1 + _rabi_rotation = 1.5 + _azimuthal_angle = np.pi/2 + _segments = [ + np.cos(_azimuthal_angle), + np.sin(_azimuthal_angle), + 0., + _rabi_rotation + ] + + primitive_control_1 = new_primitive_control( + rabi_rotation=_rabi_rotation, + maximum_rabi_rate=_rabi_rate, + azimuthal_angle=_azimuthal_angle + ) + + # Test the new_predefined_driven_control function also + primitive_control_2 = new_predefined_driven_control( + rabi_rotation=_rabi_rotation, + maximum_rabi_rate=_rabi_rate, + azimuthal_angle=_azimuthal_angle, + scheme=PRIMITIVE + ) + + for control in [primitive_control_1, primitive_control_2]: + segments = [ + control.amplitude_x[0], + control.amplitude_y[0], + control.detunings[0], + control.durations[0] + ] + assert np.allclose(_segments, segments) + assert np.allclose(_rabi_rate, control.maximum_rabi_rate) + + +def test_wimperis_1_control(): + """Test the segments of the Wimperis 1 (BB1) driven control + """ + _rabi_rotation = np.pi + _azimuthal_angle = np.pi/2 + _maximum_rabi_rate = 1 + + phi_p = np.arccos(-_rabi_rotation / (4 * np.pi)) + + _segments = np.array([ + [np.cos(_azimuthal_angle), np.sin(_azimuthal_angle)], + [np.cos(phi_p + _azimuthal_angle), np.sin(phi_p + _azimuthal_angle)], + [np.cos(3. * phi_p + _azimuthal_angle), np.sin(3. * phi_p + _azimuthal_angle)], + [np.cos(phi_p + _azimuthal_angle), np.sin(phi_p + _azimuthal_angle)] + ]) + + wimperis_control_1 = new_wimperis_1_control( + rabi_rotation=_rabi_rotation, + azimuthal_angle=_azimuthal_angle, + maximum_rabi_rate=_maximum_rabi_rate + ) + wimperis_control_2 = new_predefined_driven_control( + rabi_rotation=_rabi_rotation, + azimuthal_angle=_azimuthal_angle, + maximum_rabi_rate=_maximum_rabi_rate, + scheme=BB1 + ) + + durations = [np.pi, np.pi, np.pi * 2, np.pi] + + for control in [wimperis_control_1, wimperis_control_2]: + segments = np.vstack(( + control.amplitude_x, control.amplitude_y + )).T + assert np.allclose(segments, _segments) + assert np.allclose(control.durations, durations) + assert np.allclose(control.detunings, 0) + +def test_solovay_kitaev_1_control(): + """Test the segments of the Solovay-Kitaev 1 (SK1) driven control + """ + _rabi_rotation = np.pi + _azimuthal_angle = np.pi/2 + + phi_p = np.arccos(-_rabi_rotation / (4 * np.pi)) + + _segments = [ + [np.cos(_azimuthal_angle), + np.sin(_azimuthal_angle)], + [np.cos(-phi_p + _azimuthal_angle), + np.sin(-phi_p + _azimuthal_angle)], + [np.cos(phi_p + _azimuthal_angle), + np.sin(phi_p + _azimuthal_angle)] + ] + + sk1_control_1 = new_solovay_kitaev_1_control( + rabi_rotation=_rabi_rotation, + azimuthal_angle=_azimuthal_angle, + maximum_rabi_rate=1 + ) + + sk1_control_2 = new_predefined_driven_control( + scheme=SK1, + rabi_rotation=_rabi_rotation, + azimuthal_angle=_azimuthal_angle, + maximum_rabi_rate=1 + ) + + durations = [np.pi, 2 * np.pi, 2 * np.pi] + + for control in [sk1_control_1, sk1_control_2]: + segments = np.vstack(( + control.amplitude_x, control.amplitude_y + )).T + assert np.allclose(segments, _segments) + assert np.allclose(control.durations, durations) + assert np.allclose(control.detunings, 0) + +def test_scofulous_control(): + """Test the segments of the SCROFULOUS driven control. + Note: here we test against numerical pulse segments since the angles are + defined numerically as well. + """ + # Test that exceptions are raised upon wrong inputs for rabi_rotation + # (SCROFULOUS is only defined for pi/4, pi/2 and pi pulses) + with pytest.raises(ArgumentsValueError): + _ = new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control( + rabi_rotation=0.3 + ) + + # Construct SCROFULOUS controls for target rotations pi/4, pi/2 and pi + scrofulous_pi = new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control( + rabi_rotation=np.pi, azimuthal_angle=0.5, maximum_rabi_rate=2*np.pi + ) + + pi_segments = np.vstack(( + scrofulous_pi.amplitude_x, scrofulous_pi.amplitude_y, scrofulous_pi.detunings, + scrofulous_pi.durations + )).T + + _pi_segments = np.array([ + [0.14826172, 6.28143583, 0., 0.5], + [5.36575214, -3.26911633, 0., 0.5], + [0.14826172, 6.28143583, 0., 0.5]]) + + assert np.allclose(pi_segments, _pi_segments) + + scrofulous_pi2 = new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control( + rabi_rotation=np.pi/2, azimuthal_angle=-0.5, maximum_rabi_rate=2*np.pi + ) + + pi_on_2_segments = np.vstack(( + scrofulous_pi2.amplitude_x, scrofulous_pi2.amplitude_y, scrofulous_pi2.detunings, + scrofulous_pi2.durations + )).T + + _pi_on_2_segments = np.array([ + [5.25211762, 3.44872124, 0., 0.32], + [-1.95046211, -5.97278119, 0., 0.5], + [5.25211762, 3.44872124, 0., 0.32]]) + + assert np.allclose(pi_on_2_segments, _pi_on_2_segments) + + scrofulous_pi4 = new_short_composite_rotation_for_undoing_length_over_and_under_shoot_control( + rabi_rotation=np.pi/4, azimuthal_angle=0, maximum_rabi_rate=2*np.pi + ) + + pi_on_4_segments = np.vstack(( + scrofulous_pi4.amplitude_x, scrofulous_pi4.amplitude_y, scrofulous_pi4.detunings, + scrofulous_pi4.durations + )).T + + _pi_on_4_segments = np.array([ + [1.78286387, 6.0249327, 0., 0.26861111], + [0.54427724, -6.25956707, 0., 0.5], + [1.78286387, 6.0249327, 0., 0.26861111]]) + + assert np.allclose(pi_on_4_segments, _pi_on_4_segments) + +def test_corpse_in_scrofulous_control(): + """Test the segments of the CORPSE in SCROFULOUS driven control. + Note: here we test against numerical pulse segments since the SCROFULOUS angles are + defined numerically as well. + """ + # Test pi and pi/2 rotations + cs_pi = new_corpse_in_scrofulous_control( + rabi_rotation=np.pi, azimuthal_angle=0.5, maximum_rabi_rate=2*np.pi + ) + + pi_segments = np.vstack(( + cs_pi.amplitude_x, cs_pi.amplitude_y, cs_pi.detunings, + cs_pi.durations + )).T + + _pi_segments = np.array([ + [0.14826172, 6.28143583, 0., 1.16666667], + [-0.14826172, -6.28143583, 0., 0.83333333], + [0.14826172, 6.28143583, 0., 0.16666667], + [5.36575214, -3.26911633, 0., 1.16666667], + [-5.36575214, 3.26911633, 0., 0.83333333], + [5.36575214, -3.26911633, 0., 0.16666667], + [0.14826172, 6.28143583, 0., 1.16666667], + [-0.14826172, -6.28143583, 0., 0.83333333], + [0.14826172, 6.28143583, 0., 0.16666667]]) + + assert np.allclose(pi_segments, _pi_segments) + + cs_pi_on_2 = new_corpse_in_scrofulous_control( + rabi_rotation=np.pi/2, azimuthal_angle=0.25, maximum_rabi_rate=np.pi + ) + + pi_on_2_segments = np.vstack(( + cs_pi_on_2.amplitude_x, cs_pi_on_2.amplitude_y, cs_pi_on_2.detunings, + cs_pi_on_2.durations + )).T + + _pi_on_2_segments = np.array([ + [0.74606697, 3.05171894, 0., 2.18127065], + [-0.74606697, -3.05171894, 0., 1.7225413], + [0.74606697, 3.05171894, 0., 0.18127065], + [1.32207387, -2.84986405, 0., 2.33333333], + [-1.32207387, 2.84986405, 0., 1.66666667], + [1.32207387, -2.84986405, 0., 0.33333333], + [0.74606697, 3.05171894, 0., 2.18127065], + [-0.74606697, -3.05171894, 0., 1.7225413], + [0.74606697, 3.05171894, 0., 0.18127065]]) + + assert np.allclose(pi_on_2_segments, _pi_on_2_segments) + + +def test_corpse_control(): + """Test the segments of the CORPSE driven control + """ + _rabi_rotation = np.pi + _azimuthal_angle = np.pi/4 + + k = np.arcsin(np.sin(_rabi_rotation / 2.) / 2.) + + _segments = [ + [np.cos(_azimuthal_angle), np.sin(_azimuthal_angle), 0., + 2. * np.pi + _rabi_rotation / 2. - k], + [np.cos(np.pi + _azimuthal_angle), np.sin(np.pi + _azimuthal_angle), 0., + 2. * np.pi - 2. * k], + [np.cos(_azimuthal_angle), np.sin(_azimuthal_angle), 0., _rabi_rotation / 2. - k] + ] + + corpse_control_1 = new_compensating_for_off_resonance_with_a_pulse_sequence_control( + rabi_rotation=_rabi_rotation, + azimuthal_angle=_azimuthal_angle, + maximum_rabi_rate=1 + ) + + corpse_control_2 = new_predefined_driven_control( + scheme=CORPSE, + rabi_rotation=_rabi_rotation, + azimuthal_angle=_azimuthal_angle, + maximum_rabi_rate=1 + ) + + for control in [corpse_control_1, corpse_control_2]: + segments = np.vstack(( + control.amplitude_x, control.amplitude_y, control.detunings, control.durations + )).T + assert np.allclose(segments, _segments) + + +def test_cinbb_control(): + """Test the segments of the CinBB (BB1 made up of CORPSEs) driven control + """ + cinbb = new_compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis_control( + rabi_rotation=np.pi/3, azimuthal_angle=0.25, maximum_rabi_rate=np.pi + ) + + segments = np.vstack(( + cinbb.amplitude_x, cinbb.amplitude_y, cinbb.detunings, + cinbb.durations + )).T + + _segments = np.array([ + [3.04392815, 0.77724246, 0., 2.08623604], + [-3.04392815, -0.77724246, 0., 1.83913875], + [3.04392815, 0.77724246, 0., 0.08623604], + [-1.02819968, 2.96857033, 0., 1.], + [1.50695993, -2.75656964, 0., 2.], + [-1.02819968, 2.96857033, 0., 1.]]) + + assert np.allclose(segments, _segments) + + cinbb = new_compensating_for_off_resonance_with_a_pulse_sequence_with_wimperis_control( + rabi_rotation=np.pi/5, azimuthal_angle=-0.25, maximum_rabi_rate=np.pi + ) + + segments = np.vstack(( + cinbb.amplitude_x, cinbb.amplitude_y, cinbb.detunings, + cinbb.durations + )).T + + _segments = np.array([ + [3.04392815, -0.77724246, 0., 2.0506206], + [-3.04392815, 0.77724246, 0., 1.9012412], + [3.04392815, -0.77724246, 0., 0.0506206], + [0.62407389, 3.07898298, 0., 1.], + [-0.31344034, -3.12591739, 0., 2.], + [0.62407389, 3.07898298, 0., 1.]]) + + assert np.allclose(segments, _segments) + + +def test_cinsk1_control(): + """Test the segments of the CinSK1 (SK1 made up of CORPSEs) driven control + """ + cinsk = new_compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev_control( + rabi_rotation=np.pi/2, azimuthal_angle=0.5, maximum_rabi_rate=2*np.pi + ) + + segments = np.vstack(( + cinsk.amplitude_x, cinsk.amplitude_y, cinsk.detunings, + cinsk.durations + )).T + + _segments = np.array([ + [5.51401386, 3.0123195, 0., 1.06748664], + [-5.51401386, -3.0123195, 0., 0.88497327], + [5.51401386, 3.0123195, 0., 0.06748664], + [2.29944137, -5.84730596, 0., 1.], + [-3.67794483, 5.09422609, 0., 1.]]) + + assert np.allclose(segments, _segments) + + cinsk = new_compensating_for_off_resonance_with_a_pulse_sequence_with_solovay_kitaev_control( + rabi_rotation=2*np.pi, azimuthal_angle=-0.5, maximum_rabi_rate=2*np.pi + ) + + segments = np.vstack(( + cinsk.amplitude_x, cinsk.amplitude_y, cinsk.detunings, + cinsk.durations + )).T + + _segments = np.array([ + [5.51401386, -3.0123195, 0., 1.5], + [-5.51401386, 3.0123195, 0., 1.], + [5.51401386, -3.0123195, 0., 0.5], + [-5.36575214, -3.26911633, 0., 1.], + [-0.14826172, 6.28143583, 0., 1.]]) + + assert np.allclose(segments, _segments) + +def test_walsh_control(): + """Test the segments of the first order Walsh driven control + """ + # Test that exceptions are raised upon wrong inputs for rabi_rotation + # (WALSH control is only defined for pi/4, pi/2 and pi pulses) + with pytest.raises(ArgumentsValueError): + _ = new_walsh_amplitude_modulated_filter_1_control( + rabi_rotation=0.3 + ) + # test pi rotation + walsh_pi = new_walsh_amplitude_modulated_filter_1_control( + rabi_rotation=np.pi, azimuthal_angle=-0.35, maximum_rabi_rate=2*np.pi + ) + + pi_segments = np.vstack(( + walsh_pi.amplitude_x, walsh_pi.amplitude_y, walsh_pi.detunings, + walsh_pi.durations + )).T + + _pi_segments = np.array([ + [5.90225283, -2.15449047, 0., 0.5], + [5.90225283, -2.15449047, 0., 0.25], + [5.90225283, -2.15449047, 0., 0.25], + [5.90225283, -2.15449047, 0., 0.5]]) + + assert np.allclose(pi_segments, _pi_segments) + + # test pi/2 rotation + walsh_pi_on_2 = new_walsh_amplitude_modulated_filter_1_control( + rabi_rotation=np.pi/2, azimuthal_angle=0.57, maximum_rabi_rate=2*np.pi + ) + + pi_on_2_segments = np.vstack(( + walsh_pi_on_2.amplitude_x, walsh_pi_on_2.amplitude_y, walsh_pi_on_2.detunings, + walsh_pi_on_2.durations + )).T + + _pi_on_2_segments = np.array([ + [5.28981984, 3.39060816, 0., 0.39458478], + [5.28981984, 3.39060816, 0., 0.23041522], + [5.28981984, 3.39060816, 0., 0.23041522], + [5.28981984, 3.39060816, 0., 0.39458478]]) + + assert np.allclose(pi_on_2_segments, _pi_on_2_segments) + + # test pi/4 rotation + walsh_pi_on_4 = new_walsh_amplitude_modulated_filter_1_control( + rabi_rotation=np.pi/4, azimuthal_angle=-0.273, maximum_rabi_rate=2*np.pi + ) + pi_on_4_segments = np.vstack(( + walsh_pi_on_4.amplitude_x, walsh_pi_on_4.amplitude_y, walsh_pi_on_4.detunings, + walsh_pi_on_4.durations + )).T + + _pi_on_4_segments = np.array([ + [6.05049612, -1.69408213, 0., 0.3265702], + [6.05049612, -1.69408213, 0., 0.2359298], + [6.05049612, -1.69408213, 0., 0.2359298], + [6.05049612, -1.69408213, 0., 0.3265702]]) + + assert np.allclose(pi_on_4_segments, _pi_on_4_segments)