diff --git a/docs/time_crystals/time_crystal_circuit_generation.ipynb b/docs/time_crystals/time_crystal_circuit_generation.ipynb new file mode 100644 index 00000000..b8b7d50d --- /dev/null +++ b/docs/time_crystals/time_crystal_circuit_generation.ipynb @@ -0,0 +1,526 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "10a61b4d73a5" + }, + "outputs": [], + "source": [ + "# Copyright 2021 Google\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7763e5d20492" + }, + "source": [ + "# Time Crystal Circuit Generation\n", + "This notebook covers how Many Body Local Discrete Time Crystal circuit lists are created, from the paper: Observation of Time-Crystalline Eigenstate Order on a Quantum Processor ([Nature](https://www.nature.com/articles/s41586-021-04257-w)). \n", + "\n", + "Quantum computers and gate-based quantum circuits turn out to be well suited for crafting systems that exhibit time-crystalline behavior. Behavior is crystalline with respect to time if it has some consistent and stable pattern over time. This system's pattern must be resilient against perturbation in the same way that a space-crystalline object, like a diamond, is still a diamond if moved or heated. \n", + "\n", + "The quantum computer supplies a system of many qubits, locally connected to each other in a chain. A many-body local system like this is critical for the existence of a time crystal. Without an MBL system, it is expected that the system's state would decay into a maximum entropy state that is incompatible with the goal of stable and consistent time structure. \n", + "\n", + "The time-crystalline behavior that the DTC circuits demonstrate is perhaps the simplest kind of time-structured behavior, oscillation. Each circuit is built with some number of identical $U$-cycles. Time is represented by a circuit list where each circuit is ordered with increasingly many $U$-cycles; each cycle is a discrete time step. The eventual effect of these $U$-cycles is consistent oscillations of each qubits' polarizations. The experiments performed demonstrate that this time-crystalline oscillation behavior is stable in spite of different initial states and introduced random potentials. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f53722cb0850" + }, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "id": "0e4595827ec0" + }, + "outputs": [], + "source": [ + "!pip install cirq --pre --quiet\n", + "try:\n", + " import recirq\n", + "except ImportError:\n", + " !pip install --quiet git+https://github.com/quantumlib/ReCirq" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "e5e4f66e8e67" + }, + "outputs": [], + "source": [ + "import cirq\n", + "import recirq.time_crystals as time_crystals" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a12b700f009e" + }, + "source": [ + "## Circuit Construction\n", + "Each DTC circuit is created with symbolic parameters. Parameter values are supplied near run/simulation time with a `cirq.ParamResolver`, which means the circuit list needs to be generated only once for potentially many different experimental parameter configurations. \n", + "\n", + "The code below uses an IPython-specific utility to inspect the code of the key function that creates the symbolic circuit list, `recirq.time_crystals.symbolic_dtc_circuit_list()`. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "c0f1c1475908" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
def symbolic_dtc_circuit_list(\n",
+       "        qubits: Sequence[cirq.Qid],\n",
+       "        cycles: int\n",
+       "        ) -> List[cirq.Circuit]:\n",
+       "\n",
+       "    """ Create a list of symbolically parameterized dtc circuits, with increasing cycles\n",
+       "    Args:\n",
+       "        qubits: ordered sequence of available qubits, which are connected in a chain\n",
+       "        cycles: maximum number of cycles to generate up to\n",
+       "    Returns:\n",
+       "        list of circuits with `0, 1, 2, ... cycles` many cycles\n",
+       "    """\n",
+       "\n",
+       "    num_qubits = len(qubits)\n",
+       "\n",
+       "    # Symbol for g\n",
+       "    g_value = sp.Symbol('g')\n",
+       "\n",
+       "    # Symbols for random variance (h) and initial state, one per qubit\n",
+       "    local_fields = sp.symbols('local_field_:' + str(num_qubits))\n",
+       "    initial_state = sp.symbols('initial_state_:' + str(num_qubits))\n",
+       "\n",
+       "    # Symbols used for PhasedFsimGate, one for every qubit pair in the chain\n",
+       "    thetas = sp.symbols('theta_:' + str(num_qubits - 1))\n",
+       "    zetas = sp.symbols('zeta_:' + str(num_qubits - 1))\n",
+       "    chis = sp.symbols('chi_:' + str(num_qubits - 1))\n",
+       "    gammas = sp.symbols('gamma_:' + str(num_qubits - 1))\n",
+       "    phis = sp.symbols('phi_:' + str(num_qubits - 1))\n",
+       "\n",
+       "    # Initial moment of Y gates, conditioned on initial state\n",
+       "    initial_operations = cirq.Moment([cirq.Y(qubit) ** initial_state[index] for index, qubit in enumerate(qubits)])\n",
+       "\n",
+       "    # First component of U cycle, a moment of XZ gates.\n",
+       "    sequence_operations = []\n",
+       "    for index, qubit in enumerate(qubits):\n",
+       "        sequence_operations.append(cirq.PhasedXZGate(\n",
+       "                x_exponent=g_value, axis_phase_exponent=0.0,\n",
+       "                z_exponent=local_fields[index])(qubit))\n",
+       "\n",
+       "    # Initialize U cycle\n",
+       "    u_cycle = [cirq.Moment(sequence_operations)]\n",
+       "\n",
+       "    # Second and third components of U cycle, a chain of 2-qubit PhasedFSim gates\n",
+       "    #   The first component is all the 2-qubit PhasedFSim gates starting on even qubits\n",
+       "    #   The second component is the 2-qubit gates starting on odd qubits\n",
+       "    operation_list, other_operation_list = [],[]\n",
+       "    previous_qubit, previous_index = None, None\n",
+       "    for index, qubit in enumerate(qubits):\n",
+       "        if previous_qubit is None:\n",
+       "            previous_qubit, previous_index = qubit, index\n",
+       "            continue\n",
+       "\n",
+       "        # Add an fsim gate\n",
+       "        coupling_gate = cirq.ops.PhasedFSimGate(\n",
+       "            theta=thetas[previous_index],\n",
+       "            zeta=zetas[previous_index],\n",
+       "            chi=chis[previous_index],\n",
+       "            gamma=gammas[previous_index],\n",
+       "            phi=phis[previous_index]\n",
+       "        )\n",
+       "        operation_list.append(coupling_gate.on(previous_qubit, qubit))\n",
+       "\n",
+       "        # Swap the operation lists, to avoid two-qubit gate overlap\n",
+       "        previous_qubit, previous_index = qubit, index\n",
+       "        operation_list, other_operation_list = other_operation_list, operation_list\n",
+       "\n",
+       "    # Add the two components into the U cycle\n",
+       "    u_cycle.append(cirq.Moment(operation_list))\n",
+       "    u_cycle.append(cirq.Moment(other_operation_list))\n",
+       "\n",
+       "    # Prepare a list of circuits, with n=0,1,2,3 ... cycles many cycles\n",
+       "    circuit_list = []\n",
+       "    total_circuit = cirq.Circuit(initial_operations)\n",
+       "    circuit_list.append(total_circuit.copy())\n",
+       "    for c in range(cycles):\n",
+       "        for m in u_cycle:\n",
+       "            total_circuit.append(m)\n",
+       "        circuit_list.append(total_circuit.copy())\n",
+       "\n",
+       "    return circuit_list\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{k}{def} \\PY{n+nf}{symbolic\\PYZus{}dtc\\PYZus{}circuit\\PYZus{}list}\\PY{p}{(}\n", + " \\PY{n}{qubits}\\PY{p}{:} \\PY{n}{Sequence}\\PY{p}{[}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Qid}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n}{cycles}\\PY{p}{:} \\PY{n+nb}{int}\n", + " \\PY{p}{)} \\PY{o}{\\PYZhy{}}\\PY{o}{\\PYZgt{}} \\PY{n}{List}\\PY{p}{[}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Circuit}\\PY{p}{]}\\PY{p}{:}\n", + "\n", + " \\PY{l+s+sd}{\\PYZdq{}\\PYZdq{}\\PYZdq{} Create a list of symbolically parameterized dtc circuits, with increasing cycles}\n", + "\\PY{l+s+sd}{ Args:}\n", + "\\PY{l+s+sd}{ qubits: ordered sequence of available qubits, which are connected in a chain}\n", + "\\PY{l+s+sd}{ cycles: maximum number of cycles to generate up to}\n", + "\\PY{l+s+sd}{ Returns:}\n", + "\\PY{l+s+sd}{ list of circuits with `0, 1, 2, ... cycles` many cycles}\n", + "\\PY{l+s+sd}{ \\PYZdq{}\\PYZdq{}\\PYZdq{}}\n", + "\n", + " \\PY{n}{num\\PYZus{}qubits} \\PY{o}{=} \\PY{n+nb}{len}\\PY{p}{(}\\PY{n}{qubits}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Symbol for g}\n", + " \\PY{n}{g\\PYZus{}value} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{Symbol}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{g}\\PY{l+s+s1}{\\PYZsq{}}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Symbols for random variance (h) and initial state, one per qubit}\n", + " \\PY{n}{local\\PYZus{}fields} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{local\\PYZus{}field\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{n}{initial\\PYZus{}state} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{initial\\PYZus{}state\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits}\\PY{p}{)}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Symbols used for PhasedFsimGate, one for every qubit pair in the chain}\n", + " \\PY{n}{thetas} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{theta\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits} \\PY{o}{\\PYZhy{}} \\PY{l+m+mi}{1}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{n}{zetas} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{zeta\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits} \\PY{o}{\\PYZhy{}} \\PY{l+m+mi}{1}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{n}{chis} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{chi\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits} \\PY{o}{\\PYZhy{}} \\PY{l+m+mi}{1}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{n}{gammas} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{gamma\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits} \\PY{o}{\\PYZhy{}} \\PY{l+m+mi}{1}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{n}{phis} \\PY{o}{=} \\PY{n}{sp}\\PY{o}{.}\\PY{n}{symbols}\\PY{p}{(}\\PY{l+s+s1}{\\PYZsq{}}\\PY{l+s+s1}{phi\\PYZus{}:}\\PY{l+s+s1}{\\PYZsq{}} \\PY{o}{+} \\PY{n+nb}{str}\\PY{p}{(}\\PY{n}{num\\PYZus{}qubits} \\PY{o}{\\PYZhy{}} \\PY{l+m+mi}{1}\\PY{p}{)}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Initial moment of Y gates, conditioned on initial state}\n", + " \\PY{n}{initial\\PYZus{}operations} \\PY{o}{=} \\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Moment}\\PY{p}{(}\\PY{p}{[}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Y}\\PY{p}{(}\\PY{n}{qubit}\\PY{p}{)} \\PY{o}{*}\\PY{o}{*} \\PY{n}{initial\\PYZus{}state}\\PY{p}{[}\\PY{n}{index}\\PY{p}{]} \\PY{k}{for} \\PY{n}{index}\\PY{p}{,} \\PY{n}{qubit} \\PY{o+ow}{in} \\PY{n+nb}{enumerate}\\PY{p}{(}\\PY{n}{qubits}\\PY{p}{)}\\PY{p}{]}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} First component of U cycle, a moment of XZ gates.}\n", + " \\PY{n}{sequence\\PYZus{}operations} \\PY{o}{=} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{k}{for} \\PY{n}{index}\\PY{p}{,} \\PY{n}{qubit} \\PY{o+ow}{in} \\PY{n+nb}{enumerate}\\PY{p}{(}\\PY{n}{qubits}\\PY{p}{)}\\PY{p}{:}\n", + " \\PY{n}{sequence\\PYZus{}operations}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{PhasedXZGate}\\PY{p}{(}\n", + " \\PY{n}{x\\PYZus{}exponent}\\PY{o}{=}\\PY{n}{g\\PYZus{}value}\\PY{p}{,} \\PY{n}{axis\\PYZus{}phase\\PYZus{}exponent}\\PY{o}{=}\\PY{l+m+mf}{0.0}\\PY{p}{,}\n", + " \\PY{n}{z\\PYZus{}exponent}\\PY{o}{=}\\PY{n}{local\\PYZus{}fields}\\PY{p}{[}\\PY{n}{index}\\PY{p}{]}\\PY{p}{)}\\PY{p}{(}\\PY{n}{qubit}\\PY{p}{)}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Initialize U cycle}\n", + " \\PY{n}{u\\PYZus{}cycle} \\PY{o}{=} \\PY{p}{[}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Moment}\\PY{p}{(}\\PY{n}{sequence\\PYZus{}operations}\\PY{p}{)}\\PY{p}{]}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Second and third components of U cycle, a chain of 2\\PYZhy{}qubit PhasedFSim gates}\n", + " \\PY{c+c1}{\\PYZsh{} The first component is all the 2\\PYZhy{}qubit PhasedFSim gates starting on even qubits}\n", + " \\PY{c+c1}{\\PYZsh{} The second component is the 2\\PYZhy{}qubit gates starting on odd qubits}\n", + " \\PY{n}{operation\\PYZus{}list}\\PY{p}{,} \\PY{n}{other\\PYZus{}operation\\PYZus{}list} \\PY{o}{=} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\\PY{p}{[}\\PY{p}{]}\n", + " \\PY{n}{previous\\PYZus{}qubit}\\PY{p}{,} \\PY{n}{previous\\PYZus{}index} \\PY{o}{=} \\PY{k+kc}{None}\\PY{p}{,} \\PY{k+kc}{None}\n", + " \\PY{k}{for} \\PY{n}{index}\\PY{p}{,} \\PY{n}{qubit} \\PY{o+ow}{in} \\PY{n+nb}{enumerate}\\PY{p}{(}\\PY{n}{qubits}\\PY{p}{)}\\PY{p}{:}\n", + " \\PY{k}{if} \\PY{n}{previous\\PYZus{}qubit} \\PY{o+ow}{is} \\PY{k+kc}{None}\\PY{p}{:}\n", + " \\PY{n}{previous\\PYZus{}qubit}\\PY{p}{,} \\PY{n}{previous\\PYZus{}index} \\PY{o}{=} \\PY{n}{qubit}\\PY{p}{,} \\PY{n}{index}\n", + " \\PY{k}{continue}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Add an fsim gate}\n", + " \\PY{n}{coupling\\PYZus{}gate} \\PY{o}{=} \\PY{n}{cirq}\\PY{o}{.}\\PY{n}{ops}\\PY{o}{.}\\PY{n}{PhasedFSimGate}\\PY{p}{(}\n", + " \\PY{n}{theta}\\PY{o}{=}\\PY{n}{thetas}\\PY{p}{[}\\PY{n}{previous\\PYZus{}index}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n}{zeta}\\PY{o}{=}\\PY{n}{zetas}\\PY{p}{[}\\PY{n}{previous\\PYZus{}index}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n}{chi}\\PY{o}{=}\\PY{n}{chis}\\PY{p}{[}\\PY{n}{previous\\PYZus{}index}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n}{gamma}\\PY{o}{=}\\PY{n}{gammas}\\PY{p}{[}\\PY{n}{previous\\PYZus{}index}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n}{phi}\\PY{o}{=}\\PY{n}{phis}\\PY{p}{[}\\PY{n}{previous\\PYZus{}index}\\PY{p}{]}\n", + " \\PY{p}{)}\n", + " \\PY{n}{operation\\PYZus{}list}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{coupling\\PYZus{}gate}\\PY{o}{.}\\PY{n}{on}\\PY{p}{(}\\PY{n}{previous\\PYZus{}qubit}\\PY{p}{,} \\PY{n}{qubit}\\PY{p}{)}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Swap the operation lists, to avoid two\\PYZhy{}qubit gate overlap}\n", + " \\PY{n}{previous\\PYZus{}qubit}\\PY{p}{,} \\PY{n}{previous\\PYZus{}index} \\PY{o}{=} \\PY{n}{qubit}\\PY{p}{,} \\PY{n}{index}\n", + " \\PY{n}{operation\\PYZus{}list}\\PY{p}{,} \\PY{n}{other\\PYZus{}operation\\PYZus{}list} \\PY{o}{=} \\PY{n}{other\\PYZus{}operation\\PYZus{}list}\\PY{p}{,} \\PY{n}{operation\\PYZus{}list}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Add the two components into the U cycle}\n", + " \\PY{n}{u\\PYZus{}cycle}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Moment}\\PY{p}{(}\\PY{n}{operation\\PYZus{}list}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{n}{u\\PYZus{}cycle}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Moment}\\PY{p}{(}\\PY{n}{other\\PYZus{}operation\\PYZus{}list}\\PY{p}{)}\\PY{p}{)}\n", + "\n", + " \\PY{c+c1}{\\PYZsh{} Prepare a list of circuits, with n=0,1,2,3 ... cycles many cycles}\n", + " \\PY{n}{circuit\\PYZus{}list} \\PY{o}{=} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{n}{total\\PYZus{}circuit} \\PY{o}{=} \\PY{n}{cirq}\\PY{o}{.}\\PY{n}{Circuit}\\PY{p}{(}\\PY{n}{initial\\PYZus{}operations}\\PY{p}{)}\n", + " \\PY{n}{circuit\\PYZus{}list}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{total\\PYZus{}circuit}\\PY{o}{.}\\PY{n}{copy}\\PY{p}{(}\\PY{p}{)}\\PY{p}{)}\n", + " \\PY{k}{for} \\PY{n}{c} \\PY{o+ow}{in} \\PY{n+nb}{range}\\PY{p}{(}\\PY{n}{cycles}\\PY{p}{)}\\PY{p}{:}\n", + " \\PY{k}{for} \\PY{n}{m} \\PY{o+ow}{in} \\PY{n}{u\\PYZus{}cycle}\\PY{p}{:}\n", + " \\PY{n}{total\\PYZus{}circuit}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{m}\\PY{p}{)}\n", + " \\PY{n}{circuit\\PYZus{}list}\\PY{o}{.}\\PY{n}{append}\\PY{p}{(}\\PY{n}{total\\PYZus{}circuit}\\PY{o}{.}\\PY{n}{copy}\\PY{p}{(}\\PY{p}{)}\\PY{p}{)}\n", + "\n", + " \\PY{k}{return} \\PY{n}{circuit\\PYZus{}list}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "def symbolic_dtc_circuit_list(\n", + " qubits: Sequence[cirq.Qid],\n", + " cycles: int\n", + " ) -> List[cirq.Circuit]:\n", + "\n", + " \"\"\" Create a list of symbolically parameterized dtc circuits, with increasing cycles\n", + " Args:\n", + " qubits: ordered sequence of available qubits, which are connected in a chain\n", + " cycles: maximum number of cycles to generate up to\n", + " Returns:\n", + " list of circuits with `0, 1, 2, ... cycles` many cycles\n", + " \"\"\"\n", + "\n", + " num_qubits = len(qubits)\n", + "\n", + " # Symbol for g\n", + " g_value = sp.Symbol('g')\n", + "\n", + " # Symbols for random variance (h) and initial state, one per qubit\n", + " local_fields = sp.symbols('local_field_:' + str(num_qubits))\n", + " initial_state = sp.symbols('initial_state_:' + str(num_qubits))\n", + "\n", + " # Symbols used for PhasedFsimGate, one for every qubit pair in the chain\n", + " thetas = sp.symbols('theta_:' + str(num_qubits - 1))\n", + " zetas = sp.symbols('zeta_:' + str(num_qubits - 1))\n", + " chis = sp.symbols('chi_:' + str(num_qubits - 1))\n", + " gammas = sp.symbols('gamma_:' + str(num_qubits - 1))\n", + " phis = sp.symbols('phi_:' + str(num_qubits - 1))\n", + "\n", + " # Initial moment of Y gates, conditioned on initial state\n", + " initial_operations = cirq.Moment([cirq.Y(qubit) ** initial_state[index] for index, qubit in enumerate(qubits)])\n", + "\n", + " # First component of U cycle, a moment of XZ gates.\n", + " sequence_operations = []\n", + " for index, qubit in enumerate(qubits):\n", + " sequence_operations.append(cirq.PhasedXZGate(\n", + " x_exponent=g_value, axis_phase_exponent=0.0,\n", + " z_exponent=local_fields[index])(qubit))\n", + "\n", + " # Initialize U cycle\n", + " u_cycle = [cirq.Moment(sequence_operations)]\n", + "\n", + " # Second and third components of U cycle, a chain of 2-qubit PhasedFSim gates\n", + " # The first component is all the 2-qubit PhasedFSim gates starting on even qubits\n", + " # The second component is the 2-qubit gates starting on odd qubits\n", + " operation_list, other_operation_list = [],[]\n", + " previous_qubit, previous_index = None, None\n", + " for index, qubit in enumerate(qubits):\n", + " if previous_qubit is None:\n", + " previous_qubit, previous_index = qubit, index\n", + " continue\n", + "\n", + " # Add an fsim gate\n", + " coupling_gate = cirq.ops.PhasedFSimGate(\n", + " theta=thetas[previous_index],\n", + " zeta=zetas[previous_index],\n", + " chi=chis[previous_index],\n", + " gamma=gammas[previous_index],\n", + " phi=phis[previous_index]\n", + " )\n", + " operation_list.append(coupling_gate.on(previous_qubit, qubit))\n", + "\n", + " # Swap the operation lists, to avoid two-qubit gate overlap\n", + " previous_qubit, previous_index = qubit, index\n", + " operation_list, other_operation_list = other_operation_list, operation_list\n", + "\n", + " # Add the two components into the U cycle\n", + " u_cycle.append(cirq.Moment(operation_list))\n", + " u_cycle.append(cirq.Moment(other_operation_list))\n", + "\n", + " # Prepare a list of circuits, with n=0,1,2,3 ... cycles many cycles\n", + " circuit_list = []\n", + " total_circuit = cirq.Circuit(initial_operations)\n", + " circuit_list.append(total_circuit.copy())\n", + " for c in range(cycles):\n", + " for m in u_cycle:\n", + " total_circuit.append(m)\n", + " circuit_list.append(total_circuit.copy())\n", + "\n", + " return circuit_list" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import inspect\n", + "from IPython.display import Code\n", + "Code(inspect.getsource(time_crystals.symbolic_dtc_circuit_list), language='python')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "e13de6da1fc6" + }, + "source": [ + "The construction of each circuit is surprisingly succinct. \n", + "\n", + "The circuit expects the quantum computer to be in the all-zeros state, and starts with a sequence of `cirq.Y` gates conditioned on the provided `initial state` parameter, after initializing the necessary symbolic variables. \n", + "\n", + "Each $U$-cycle consists of three moments. First, a moment of `cirq.PhasedXZGate`s, with one for each qubit. Each `cirq.PhasedXZGate` takes the control parameter `g` as its X-exponent, and the random potentials necessary for many-body localization provided by `local_fields` for its Y-exponent.\n", + "\n", + "The second and third moments both cause the oscillation behavior and compensate for the first disorder moment. The qubits are connected in a chain, and each qubit pair connection in that chain is coupled with a `cirq.PhasedFSimGate` that uses the parameters `[theta, zetas, chi, gamma, phi]`. To keep gates from overlapping on the same qubit, this chain of gates is split into the second and third moments, such that no two gates share a qubit within each moment. \n", + "\n", + "Finally, `symbolic_dtc_circuit_list()` builds and returns a list of circuits with $0,1,2,..., cycles$ many $U$-cycles in them. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "8397a5eab5f7" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "circuit of length 1\n", + "\n", + "(0, 0): ───Y^initial_state_0───\n", + "\n", + "(0, 1): ───Y^initial_state_1───\n", + "\n", + "(0, 2): ───Y^initial_state_2───\n", + "\n", + "(0, 3): ───Y^initial_state_3───\n", + "\n", + "circuit of length 4\n", + "\n", + "(0, 0): ───Y^initial_state_0───PhXZ(a=0,x=g,z=local_field_0)────────────────────────────────────────────────────PhFSim(theta_0, zeta_0, chi_0, gamma_0, phi_0)───\n", + " │\n", + "(0, 1): ───Y^initial_state_1───PhXZ(a=0,x=g,z=local_field_1)───PhFSim(theta_1, zeta_1, chi_1, gamma_1, phi_1)───PhFSim(theta_0, zeta_0, chi_0, gamma_0, phi_0)───\n", + " │\n", + "(0, 2): ───Y^initial_state_2───PhXZ(a=0,x=g,z=local_field_2)───PhFSim(theta_1, zeta_1, chi_1, gamma_1, phi_1)───PhFSim(theta_2, zeta_2, chi_2, gamma_2, phi_2)───\n", + " │\n", + "(0, 3): ───Y^initial_state_3───PhXZ(a=0,x=g,z=local_field_3)────────────────────────────────────────────────────PhFSim(theta_2, zeta_2, chi_2, gamma_2, phi_2)───\n", + "\n", + "circuit of length 7\n", + "\n", + "(0, 0): ───Y^initial_state_0───PhXZ(a=0,x=g,z=local_field_0)────────────────────────────────────────────────────PhFSim(theta_0, zeta_0, chi_0, gamma_0, phi_0)───PhXZ(a=0,x=g,z=local_field_0)────────────────────────────────────────────────────PhFSim(theta_0, zeta_0, chi_0, gamma_0, phi_0)───\n", + " │ │\n", + "(0, 1): ───Y^initial_state_1───PhXZ(a=0,x=g,z=local_field_1)───PhFSim(theta_1, zeta_1, chi_1, gamma_1, phi_1)───PhFSim(theta_0, zeta_0, chi_0, gamma_0, phi_0)───PhXZ(a=0,x=g,z=local_field_1)───PhFSim(theta_1, zeta_1, chi_1, gamma_1, phi_1)───PhFSim(theta_0, zeta_0, chi_0, gamma_0, phi_0)───\n", + " │ │\n", + "(0, 2): ───Y^initial_state_2───PhXZ(a=0,x=g,z=local_field_2)───PhFSim(theta_1, zeta_1, chi_1, gamma_1, phi_1)───PhFSim(theta_2, zeta_2, chi_2, gamma_2, phi_2)───PhXZ(a=0,x=g,z=local_field_2)───PhFSim(theta_1, zeta_1, chi_1, gamma_1, phi_1)───PhFSim(theta_2, zeta_2, chi_2, gamma_2, phi_2)───\n", + " │ │\n", + "(0, 3): ───Y^initial_state_3───PhXZ(a=0,x=g,z=local_field_3)────────────────────────────────────────────────────PhFSim(theta_2, zeta_2, chi_2, gamma_2, phi_2)───PhXZ(a=0,x=g,z=local_field_3)────────────────────────────────────────────────────PhFSim(theta_2, zeta_2, chi_2, gamma_2, phi_2)───\n" + ] + } + ], + "source": [ + "qubits = [cirq.GridQubit(0,i) for i in range(4)]\n", + "circuit_list = time_crystals.symbolic_dtc_circuit_list(qubits, 2)\n", + "for circuit in circuit_list: \n", + " print('\\ncircuit of length ' + str(len(circuit)) + \"\\n\")\n", + " print(circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3ba0e882df83" + }, + "source": [ + "After the initial line of `cirq.Y` gates, each consecutive circuit in the list has an additional cycle of `cirq.PhasedXZGate`s, followed by the chain of `cirq.PhasedFSimGate`s on alternating qubit pairs. Each cycle of three moments becomes one time step in the later analysis of stable oscillations over time. \n", + "\n", + "The next step is to perform experiments to collect evidence of the time-crystalline behavior of the quantum state's polarizations. See the [Time Crystal Data Collection](time_crystal_data_collection.ipynb) notebook for the experiments, and the [Time Crystal Data Analysis](time_crystal_data_analysis.ipynb) notebook for the graphed data and results. " + ] + } + ], + "metadata": { + "colab": { + "name": "time_crystal_circuit_generation.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/docs/time_crystals/time_crystal_data_collection.ipynb b/docs/time_crystals/time_crystal_data_collection.ipynb new file mode 100644 index 00000000..3d2e3dda --- /dev/null +++ b/docs/time_crystals/time_crystal_data_collection.ipynb @@ -0,0 +1,537 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "10a61b4d73a5" + }, + "outputs": [], + "source": [ + "# Copyright 2021 Google\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4bdef84a5ef5" + }, + "source": [ + "# Time Crystal Data Collection\n", + "\n", + "This notebook acts as a script to run the experiments and save the data associated with Figures 2d through 3d in the paper: Observation of Time-Crystalline Eigenstate Order on a Quantum Processor ([Nature](https://www.nature.com/articles/s41586-021-04257-w)). \n", + "\n", + "Each of the five experiments are built using `recirq.time_crystals.dtctasks.CompareDTCTask`, which defines the experiment parameters that are to be compared. `CompareDTCTask.dtctasks()` then creates `recirq.time_crystals.dtctasks.DTCTask`s with all of the requisite parameters for an instance of the experiment. \n", + "\n", + "A `DTCTask` has the following attributes and default values: \n", + "- `qubits`: Sequence of qubits connected in a chain. Defaults to a line of $16$ connected `cirq.GridQubits`. \n", + "- `disorder_instances`: Number of disorder instances to simulate and average resulting polarizations over. Defaults to $36$.\n", + "- `g`: Control parameter which influences how well the system is able to maintain time-crystalline behavior. Used in `cirq.PhasedXZGate`s in the circuit. Defaults to $0.94$.\n", + "- `initial_state` or `initial_states`: Only one should be supplied. Defines the input state of the system and is implemented with `cirq.Y` gates in the circuit. If `initial_state` is supplied, it will be repeated and used for every disorder instance. Defaults to a different random bit string for each disorder instance.\n", + "- `local_fields`: Random potentials critical to enable many-body local behavior. Used in `cirq.PhasedXZGate`s in the circuit. Defaults to uniformly selected float values between $-1.0$ and $1.0$.\n", + "- `thetas`, `zetas` and `chis`: Parameters used in the FSim gates in the circuit. Defaults to zero in all cases.\n", + "- `phis`: Parameter used in the FSim gates in the circuit. Affects the stability of the time-crystalline behavior. Defaults to uniformly selected float values between $-1.5*\\pi$ and $-0.5*\\pi$. \n", + "- `gammas`: Parameter used in the FSim gates in the circuit. `Gammas` and `phis` are interdependent such that they satisfy $gammas = -2*phis$; One is set according to this equation if the other is supplied, otherwise use the default `phis` and calculate `gammas` from that.\n", + "\n", + "`CompareDTCTask.dtctasks()` takes the product over the values of the `options_dict` supplied to the `CompareDTCTask` object, and passes those values as parameters to the `__init__()` function for `DTCTask`. Any parameter not supplied takes it's default value, meaning only the different parameter options that are to be compared need to be supplied to `CompareDTCTask`'s `options_dict`. This also means that supplying one parameter option for a parameter fixes that parameter option across all cases, instead of using the defaults, which may randomly generate values for each different `DTCTask`. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "f53722cb0850" + }, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EkaIfQIIkXjE" + }, + "outputs": [], + "source": [ + "!pip install cirq --pre --quiet\n", + "try:\n", + " import recirq\n", + "except ImportError:\n", + " !pip install --quiet git+https://github.com/quantumlib/ReCirq" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "5tTJoyYMk0bK" + }, + "outputs": [], + "source": [ + "import cirq\n", + "import itertools\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import recirq.time_crystals as time_crystals" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c1bae56e4403" + }, + "source": [ + "## Variables used in all experiments\n", + "Defines the qubits, number of DTC cycles (time steps) to evaluate, and the `base_dir` to store experiment results in." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "D8ntbOBa4MKZ" + }, + "outputs": [], + "source": [ + "# define the qubits to use\n", + "qubit_locations = [(3, 9), (3, 8), (3, 7), (4, 7), (4, 8), (5, 8), (5, 7), (5, 6), (6, 6), (6, 5), (7, 5), (8, 5),\n", + " (8, 4), (8, 3), (7, 3), (6, 3)]\n", + "\n", + "qubits = [cirq.GridQubit(*idx) for idx in qubit_locations]\n", + "num_qubits = len(qubits)\n", + "\n", + "# number of cycles to evaluate over\n", + "num_cycles = 100\n", + "\n", + "# directory to store data in\n", + "base_dir = time_crystals.DEFAULT_BASE_DIR" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "17506537dabc" + }, + "source": [ + "## Figure 2d's Experiment\n", + "This experiment considers the constant `g`, which affects the ability for the DTC system to oscillate consistently.\n", + "\n", + "It compares two different values for `g`, $0.6$ and $0.94$, but uses the same disorder instances (randomly selected parameter values) for each of the two values of `g`. \n", + "\n", + "Define this with `options_dict` below, which results takes a product over the values of the dictionary. The result is the following two `recirq.time_crystals.DTCTasks`, with different values of `g`, but using the same `initial_states`, `local_fields`, and `gammas`: \n", + "- `DTCTask(g = 0.6, initial_states = initial_states, local_fields = local_fields, gammas = gammas)`\n", + "- `DTCTask(g = 0.94, initial_states = initial_states, local_fields = local_fields, gammas = gammas)`\n", + "\n", + "These two `DTCTask`s are each simulated over 36 disorder instances, have the polarizations for each qubit calculated, autocorrelated with the initial state, averaged, and finally saved as a json. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "8_rImPnElMGJ" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2min 48s, sys: 84.9 ms, total: 2min 48s\n", + "Wall time: 2min 48s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# number of disorder instances to average over\n", + "disorder_instances = 36\n", + "\n", + "# disorder instances h\n", + "local_fields = np.random.uniform(-1.0, 1.0, (disorder_instances, num_qubits))\n", + "\n", + "# initial states, one for each disorder instance\n", + "initial_states = np.random.choice(2, (disorder_instances, num_qubits))\n", + "\n", + "# gammas for phased FSim gates\n", + "gammas = np.random.uniform(-0.5*np.pi, -1.5*np.pi, (disorder_instances, num_qubits - 1))\n", + "\n", + "# create comparison task\n", + "options_dict = {\n", + " 'g': [0.6, 0.94],\n", + " 'initial_states': [initial_states], \n", + " 'local_fields' : [local_fields],\n", + " 'gammas': [gammas]\n", + "}\n", + "comparedtctask = time_crystals.CompareDTCTask(qubits, num_cycles, disorder_instances, options_dict)\n", + "\n", + "# create polarizations generator\n", + "polarizations_generator = time_crystals.run_comparison_experiment(comparedtctask, autocorrelate=True, take_abs=False)\n", + "\n", + "# collect polarizations by g option\n", + "average_polarizations = np.empty((2, num_cycles+1, num_qubits))\n", + "\n", + "for g_index, disorder_averaged_polarizations in enumerate(polarizations_generator):\n", + " average_polarizations[g_index, :, :] = disorder_averaged_polarizations\n", + "\n", + "# save data in json format\n", + "filename = f'{base_dir}/2d.json'\n", + "with open(filename, 'w+') as f:\n", + " cirq.to_json(average_polarizations, file_or_fn=f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "116c8af588df" + }, + "source": [ + "## Figure 3a's Experiment\n", + "This experiment compares six options: the product between:\n", + "- Two options for `phis`: Uniformly, randomly selected `phis` between $-1.5\\pi$ and $-0.5\\pi$, and a fixed value of $-0.4$ for all `phis`. \n", + "- Three options for `initial_state`: \n", + " - The polarized state of all zeros: `0000000000000000`\n", + " - The Néel state of alternating zeros and ones: `0101010101010101`\n", + " - A state with randomly selected zeros and ones: `00111000010011001111` (the first $16$ qubits)\n", + "\n", + "It uses the same `local_fields` for all cases, autocorrelates the polarizations relative to the initial states, and averages over $24$ disorder instances. \n", + "\n", + "It also averages the `disorder_averaged_polarizations` over all $16$ qubit states before storing the results." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "c9ef4b74d2ea" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 5min 37s, sys: 140 ms, total: 5min 37s\n", + "Wall time: 5min 37s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# number of disorder instances to average over\n", + "disorder_instances = 24\n", + "\n", + "# disorder instances h\n", + "local_fields = np.random.uniform(-1.0, 1.0, (disorder_instances, num_qubits))\n", + "\n", + "# prepare 3 initial states to compare\n", + "neel_initial_state = np.tile([0,1], num_qubits//2)\n", + "polarized_initial_state = np.full(num_qubits, 0)\n", + "random_initial_state = [0,0,1,1,1,0,0,0,0,1,0,0,1,1,0,0,1,1,1,1][:num_qubits]\n", + "initial_states = [neel_initial_state, polarized_initial_state, random_initial_state]\n", + "\n", + "# prepare random and fixed phis to compare\n", + "disordered_phis = np.random.uniform(-1.5*np.pi, -0.5*np.pi, (disorder_instances, num_qubits - 1))\n", + "fixed_phis = np.full((disorder_instances, num_qubits - 1), -0.4)\n", + "\n", + "# create comparison task\n", + "options_dict = {\n", + " 'local_fields': [local_fields], \n", + " 'initial_state': initial_states,\n", + " 'phis': [disordered_phis, fixed_phis]\n", + "}\n", + "options_order = ['local_fields', 'phis', 'initial_state']\n", + "comparedtctask = time_crystals.CompareDTCTask(qubits, num_cycles, disorder_instances, options_dict, options_order)\n", + "\n", + "# prepare polarizations and indices generators\n", + "polarizations_generator = time_crystals.run_comparison_experiment(comparedtctask, autocorrelate=True, take_abs=False)\n", + "indices_iterator = itertools.product(range(2), range(len(initial_states)))\n", + "\n", + "# collect polarizations averaged over qubit sites by phi and initial state options\n", + "average_polarizations = np.empty((2, len(initial_states), num_cycles+1))\n", + "\n", + "for (phi_index, initial_state_index), disorder_averaged_polarizations in zip(indices_iterator, polarizations_generator):\n", + " # store average over all qubit sites\n", + " average_polarizations[phi_index, initial_state_index, :] = np.mean(disorder_averaged_polarizations, axis=1)\n", + "\n", + "# save data in json format\n", + "filename = f'{base_dir}/3a.json'\n", + "with open(filename, 'w+') as f:\n", + " cirq.to_json(average_polarizations, file_or_fn=f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a9efcbf86bd4" + }, + "source": [ + "## Figure 3b's Experiment\n", + "This experiment compares $40$ different cases, the product between: \n", + "- Two options for `phis`: Uniformly, randomly selected `phis` and `phis` fixed at $-0.4$.\n", + "- 20 options for `initial_state`: 20 randomly selected bit string initial states.\n", + "\n", + "Again, the same random potentials, `local_fields`, are used in all cases. The resulting polarizations are autocorrelated with the initial states, **have their absolute value taken**, and are averaged over $24$ disorder instances. \n", + "\n", + "This time, store the average over all of the $16$ qubits, but only for cycles $30$ and $31$. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "id": "0f8ed2aed81c" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 37min 47s, sys: 925 ms, total: 37min 48s\n", + "Wall time: 37min 48s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# instance counts for disorder and random initial states\n", + "disorder_instances = 24\n", + "initial_state_instances = 20 # this is 500 in the paper\n", + "\n", + "# disorder instances h\n", + "local_fields = np.random.uniform(-1.0, 1.0, (disorder_instances, num_qubits))\n", + "\n", + "# prepare random initial states\n", + "initial_states = np.random.choice(2, (initial_state_instances, num_qubits))\n", + "\n", + "# prepare random and fixed phis to compare\n", + "disordered_phis = np.random.uniform(-1.5*np.pi, -0.5*np.pi, (disorder_instances, num_qubits - 1))\n", + "fixed_phis = np.full((disorder_instances, num_qubits - 1), -0.4)\n", + "\n", + "# create comparison task\n", + "options_dict = {\n", + " 'local_fields': [local_fields],\n", + " 'initial_state': initial_states,\n", + " 'phis': [disordered_phis, fixed_phis]\n", + "}\n", + "options_order = ['local_fields', 'phis', 'initial_state']\n", + "comparedtctask = time_crystals.CompareDTCTask(qubits, num_cycles, disorder_instances, options_dict, options_order)\n", + "\n", + "# prepare polarizations and indices generators\n", + "polarizations_generator = time_crystals.run_comparison_experiment(comparedtctask, autocorrelate=True, take_abs=True)\n", + "indices_iterator = itertools.product(range(2), range(len(initial_states)))\n", + "\n", + "# collect polarizations, averaged over qubit sites and cycles 30 and 31, by phi and initial state options\n", + "average_polarizations = np.empty((2, initial_state_instances))\n", + "\n", + "for (phi_index, initial_state_index), disorder_averaged_polarizations in zip(indices_iterator, polarizations_generator):\n", + " # store average over qubit sites and cycles 30 and 31\n", + " average_polarizations[phi_index, initial_state_index] = np.mean(disorder_averaged_polarizations[30:32, :])\n", + " \n", + "# save data in json format\n", + "filename = f'{base_dir}/3b.json'\n", + "with open(filename, 'w+') as f:\n", + " cirq.to_json(average_polarizations, file_or_fn=f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "30bde5b12339" + }, + "source": [ + "## Figure 3c's Experiment\n", + "This experiment compares $4$ different cases, the product between: \n", + "- Two options for `phis`: Uniformly, randomly selected `phis` and `phis` fixed at $-0.4$.\n", + "- Two options for `initial_state`: \n", + " - The polarized initial state of all zeros: `0000000000000000`\n", + " - The polarized initial state, but disturbed at qubit index $11$: `0000000000010000`\n", + "\n", + "The same `local_fields`, are used in all cases. The polarizations are **not** autocorrelated, and are averaged over $24$ disorder instances. \n", + "\n", + "Store the polarizations matrix of shape `(num_cycles + 1, num_qubits)` without averaging, but only over cycles $30$ through $60$, and only over qubits $11$ through $14$. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "id": "9e1c8aa67147" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 3min 44s, sys: 108 ms, total: 3min 44s\n", + "Wall time: 3min 44s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# number of disorder instances to average over\n", + "disorder_instances = 24\n", + "\n", + "# disorder parameters h\n", + "local_fields = np.random.uniform(-1.0, 1.0, (disorder_instances, num_qubits))\n", + "\n", + "# prepare random and fixed phis to compare\n", + "disordered_phis = np.random.uniform(-1.5*np.pi, -0.5*np.pi, (disorder_instances, num_qubits - 1))\n", + "fixed_phis = np.full((disorder_instances, num_qubits - 1), -0.4)\n", + "\n", + "# prepare two initial states to compare\n", + "polarized_initial_state = [0]*num_qubits\n", + "disturb_qubit = 11\n", + "disturbed_polarized_initial_state = list(polarized_initial_state)\n", + "disturbed_polarized_initial_state[disturb_qubit] = 1\n", + "initial_states = [polarized_initial_state, disturbed_polarized_initial_state]\n", + "\n", + "# create comparison task\n", + "options_dict = {\n", + " 'local_fields': [local_fields],\n", + " 'initial_state': initial_states,\n", + " 'phis': [disordered_phis, fixed_phis]\n", + " \n", + "}\n", + "options_order = ['local_fields', 'phis', 'initial_state']\n", + "comparedtctask = time_crystals.CompareDTCTask(qubits, num_cycles, disorder_instances, options_dict, options_order)\n", + "\n", + "# prepare polarizations and indices generators\n", + "polarizations_generator = time_crystals.run_comparison_experiment(comparedtctask, autocorrelate=False, take_abs=False)\n", + "indices_iterator = itertools.product(range(2), range(len(initial_states)))\n", + "\n", + "# collect polarizations, averaged over cycles 30 through 60 and qubits 11 through 14, by phi and initial state options\n", + "average_polarizations = np.empty((2, 2, 31, 4))\n", + "\n", + "for (phi_index, initial_state_index), disorder_averaged_polarizations in zip(indices_iterator, polarizations_generator):\n", + " # store average over cycles 30 and 31, and qubits 11 through 14\n", + " average_polarizations[phi_index, initial_state_index, :, :] = disorder_averaged_polarizations[30:61, 11:15]\n", + "\n", + "# save data in json format\n", + "filename = f'{base_dir}/3c.json'\n", + "with open(filename, 'w+') as f:\n", + " cirq.to_json(average_polarizations, file_or_fn=f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cb509ac9594b" + }, + "source": [ + "## Figure 3d's Experiment\n", + "This again experiment compares $4$ different cases, the product between: \n", + "- Two options for `phis`: Uniformly, randomly selected `phis` and `phis` fixed at $-0.4$.\n", + "- Two options for `initial_state`: \n", + " - The polarized initial state of all zeros: `0000000000000000`\n", + " - The polarized initial state, but disturbed at qubit index $11$: `0000000000010000`\n", + "\n", + "However, this experiment differs from 3c's in that, for each of the two `initial_state`s, different random `local_fields` and `phis` are generated. Do this by creating two different `CompareDTCTasks` for the different `initial_state`s, each of which compares `phis`. The collected polarizations are **not** autocorrelated, and are averaged over $24$ disorder instances. \n", + "\n", + "Store the polarizations matrix of shape `(num_cycles + 1, num_qubits)` without averaging, indexed by `phis` option and `initial_state` option. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "id": "a19b4c927509" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 11min 25s, sys: 272 ms, total: 11min 25s\n", + "Wall time: 11min 25s\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "# prepare two initial states to compare\n", + "polarized_initial_state = np.full(num_qubits, 0)\n", + "disturb_qubit = 11\n", + "disturbed_polarized_initial_state = polarized_initial_state.copy()\n", + "disturbed_polarized_initial_state[disturb_qubit] = 1\n", + "initial_states = [polarized_initial_state, disturbed_polarized_initial_state]\n", + "\n", + "# use different disorder instances for the two initial states\n", + "disorder_instances_options = [64, 81]\n", + "\n", + "# collect polarizations by phi and initial state options\n", + "average_polarizations = np.empty((2, 2, num_cycles + 1, num_qubits))\n", + "\n", + "# iterate over initial states and their associated number of disorder instances\n", + "for initial_state_index, (initial_state, disorder_instances) in enumerate(zip(initial_states, disorder_instances_options)): \n", + " \n", + " # disorder parameter h\n", + " local_fields = np.random.uniform(-1.0, 1.0, (disorder_instances, num_qubits))\n", + " \n", + " # prepare random and fixed phis to compare\n", + " disordered_phis = np.random.uniform(-1.5*np.pi, -0.5*np.pi, (disorder_instances, num_qubits - 1))\n", + " fixed_phis = np.full((disorder_instances, num_qubits - 1), -0.4)\n", + "\n", + " # create comparison task\n", + " options_dict = {\n", + " 'initial_state': [initial_state],\n", + " 'local_fields': [local_fields],\n", + " 'phis': [disordered_phis, fixed_phis]\n", + " }\n", + " options_order = ['local_fields', 'phis', 'initial_state']\n", + " comparedtctask = time_crystals.CompareDTCTask(qubits, num_cycles, disorder_instances, options_dict, options_order)\n", + "\n", + " # prepare polarizations and indices generators\n", + " polarizations_generator = time_crystals.run_comparison_experiment(comparedtctask, autocorrelate=False, take_abs=False)\n", + " \n", + " for phi_index, disorder_averaged_polarizations in enumerate(polarizations_generator):\n", + " # store average polarizations\n", + " average_polarizations[phi_index, initial_state_index, :, :] = disorder_averaged_polarizations\n", + "\n", + "# save data in json format\n", + "filename = f'{base_dir}/3d.json'\n", + "with open(filename, 'w+') as f:\n", + " cirq.to_json(average_polarizations, file_or_fn=f)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "833e5329b35e" + }, + "source": [ + "## Next Steps\n", + "With the data collected and saved, move on to the [Time Crystal Data Analysis](time_crystal_data_analysis.ipynb) notebook to generate the plots and evaluate their results." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "time_crystal_data_collection.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/recirq/time_crystals/__init__.py b/recirq/time_crystals/__init__.py new file mode 100644 index 00000000..d196895b --- /dev/null +++ b/recirq/time_crystals/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2021 Google +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from recirq.time_crystals.dtctask import * +from recirq.time_crystals.dtc_utilities import * diff --git a/recirq/time_crystals/dtc_utilities.py b/recirq/time_crystals/dtc_utilities.py new file mode 100644 index 00000000..da357697 --- /dev/null +++ b/recirq/time_crystals/dtc_utilities.py @@ -0,0 +1,236 @@ +# Copyright 2021 Google +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Sequence, Tuple, List + +import cirq +import functools +from recirq.time_crystals.dtctask import DTCTask, CompareDTCTask +import numpy as np +import sympy as sp + +def symbolic_dtc_circuit_list( + qubits: Sequence[cirq.Qid], + cycles: int + ) -> List[cirq.Circuit]: + + """ Create a list of symbolically parameterized dtc circuits, with increasing cycles + Args: + qubits: ordered sequence of available qubits, which are connected in a chain + cycles: maximum number of cycles to generate up to + Returns: + list of circuits with `0, 1, 2, ... cycles` many cycles + """ + + num_qubits = len(qubits) + + # Symbol for g + g_value = sp.Symbol('g') + + # Symbols for random variance (h) and initial state, one per qubit + local_fields = sp.symbols('local_field_:' + str(num_qubits)) + initial_state = sp.symbols('initial_state_:' + str(num_qubits)) + + # Symbols used for PhasedFsimGate, one for every qubit pair in the chain + thetas = sp.symbols('theta_:' + str(num_qubits - 1)) + zetas = sp.symbols('zeta_:' + str(num_qubits - 1)) + chis = sp.symbols('chi_:' + str(num_qubits - 1)) + gammas = sp.symbols('gamma_:' + str(num_qubits - 1)) + phis = sp.symbols('phi_:' + str(num_qubits - 1)) + + # Initial moment of Y gates, conditioned on initial state + initial_operations = cirq.Moment([cirq.Y(qubit) ** initial_state[index] for index, qubit in enumerate(qubits)]) + + # First component of U cycle, a moment of XZ gates. + sequence_operations = [] + for index, qubit in enumerate(qubits): + sequence_operations.append(cirq.PhasedXZGate( + x_exponent=g_value, axis_phase_exponent=0.0, + z_exponent=local_fields[index])(qubit)) + + # Initialize U cycle + u_cycle = [cirq.Moment(sequence_operations)] + + # Second and third components of U cycle, a chain of 2-qubit PhasedFSim gates + # The first component is all the 2-qubit PhasedFSim gates starting on even qubits + # The second component is the 2-qubit gates starting on odd qubits + operation_list, other_operation_list = [],[] + previous_qubit, previous_index = None, None + for index, qubit in enumerate(qubits): + if previous_qubit is None: + previous_qubit, previous_index = qubit, index + continue + + # Add an fsim gate + coupling_gate = cirq.ops.PhasedFSimGate( + theta=thetas[previous_index], + zeta=zetas[previous_index], + chi=chis[previous_index], + gamma=gammas[previous_index], + phi=phis[previous_index] + ) + operation_list.append(coupling_gate.on(previous_qubit, qubit)) + + # Swap the operation lists, to avoid two-qubit gate overlap + previous_qubit, previous_index = qubit, index + operation_list, other_operation_list = other_operation_list, operation_list + + # Add the two components into the U cycle + u_cycle.append(cirq.Moment(operation_list)) + u_cycle.append(cirq.Moment(other_operation_list)) + + # Prepare a list of circuits, with n=0,1,2,3 ... cycles many cycles + circuit_list = [] + total_circuit = cirq.Circuit(initial_operations) + circuit_list.append(total_circuit.copy()) + for c in range(cycles): + for m in u_cycle: + total_circuit.append(m) + circuit_list.append(total_circuit.copy()) + + return circuit_list + +def simulate_dtc_circuit_list(circuit_list: Sequence[cirq.Circuit], param_resolver: cirq.ParamResolver, qubit_order: Sequence[cirq.Qid]) -> np.ndarray: + """ Simulate a dtc circuit list for a particular param_resolver + Depends on the fact that simulating the last circuit in the list also simulates each previous circuit along the way + Args: + circuit_list: DTC circuit list; each element is a circuit with increasingly many cycles + param_resolver: `cirq.ParamResolver` to resolve symbolic parameters + qubit_order: ordered sequence of qubits connected in a chain + Returns: + `np.ndarray` of shape (len(circuit_list), 2**number of qubits) representing the probability of measuring each bit string, for each circuit in the list + """ + + # prepare simulator + simulator = cirq.Simulator() + + # record lengths of circuits in list + circuit_positions = [len(c) - 1 for c in circuit_list] + + # only simulate one circuit, the last one + circuit = circuit_list[-1] + + # use simulate_moment_steps to recover all of the state vectors necessary, while only simulating the circuit list once + probabilities = [] + for k, step in enumerate(simulator.simulate_moment_steps(circuit=circuit, param_resolver=param_resolver, qubit_order=qubit_order)): + # add the state vector if the number of moments simulated so far is equal to the length of a circuit in the circuit_list + if k in circuit_positions: + probabilities.append(np.abs(step.state_vector()) ** 2) + + return np.asarray(probabilities) + +def simulate_dtc_circuit_list_sweep(circuit_list: Sequence[cirq.Circuit], param_resolvers: Sequence[cirq.ParamResolver], qubit_order: Sequence[cirq.Qid]): + """ Simulate a dtc circuit list over a sweep of param_resolvers + Args: + circuit_list: DTC circuit list; each element is a circuit with increasingly many cycles + param_resolvers: list of `cirq.ParamResolver`s to sweep over + qubit_order: ordered sequence of qubits connected in a chain + Yields: + for each param_resolver, `np.ndarray`s of shape (len(circuit_list), 2**number of qubits) representing the probability of measuring each bit string, for each circuit in the list + """ + + # iterate over param resolvers and simulate for each + for param_resolver in param_resolvers: + yield simulate_dtc_circuit_list(circuit_list, param_resolver, qubit_order) + +def get_polarizations(probabilities: np.ndarray, num_qubits: int, cycles_axis: int = -2, probabilities_axis: int = -1, initial_states: np.ndarray = None) -> np.ndarray: + """ Get polarizations from matrix of probabilities, possibly autocorrelated on the initial state + Args: + probabilities: `np.ndarray` of shape (:, cycles, probabilities) representing probability to measure each bit string + num_qubits: the number of qubits in the circuit the probabilities were generated from + cycles_axis: the axis that represents the dtc cycles (if not in -2 indexed axis) + probabilities_axis: the axis that represents the probabilities for each bit string (if not in -1 indexed axis) + initial_states: `np.ndarray` of shape (:, qubits) representing the initial state for each dtc circuit list + Returns: + `np.ndarray` of shape (:, cycles, qubits) that represents each qubit's polarization + """ + + # prepare list of polarizations for each qubit + polarizations = [] + for qubit_index in range(num_qubits): + # select all indices in range(2**num_qubits) for which the associated element of the statevector has qubit_index as zero + shift_by = num_qubits - qubit_index - 1 + state_vector_indices = [i for i in range(2 ** num_qubits) if not (i >> shift_by) % 2] + + # sum over all amplitudes for qubit states for which qubit_index is zero, and rescale them to [-1,1] + polarization = 2.0 * np.sum(probabilities.take(indices=state_vector_indices, axis=probabilities_axis), axis=probabilities_axis) - 1.0 + polarizations.append(polarization) + + # turn polarizations list into an array, and move the new, leftmost axis for qubits to probabilities_axis + polarizations = np.moveaxis(np.asarray(polarizations), 0, probabilities_axis) + + # flip polarizations according to the associated initial_state, if provided + # this means that the polarization of a qubit is relative to it's initial state + if initial_states is not None: + initial_states = 1 - 2.0 * initial_states + polarizations = initial_states * polarizations + + return polarizations + + +def signal_ratio(zeta_1: np.ndarray, zeta_2: np.ndarray): + ''' Calculate signal ratio between two signals + Args: + zeta_1: signal (`np.ndarray` to represent polarization over time) + zeta 2: signal (`np.ndarray` to represent polarization over time) + Returns: + computed ratio signal of zeta_1 and zeta_2 (`np.ndarray` to represent polarization over time) + ''' + + return np.abs(zeta_1 - zeta_2)/(np.abs(zeta_1) + np.abs(zeta_2)) + + +def simulate_for_polarizations(dtctask: DTCTask, circuit_list: Sequence[cirq.Circuit], autocorrelate: bool = True, take_abs: bool = False): + """ Simulate and get polarizations for a single DTCTask and circuit list + Args: + dtctask: DTCTask noting the parameters to simulate over some number of disorder instances + circuit_list: symbolic dtc circuit list + autocorrelate: whether or not to autocorrelate the polarizations with their respective initial states + take_abs: whether or not to take the absolute value of the polarizations + Returns: + simulated polarizations (np.ndarray of shape (num_cycles, num_qubits)) from the experiment, averaged over disorder instances + """ + + # create param resolver sweep + param_resolvers = dtctask.param_resolvers() + + # prepare simulation generator + probabilities_generator = simulate_dtc_circuit_list_sweep(circuit_list, param_resolvers, dtctask.qubits) + + # map get_polarizations over probabilities_generator + polarizations_generator = map(lambda probabilities, initial_state: + get_polarizations(probabilities, num_qubits=len(dtctask.qubits), cycles_axis=0, probabilities_axis=1, initial_states=(initial_state if autocorrelate else None)), + probabilities_generator, dtctask.initial_states) + + # take sum of (absolute value of) polarizations over different disorder instances + polarization_sum = functools.reduce(lambda x,y: x+(np.abs(y) if take_abs else y), polarizations_generator, np.zeros((len(circuit_list), len(dtctask.qubits)))) + + # get average over disorder instances + disorder_averaged_polarizations = polarization_sum / dtctask.disorder_instances + + return disorder_averaged_polarizations + + +def run_comparison_experiment(comparedtctask: CompareDTCTask, autocorrelate: bool = True, take_abs: bool = False): + """ Run comparison experiment from a CompareDTCTask + Args: + comparedtctask: CompareDTCTask which notes which dtc arguments to compare, and default arguments + autocorrelate: whether or not to autocorrelate the polarizations with their respective initial states + take_abs: whether or not to take the absolute value of the polarizations + Yields: + disorder averaged polarizations, in order of the product of options supplied to comparedtctask, with all other parameters default + """ + + for dtctask in comparedtctask.dtctasks(): + yield simulate_for_polarizations(dtctask=dtctask, circuit_list=comparedtctask.circuit_list, autocorrelate=autocorrelate, take_abs=take_abs) diff --git a/recirq/time_crystals/dtctask.py b/recirq/time_crystals/dtctask.py new file mode 100644 index 00000000..addec1ad --- /dev/null +++ b/recirq/time_crystals/dtctask.py @@ -0,0 +1,254 @@ +# Copyright 2021 Google +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cirq +import recirq +import datetime +import itertools +import numpy as np +from typing import Sequence, Optional, Dict +import os + +EXPERIMENT_NAME = "time_crystals" +DEFAULT_BASE_DIR = os.path.expanduser(f'~/cirq_results/{EXPERIMENT_NAME}') + + +@recirq.json_serializable_dataclass(namespace='recirq.readout_scan', + registry=recirq.Registry, + frozen=False) +class CompareDTCTask: + """ A task for managing inputs to a comparison Discrete Time Crystal experiment, comparing different options for parameters + + Attributes + dataset_id: unique identifier for this dataset + qubits: chain of connected qubits available for the circuit + cycles: number of DTC cycles to consider for circuits + circuit_list: symbolic DTC circuit list + disorder_instances: number of disorder instances averaged over + options_dict: dict mapping DTCTask attribute names to options for that attribute, to take a product over + options_order: sequence of keys in options_dict, defining order of product over options + """ + + # Task parameters + dataset_id: str + + # experiment parameters + qubits: Sequence[cirq.Qid] + cycles: int + disorder_instances: int + circuit_list: Sequence[cirq.Circuit] + + # options to take product over + options_dict: Dict[str, Sequence[np.ndarray]] + options_order: Sequence[str] + + def __init__( + self, + qubits: Sequence[cirq.Qid], + cycles: int, + disorder_instances: int, + options_dict: Dict[str, Sequence[np.ndarray]], + options_order: Optional[Sequence[str]] = None): + + self.dataset_id = datetime.datetime.utcnow() + + self.qubits = qubits + self.cycles = cycles + self.disorder_instances = disorder_instances + + # create symbolic circuit list from qubits and cycles count + self.circuit_list = recirq.time_crystals.symbolic_dtc_circuit_list(qubits, cycles) + + self.options_dict = options_dict + self.options_order = list(self.options_dict.keys()) if options_order is None else options_order + + # check that the input parameters are consistent + assert set(self.options_order) == set(self.options_dict.keys()), 'options_order and the keys of options_dict are not the same' + assert not {'initial_states', 'initial_state'} <= self.options_dict.keys(), 'do not supply both initial_states and initial_state' + + + @property + def fn(self): + fn = (f'{self.dataset_id}/' + f'{len(self.qubits)}/' + f'{self.cycles}/' + f'{self.disorder_instances}/' + f'{self.options_dict}') + return fn + + + def dtctasks(self): + """ Yield a sequence of DTCTasks that are the product of the options in self.options_dict. + All DTCTask attributes not in options_dict are taken to be their default values + Yields: + DTCTasks with parameters taken from self.options_dict + """ + + # take product over elements of options_dict, in the order of options_order + for components in itertools.product(*(self.options_dict[attribute_name] for attribute_name in self.options_order)): + # prepare arguments for DTCTask + kwargs = dict(zip(self.options_order, components)) + yield DTCTask(qubits=self.qubits, disorder_instances=self.disorder_instances, **kwargs) + + +@recirq.json_serializable_dataclass(namespace='recirq.readout_scan', + registry=recirq.Registry, + frozen=False) +class DTCTask: + """ A task for managing inputs to a Discrete Time Crystal experiment, over some number of disorder instances + + Attributes: + dataset_id: unique identifier for this dataset + qubits: a chain of connected qubits available for the circuit + disorder_instances: number of disorder instances averaged over + initial_states: initial state of the system used in circuit + g: control parameter used in circuit + local_fields: random potentials used in circuit + thetas: theta parameters for FSim Gate used in circuit + zetas: zeta parameters for FSim Gate used in circuit + chis: chi parameters for FSim Gate used in circuit + phis: phi parameters for FSim Gate used in circuit + gammas: gamma parameters for FSim Gate used in circuit + + """ + # Task parameters + dataset_id: str + + # experiment parameters + qubits: Sequence[cirq.Qid] + disorder_instances: int + + # FSim Gate parameters + # ndarrays in this section are in shape (disorder_instances, len(qubits) - 1) + g: int + initial_states: np.ndarray + local_fields: np.ndarray + + # FSim Gate Parameters + # ndarrays in this section are in shape (disorder_instances, len(qubits) - 1) + thetas: np.ndarray + zetas: np.ndarray + chis: np.ndarray + gammas: np.ndarray + phis: np.ndarray + + + def __init__( + self, + qubits: Optional[Sequence[cirq.Qid]] = None, + disorder_instances: Optional[int] = None, + g: Optional[int] = None, + initial_state: Optional[np.ndarray] = None, + initial_states: Optional[np.ndarray] = None, + local_fields: Optional[np.ndarray] = None, + thetas: Optional[np.ndarray] = None, + zetas: Optional[np.ndarray] = None, + chis: Optional[np.ndarray] = None, + gammas: Optional[np.ndarray] = None, + phis: Optional[np.ndarray] = None + ): + + self.dataset_id = datetime.datetime.utcnow() + + self.disorder_instances = 36 if disorder_instances is None else disorder_instances + + self.g = 0.94 if g is None else g + + if qubits is None: + qubit_locations = [(3, 9), (3, 8), (3, 7), (4, 7), (4, 8), (5, 8), (5, 7), (5, 6), (6, 6), (6, 5), (7, 5), (8, 5), (8, 4), (8, 3), (7, 3), (6, 3)] + self.qubits = [cirq.GridQubit(*idx) for idx in qubit_locations] + else: + self.qubits = qubits + + num_qubits = len(self.qubits) + + # only enable use of initial_state or initial_states + assert initial_state is None or initial_states is None, 'do not supply both initial_state and initial_states' + if initial_state is None and initial_states is None: + self.initial_states = np.random.choice(2, (self.disorder_instances, num_qubits)) + elif initial_states is None: + assert len(initial_state) == num_qubits, f'initial_state is of shape {str(len(initial_state))}, not (num_qubits,)' + self.initial_states = np.tile(initial_state, (self.disorder_instances, 1)) + elif initial_state is None: + assert initial_states.shape == (self.disorder_instances, num_qubits), f'initial_states is of shape {initial_states.shape}, not (disorder_instances, num_qubits)' + self.initial_states = initial_states + + if local_fields is None: + self.local_fields = np.random.uniform(-1.0, 1.0, (self.disorder_instances, num_qubits)) + else: + assert local_fields.shape == (self.disorder_instances, num_qubits), f'local_fields is of shape {local_fields.shape}, not (disorder_instnaces, num_qubits)' + self.local_fields = local_fields + + zero_params = [thetas, zetas, chis] + for index, zero_param in enumerate(zero_params): + if zero_param is None: + zero_params[index] = np.zeros((self.disorder_instances, num_qubits - 1)) + else: + assert zero_param.shape == (self.disorder_instances, num_qubits - 1), f'thetas, zetas or chis is of shape {zero_param.shape}, not (disorder_instances, num_qubits - 1)' + self.thetas, self.zetas, self.chis = zero_params + + # if gamma or phi is not supplied, generate it from the other such that phis == -2*gammas + if gammas is None and phis is None: + self.gammas = -np.random.uniform(0.5*np.pi, 1.5*np.pi, (self.disorder_instances, num_qubits - 1)) + self.phis = -2*self.gammas + elif phis is None: + assert gammas.shape == (self.disorder_instances, num_qubits - 1), f'gammas is of shape {gammas.shape}, not (disorder_instances, num_qubits - 1)' + self.gammas = gammas + self.phis = -2*self.gammas + elif gammas is None: + assert phis.shape == (self.disorder_instances, num_qubits - 1), f'phis is of shape {phis.shape}, not (disorder_instances, num_qubits - 1)' + self.phis = phis + self.gammas = -1/2*self.phis + else: + assert gammas.shape == (self.disorder_instances, num_qubits - 1), f'gammas is of shape {gammas.shape}, not (disorder_instances, num_qubits - 1)' + assert phis.shape == (self.disorder_instances, num_qubits - 1), f'phis is of shape {phis.shape}, not (disorder_instances, num_qubits - 1)' + self.phis = phis + self.gammas = gammas + + + @property + def fn(self): + fn = (f'{self.dataset_id}/' + f'{self.qubits}/' + f'{self.disorder_instances}/' + f'{self.g}/' + f'{self.initial_states}/' + f'{self.local_fields}/' + f'{self.thetas}/' + f'{self.zetas}/' + f'{self.chis}/' + f'{self.gammas}/' + f'{self.phis}/') + return fn + + + def param_resolvers(self): + """ return a sweep over param resolvers for the parameters of this task + Returns: + `cirq.Zip` object with self.disorder_instances many `cirq.ParamResolver`s + """ + + # initialize the dict and add the first, non-qubit-dependent parameter, g + factor_dict = {'g': np.full(self.disorder_instances, self.g).tolist()} + + # iterate over the different parameters + qubit_varying_factors = ["initial_states", "local_fields", "thetas", "zetas", "chis", "gammas", "phis"] + for factor in qubit_varying_factors: + factor_options = getattr(self, factor) + # iterate over each index in the qubit chain and the various options for that qubit + for index, qubit_factor_options in enumerate(factor_options.transpose()): + factor_name = factor[:-1] + factor_dict[f'{factor_name}_{index}'] = qubit_factor_options.tolist() + + return cirq.study.dict_to_zip_sweep(factor_dict) diff --git a/recirq/time_crystals/dtctask_test.py b/recirq/time_crystals/dtctask_test.py new file mode 100644 index 00000000..6a8db92d --- /dev/null +++ b/recirq/time_crystals/dtctask_test.py @@ -0,0 +1,69 @@ +# Copyright 2021 Google +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import recirq.time_crystals as time_crystals +import cirq +import numpy as np + +def test_DTCTask(): + np.random.seed(5) + qubit_locations = [(3, 9), (3, 8), (3, 7), (4, 7), (4, 8), (5, 8), (5, 7), (5, 6), (6, 6), (6, 5), (7, 5), (8, 5), + (8, 4), (8, 3), (7, 3), (6, 3)] + + qubits = [cirq.GridQubit(*idx) for idx in qubit_locations] + num_qubits = len(qubits) + g = 0.94 + instances = 36 + initial_state = np.random.choice(2, num_qubits) + local_fields = np.random.uniform(-1.0, 1.0, (instances, num_qubits)) + thetas = np.zeros((instances, num_qubits - 1)) + zetas = np.zeros((instances, num_qubits - 1)) + chis = np.zeros((instances, num_qubits - 1)) + gammas = -np.random.uniform(0.5*np.pi, 1.5*np.pi, (instances, num_qubits - 1)) + phis = -2*gammas + args = ['qubits', 'g', 'initial_state', 'local_fields', 'thetas', 'zetas', 'chis', 'gammas', 'phis'] + default_resolvers = time_crystals.DTCTask().param_resolvers() + for arg in args: + kwargs = {} + for name in args: + kwargs[name] = None if name is arg else locals()[name] + dtctask = time_crystals.DTCTask(disorder_instances=instances, **kwargs) + param_resolvers = dtctask.param_resolvers() + +def test_CompareDTCTask(): + np.random.seed(5) + qubit_locations = [(3, 9), (3, 8), (3, 7), (4, 7), (4, 8), (5, 8)]#, (5, 7), (5, 6), (6, 6), (6, 5), (7, 5), (8, 5), (8, 4), (8, 3), (7, 3), (6, 3)] + qubits = [cirq.GridQubit(*idx) for idx in qubit_locations] + num_qubits = len(qubits) + disorder_instances = 8 + initial_state_instances = 6 + cycles = 10 + + options = { + 'g': [0.6, 0.94], + 'local_fields': [np.random.uniform(-1.0, 1.0, (disorder_instances, num_qubits))], + 'initial_state': [[0]*num_qubits, [0,1]*(num_qubits//2), np.random.choice(2, num_qubits)], + 'initial_states': [np.random.choice(2, (disorder_instances, num_qubits))], + 'gammas': [np.random.uniform(-0.5*np.pi, -1.5*np.pi, (disorder_instances, num_qubits - 1))], + 'phis': [np.random.uniform(-1.5*np.pi, -0.5*np.pi, (disorder_instances, num_qubits - 1)), np.full((disorder_instances, num_qubits - 1), -0.4)], + } + for initial in ['initial_state', 'initial_states']: + for variable in ['gammas', 'phis']: + options_dict = {k:v for k,v in options.items() if k not in (initial, variable)} + compare_dtctask = time_crystals.CompareDTCTask(qubits, cycles, disorder_instances, options_dict) + for autocorrelate in [True,False]: + for take_abs in [True,False]: + for index, polarizations in enumerate(time_crystals.run_comparison_experiment(compare_dtctask, autocorrelate, take_abs)): + print(index, autocorrelate, take_abs, initial, variable) + print(polarizations)