{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to represent quantum systems using graphs\n",
"**Represent quantum systems for optimization, simulation, and other tasks using graphs**\n",
"\n",
"The typical purpose of graph objects in Boulder Opal is to represent quantum systems.\n",
"The graph representation of such a system can be used to perform several tasks, including [robust control](https://docs.q-ctrl.com/boulder-opal/tutorials/design-robust-single-qubit-gates-using-computational-graphs) (for calculating optimized control pulses), [simulation](https://docs.q-ctrl.com/boulder-opal/tutorials/simulate-the-dynamics-of-a-single-qubit-using-computational-graphs) (to understand the dynamics of the system in the presence of specific controls and noises), and [system identification](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-perform-hamiltonian-parameter-estimation-using-a-small-amount-of-measured-data) (to estimate the values of unknown system parameters based on measurements of the system).\n",
"\n",
"Please refer to our topic [Understanding graphs in Boulder Opal](https://docs.q-ctrl.com/boulder-opal/topics/) for context on what graphs are used for and why.\n",
"\n",
"In what follows, we use graphs to define time-dependent Hamiltonians for simulation or optimization.\n",
"For information on how to use graphs for optimization tasks, see the user guide on [calculating and optimizing with graphs](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-calculate-and-optimize-with-graphs)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary workflow\n",
"\n",
"Here we outline the general procedure for representing your quantum systems and the computations performed on them in the form of graph objects.\n",
"\n",
"### 1. Create graph inputs\n",
"For an optimization, these are optimizable variables that can be tuned by the optimizer to minimize a cost function.\n",
"For a simulation, the inputs can be known values for control pulses, or quantities derived from known values.\n",
"\n",
"### 2. Create signals\n",
"*Signals* are scalar-valued functions of time, which may be non-linear, have enforced temporal structure such as time symmetry, or more generally can depend arbitrarily on the inputs.\n",
"They only contain one numerical value in each period of time, which means their shape is zero-dimensional.\n",
"Signals represent the time-dependent envelope of the Hamiltonian.\n",
"You create piecewise-constant (PWC) signals using the `graph.pwc_signal` operation.\n",
"\n",
"### 3. Create Hamiltonian operators\n",
"You create *operators* or PWC operator-valued (2D) functions of time by multiplying constant matrices (for example Pauli matrices) with signals.\n",
"Usually these operators represent individual terms in your Hamiltonian.\n",
"You can also create constant operators to represent static terms in your Hamiltonian.\n",
"Finally, you sum the individual operators created into a single Hamiltonian operator.\n",
"\n",
"### 4. Add graph nodes representing computations on your quantum system\n",
"Once you have defined the nodes that describe your quantum system, you can add extra nodes representing the computations that you want to perform on it.\n",
"For example, Boulder Opal offers [time evolution operations](https://docs.q-ctrl.com/boulder-opal/references/qctrl/Graphs.html#time-evolution), among many others.\n",
"\n",
"Note that while this approach to constructing Hamiltonians is the most common, it is not a requirement.\n",
"You can use graphs to perform a wide variety of other computations too.\n",
"For example, Boulder Opal also provides specialized functions for working with trapped ions systems that take advantage of certain approximations to bypass Hamiltonian-level descriptions of the system (see the [How to design error-robust Mølmer–Sørensen gates for trapped ions](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-optimize-error-robust-molmer-sorensen-gates-for-trapped-ions) user guide for details)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example: Optimize a pulse to steer a qubit to a target dynamics\n",
"\n",
"In this example, we will perform a simple optimization task for a quantum system using graphs. The system of interest is described by a Hamiltonian of the form\n",
"$$\n",
"H(t) = \\frac{\\omega_0}{2} \\sigma_z + \\alpha(t) \\sigma_x + \\beta(t) \\sigma_z,\n",
"$$\n",
"where $\\omega_0$ is the qubit frequency, $\\alpha(t)$ is a time-dependent control, which we can optimize, and $\\beta(t)$ is a dephasing noise process.\n",
"The dephasing amplitude is slowly varying so that you can assume it is constant at each different realization.\n",
"\n",
"The goal of the optimization will be to remove the dephasing noise from the qubit by choosing an appropriate pulse $\\alpha(t)$. Thus, the target unitary at the total duration time $T$ of the experiment is given by\n",
"$$\n",
"U_\\mathrm{target}(T) = \\exp \\left(-i \\frac{\\omega_0}{2} T \\sigma_z \\right) = \\cos \\left( \\frac{\\omega_0}{2} T \\right) I - i \\sin \\left( \\frac{\\omega_0}{2} T \\right) \\sigma_z .\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"# Import packages.\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from qctrlvisualizer import (\n",
" get_qctrl_style,\n",
" plot_controls,\n",
" plot_population_dynamics,\n",
" display_bloch_sphere,\n",
")\n",
"\n",
"from qctrl import Qctrl\n",
"\n",
"# Apply Q-CTRL style to plots created in pyplot.\n",
"plt.style.use(get_qctrl_style())\n",
"\n",
"# Start a Boulder Opal session.\n",
"qctrl = Qctrl()"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# Define system parameters.\n",
"\n",
"# Qubit frequency.\n",
"omega_0 = 2 * np.pi * 0.5e6 # rad/s\n",
"\n",
"# Pulse parameters.\n",
"segment_count = 50\n",
"duration = 10e-6 # s\n",
"\n",
"# Maximum value for |α(t)|.\n",
"alpha_max = 2 * np.pi * 0.25e6 # rad/s"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"# Create an empty graph.\n",
"graph = qctrl.create_graph()\n",
"\n",
"# Add respective nodes to the graph.\n",
"\n",
"# Real PWC signal representing α(t).\n",
"alpha = graph.utils.real_optimizable_pwc_signal(\n",
" segment_count=segment_count,\n",
" duration=duration,\n",
" minimum=-alpha_max,\n",
" maximum=alpha_max,\n",
" name=\"$\\\\alpha$\",\n",
")\n",
"\n",
"# System Hamiltonian without dephasing.\n",
"hamiltonian = 0.5 * omega_0 * graph.pauli_matrix(\"Z\") + alpha * graph.pauli_matrix(\"X\")\n",
"\n",
"# Dephasing noise amplitude.\n",
"beta = 2 * np.pi * 20e3 # rad/s\n",
"\n",
"# (Constant) dephasing noise term.\n",
"dephasing = beta * graph.pauli_matrix(\"Z\")\n",
"\n",
"# Target operation.\n",
"target_operator = np.cos(0.5 * omega_0 * duration) * graph.pauli_matrix(\n",
" \"I\"\n",
") - 1j * np.sin(0.5 * omega_0 * duration) * graph.pauli_matrix(\"Z\")\n",
"\n",
"# Target operation node.\n",
"target = graph.target(operator=target_operator)\n",
"\n",
"# Robust infidelity.\n",
"robust_infidelity = graph.infidelity_pwc(\n",
" hamiltonian=hamiltonian,\n",
" noise_operators=[dephasing],\n",
" target=target,\n",
" name=\"robust infidelity\",\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/100 [00:00"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Run the optimization and plot the results.\n",
"\n",
"# Run the optimization.\n",
"optimization_result = qctrl.functions.calculate_optimization(\n",
" graph=graph, cost_node_name=\"robust infidelity\", output_node_names=[\"$\\\\alpha$\"]\n",
")\n",
"\n",
"# Print the optimized value of the cost function.\n",
"print()\n",
"print(f\"Optimized robust cost: {optimization_result.cost:.3e}\")\n",
"\n",
"# Plot the optimized control pulse.\n",
"plot_controls(controls=optimization_result.output)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example: Noisy qubit evolution\n",
"\n",
"In this example, we will represent the following single qubit Hamiltonian as a graph\n",
"$$\n",
"H(t) = \\frac{\\Omega}{2} \\sigma_x + \\delta \\sigma_z + \\beta(t) \\sigma_z,\n",
"$$\n",
"where $\\Omega$ is the qubit Rabi frequency, $\\delta$ is a detuning, and $\\beta(t)$ is a random dephasing noise process induced by a classical bath.\n",
"We will model $\\beta(t)$ as a Wiener process, which physically corresponds to Brownian noise, for example, through instrument errors.\n",
"The goal will be to simulate the qubit's noisy time evolution by using a graph.\n",
"For this purpose, we will add a node to the graph that computes random values corresponding to the Wiener process $\\beta(t)$.\n",
"This is done by using the operations `graph.random_normal`, which produces random samples from a normal distribution, and `graph.cumulative_sum`, which adds to each element of a list the sum of all previous elements."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"# Define system parameters.\n",
"\n",
"# Simulation parameters.\n",
"segment_count = 200\n",
"duration = 30e-8 # s\n",
"sample_times = np.linspace(0.0, duration, segment_count)\n",
"\n",
"# Physical system parameters.\n",
"Omega = 2 * np.pi * 10e6 # rad/s\n",
"delta = 2 * np.pi * 2e4 # rad/s\n",
"average_noise_strength = 2 * np.pi * 1.5e6 # rad/s"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"# Create an empty graph.\n",
"graph = qctrl.create_graph()\n",
"\n",
"# Add respective nodes to the graph.\n",
"\n",
"# Create the Hamiltonian corresponding to the noiseless evolution.\n",
"noiseless_hamiltonian = graph.constant_pwc(\n",
" constant=0.5 * Omega * graph.pauli_matrix(\"X\") + delta * graph.pauli_matrix(\"Z\"),\n",
" duration=duration,\n",
")\n",
"\n",
"# Wiener process which models the dephasing noise.\n",
"\n",
"# Compute random samples from a normal distribution.\n",
"samples = graph.random_normal(\n",
" shape=(segment_count,),\n",
" mean=0.0,\n",
" standard_deviation=average_noise_strength,\n",
" seed=0,\n",
" name=\"samples\",\n",
")\n",
"\n",
"# Build a Wiener process from the randomly chosen samples.\n",
"beta_values = graph.cumulative_sum(samples, name=\"beta values\")\n",
"\n",
"# Create a piecewise-constant signal from beta_vales.\n",
"beta_signal = graph.pwc_signal(\n",
" values=beta_values, duration=duration, name=\"beta signal\"\n",
")\n",
"\n",
"# Create a node corresponding to the dephasing Hamiltonian.\n",
"dephasing = beta_signal * graph.pauli_matrix(\"Z\")\n",
"\n",
"# Define the total Hamiltonian.\n",
"noisy_hamiltonian = noiseless_hamiltonian + dephasing\n",
"\n",
"# Unitary time evolution generated by the total noisy Hamiltonian.\n",
"noisy_unitaries = graph.time_evolution_operators_pwc(\n",
" hamiltonian=noisy_hamiltonian, sample_times=sample_times, name=\"noisy unitaries\"\n",
")\n",
"\n",
"# Initial state of the qubit, |0⟩.\n",
"initial_state = graph.fock_state(2, 0)[:, None]\n",
"\n",
"# Evolved states, |ψ(t)⟩ = U(t) |0⟩.\n",
"evolved_noisy_states = noisy_unitaries @ initial_state\n",
"evolved_noisy_states.name = \"noisy states\""
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/100 [00:00\n",
"