{
"cells": [
{
"cell_type": "markdown",
"id": "1a5e2004",
"metadata": {},
"source": [
"# How to reuse graph definitions in different calculations\n",
"**Reapply graph nodes for multiple applications**\n",
"\n",
"In Boulder Opal, graph definitions can be easily carried over between different graphs.\n",
"For example, the nodes used to create a Hamiltonian can first be used in an optimization task, and then later used in simulation.\n",
"This is especially useful if one is changing properties of the graph that could not be easily batched together, for example, the dimension of the Fock space or the structure of the Hamiltonian.\n",
"If instead you want to perform a simulation using the results of an optimization in a single calculation, see [the relevant user guide](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-perform-optimization-and-simulation-in-the-same-calculation).\n",
"\n",
"Below we show an example of reusing nodes for optimization and simulation in separate graphs."
]
},
{
"cell_type": "markdown",
"id": "53c13704-9f42-47a9-98c0-5ebfc0cfc56d",
"metadata": {},
"source": [
"## Summary workflow\n",
"### 1. Define a function for the nodes to be reused\n",
"For convenience, start by creating a function that contains the nodes to be reused. \n",
"One of the arguments of the function should be `graph` so that the function can be used to add nodes on to the graph.\n",
"The function should also be passed any parameters required to define the nodes.\n",
"After these nodes are added to graph, any nodes required to compute the graph (for example the Hamiltonian) should be returned.\n",
"\n",
"For example, one might define a function to create an infidelity node for a given Hamiltonian and some target:\n",
"```python\n",
"def create_infidelity(graph, hamiltonian):\n",
" target = graph.target(operator=graph.pauli_matrix(\"Y\"))\n",
" return graph.infidelity_pwc(\n",
" hamiltonian=hamiltonian,\n",
" target=target,\n",
" name=\"infidelity\",\n",
" )\n",
"```\n",
"### 2. Define and execute the computational graph\n",
"Set up a [graph object](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-represent-quantum-systems-using-graphs) to carry out some task, be it to perform a [control optimization](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-optimize-controls-in-arbitrary-quantum-systems-using-graphs) or [simulation](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-simulate-quantum-dynamics-for-noiseless-systems-using-graphs), using the previously defined function.\n",
"With the graph object created, an optimization can be run using the `qctrl.functions.calculate_optimization` function or a simulation can be run using the `qctrl.functions.calculate_graph`.\n",
"\n",
"### 3. Define and execute other graphs\n",
"Reusing the convenience function, define a new graph that carries out a related task.\n",
"For example, if the first calculation is an optimization, then the new graph might be a [noisy simulation](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-simulate-quantum-dynamics-subject-to-noise-with-graphs) or a [quasi-static parameter scan](https://docs.q-ctrl.com/boulder-opal/user-guides/how-to-evaluate-control-susceptibility-to-quasi-static-noise) with the optimized results."
]
},
{
"cell_type": "markdown",
"id": "b5d370d7-1fd2-4b1b-9720-601b3c54acec",
"metadata": {},
"source": [
"## Example: Reusing nodes for simulation and optimization of a qubit\n",
"In this example we will implement a Y gate in a noisy single-qubit system. \n",
"The system is described by a Hamiltonian of the form,\n",
"$$\n",
"H(t) = \\alpha(t) \\sigma_{z} + \\frac{1}{2}\\left(\\gamma(t)\\sigma_{-} + \\gamma^*(t)\\sigma_{+}\\right) + \\delta \\sigma_{z} + \\beta(t) \\sigma_{z}\n",
"$$\n",
"where,\n",
"$\\sigma_{z}$ is the Pauli Z operator,\n",
"$\\sigma_{\\pm}$ are the qubit ladder operators,\n",
"$\\alpha(t)$ and $\\gamma(t)$ are, respectively, real and complex optimizable time-dependent controls,\n",
"$\\delta$ is the qubit detuning, and,\n",
"$\\beta(t)$ is a dephasing noise process.\n",
"\n",
"First we will optimize $\\alpha(t)$ and $\\gamma(t)$ to produce a Y gate in the absence of any noise, $\\beta(t)=0$.\n",
"Then we will optimize the controls in the presence of a dephasing amplitude on the order of $\\beta(t)=\\beta_0$.\n",
"Finally we will test how both sets of controls perform in the presence of real stochastic noise sampled from a normal distribution centered at $\\beta_0$. We see that for the robust controls optimized in the presence of constant noise, the associated infidelity is significantly lower than for the controls optimized without any noise. \n",
"\n",
"To reduce the amount of code required nodes are reused by defining functions to create the optimiziable controls, compute the Hamiltonian and to compute infidelity.\n",
"In total three graphs are used, two for optimizating the controls and another for simulation. "
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "75cf8709-2bbd-4ede-b803-85952328298a",
"metadata": {},
"outputs": [],
"source": [
"def create_optimizable_controls(graph):\n",
" \"\"\"Create the optimizable controls α(t) and γ(t).\"\"\"\n",
" # Maximum value for |α(t)|.\n",
" alpha_max = 2 * np.pi * 0.25e6 # rad/s\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",
" # Maximum value for |γ(t)|.\n",
" gamma_max = 2 * np.pi * 0.5e6 # rad/s\n",
"\n",
" # Complex PWC signal representing γ(t)\n",
" gamma = graph.utils.complex_optimizable_pwc_signal(\n",
" segment_count=segment_count,\n",
" duration=duration,\n",
" maximum=gamma_max,\n",
" name=\"$\\\\gamma$\",\n",
" )\n",
" return alpha, gamma\n",
"\n",
"\n",
"def create_noiseless_hamiltonian(graph, alpha_control, gamma_control):\n",
" \"\"\"Create the noiseless Hamiltonian from the controls α(t) and γ(t).\"\"\"\n",
" # Detuning δ.\n",
" delta = 2 * np.pi * 0.25e6 # rad/s\n",
" detuning_hamiltonian = delta * graph.pauli_matrix(\"Z\")\n",
"\n",
" # Control Hamiltonian.\n",
" control_hamiltonian = alpha_control * graph.pauli_matrix(\n",
" \"Z\"\n",
" ) + graph.hermitian_part(gamma_control * graph.pauli_matrix(\"M\"))\n",
"\n",
" # Total Hamiltonian.\n",
" return detuning_hamiltonian + control_hamiltonian\n",
"\n",
"\n",
"def create_infidelity(graph, hamiltonian, noise_operators=None):\n",
" \"\"\"\n",
" Create the infidelity node with the option to pass a noise operator.\n",
" If a noise operator is passed then the operational infidelity plus\n",
" filter function values is calculated.\n",
" \"\"\"\n",
" # Target operation node.\n",
" target = graph.target(operator=graph.pauli_matrix(\"Y\"))\n",
"\n",
" # Compute infidelity.\n",
" graph.infidelity_pwc(\n",
" hamiltonian=hamiltonian,\n",
" noise_operators=noise_operators,\n",
" target=target,\n",
" name=\"infidelity\",\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "567241f1-14a2-4c11-8385-685c6adbdec9",
"metadata": {},
"outputs": [],
"source": [
"# Import packages.\n",
"import numpy as np\n",
"from qctrl import Qctrl\n",
"\n",
"# Start a Boulder Opal session.\n",
"qctrl = Qctrl()\n",
"\n",
"# Pulse parameters.\n",
"segment_count = 50\n",
"duration = 10e-6 # s\n",
"\n",
"# Dephasing noise amplitude.\n",
"beta_0 = 2 * np.pi * 20e3 # rad/s"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "dd728a25-b331-4268-8b61-240f3c87e103",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
" 0%| | 0/100 [00:00