From e23b9ba444ad7ac90e2dc4b85b861b665b87c85f Mon Sep 17 00:00:00 2001 From: Roland-djee <9250798+Roland-djee@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:52:06 +0100 Subject: [PATCH 1/2] [Docs] Docs improvements (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João P. Moutinho <56390829+jpmoutinho@users.noreply.github.com> --- docs/digital_analog_qc/analog-basics.md | 67 ++++---- docs/digital_analog_qc/analog-qubo.md | 128 ++++++++------- docs/digital_analog_qc/daqc-cnot.md | 203 ++++++++++++------------ docs/digital_analog_qc/pulser-basic.md | 111 +++++++------ mkdocs.yml | 2 +- 5 files changed, 268 insertions(+), 243 deletions(-) diff --git a/docs/digital_analog_qc/analog-basics.md b/docs/digital_analog_qc/analog-basics.md index 4bf619c2..015efa7b 100644 --- a/docs/digital_analog_qc/analog-basics.md +++ b/docs/digital_analog_qc/analog-basics.md @@ -16,7 +16,6 @@ where $\Omega$ is the Rabi frequency, $\delta$ is the detuning, $\hat n = \frac{ - [`WaitBlock`][qadence.blocks.analog.WaitBlock] by free-evolving $\mathcal{H}_{\textrm{int}}$ - [`ConstantAnalogRotation`][qadence.blocks.analog.ConstantAnalogRotation] by free-evolving $\mathcal{H}$ - The `wait` operation can be emulated with an $ZZ$- (Ising) or an $XY$-interaction: ```python exec="on" source="material-block" result="json" @@ -97,11 +96,8 @@ print(f"Analog Kron block = {analog_kron}") # markdown-exec: hide ## Fitting a simple function -Analog blocks can indeed be parametrized to, for instance, create small ansatze to fit a sine function. When using the `pyqtorch` backend the -`add_interaction` function is called automatically. As usual, we can choose which -differentiation backend we want to use: autodiff or parameter shift rule (PSR). +Analog blocks can be parametrized in the usual Qadence manner. Like any other parameters, they can be optimized. The next snippet examplifies the creation of an analog and paramertized ansatze to fit a sine function. First, define an ansatz block and an observable: -First we define an ansatz block and an observable ```python exec="on" source="material-block" session="sin" import torch from qadence import Register, FeatureParameter, VariationalParameter @@ -110,19 +106,20 @@ from qadence import wait, chain, add pi = torch.pi -# two qubit register +# A two qubit register. reg = Register.from_coordinates([(0, 0), (0, 12)]) -# analog ansatz with input parameter +# An analog ansatz with an input time parameter. t = FeatureParameter("t") + block = chain( - AnalogRX(pi / 2), + AnalogRX(pi/2.), AnalogRZ(t), wait(1000 * VariationalParameter("theta", value=0.5)), - AnalogRX(pi / 2), + AnalogRX(pi/2), ) -# observable +# Total magnetization observable. obs = add(Z(i) for i in range(reg.n_qubits)) ``` @@ -139,40 +136,49 @@ obs = add(Z(i) for i in range(reg.n_qubits)) ax.scatter(xnp, ynp, **kwargs) ``` -Then we define the dataset we want to train on and plot the initial prediction. +Next, define the dataset to train on and plot the initial prediction. The differentiation mode can be set to either `DiffMode.AD` or `DiffMode.GPSR`. + ```python exec="on" source="material-block" html="1" result="json" session="sin" import matplotlib.pyplot as plt -from qadence import QuantumCircuit, QuantumModel +from qadence import QuantumCircuit, QuantumModel, DiffMode -# define quantum model; including digital-analog emulation +# Define a quantum model including digital-analog emulation. circ = QuantumCircuit(reg, block) -model = QuantumModel(circ, obs, diff_mode="gpsr") +model = QuantumModel(circ, obs, diff_mode=DiffMode.GPSR) +# Time support dataset. x_train = torch.linspace(0, 6, steps=30) +# Function to fit. y_train = -0.64 * torch.sin(x_train + 0.33) + 0.1 +# Initial prediction. y_pred_initial = model.expectation({"t": x_train}) -fig, ax = plt.subplots() -scatter(ax, x_train, y_train, label="Training points", marker="o", color="green") -plot(ax, x_train, y_pred_initial, label="Initial prediction") -plt.legend() +fig, ax = plt.subplots() # markdown-exec: hide +plt.xlabel("Time [μs]") # markdown-exec: hide +plt.ylabel("Sin [arb.]") # markdown-exec: hide +scatter(ax, x_train, y_train, label="Training points", marker="o", color="green") # markdown-exec: hide +plot(ax, x_train, y_pred_initial, label="Initial prediction") # markdown-exec: hide +plt.legend() # markdown-exec: hide from docs import docsutils # markdown-exec: hide print(docsutils.fig_to_html(fig)) # markdown-exec: hide ``` -The rest is the usual PyTorch training routine. +Finally, the classical optimization part is handled by PyTorch: + ```python exec="on" source="material-block" html="1" result="json" session="sin" + +# Use PyTorch built-in functionality. mse_loss = torch.nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=5e-2) - +# Define a loss function. def loss_fn(x_train, y_train): return mse_loss(model.expectation({"t": x_train}).squeeze(), y_train) - -# train +# Number of epochs to train over. n_epochs = 200 +# Optimization loop. for i in range(n_epochs): optimizer.zero_grad() @@ -180,17 +186,16 @@ for i in range(n_epochs): loss.backward() optimizer.step() - # if (i + 1) % 10 == 0: - # print(f"Epoch {i+1:0>3} - Loss: {loss.item()}\n") - -# visualize +# Get and visualize the final prediction. y_pred = model.expectation({"t": x_train}) -fig, ax = plt.subplots() -scatter(ax, x_train, y_train, label="Training points", marker="o", color="green") -plot(ax, x_train, y_pred_initial, label="Initial prediction") -plot(ax, x_train, y_pred, label="Final prediction") -plt.legend() +fig, ax = plt.subplots() # markdown-exec: hide +plt.xlabel("Time [μs]") # markdown-exec: hide +plt.ylabel("Sin [arb.]") # markdown-exec: hide +scatter(ax, x_train, y_train, label="Training points", marker="o", color="green") # markdown-exec: hide +plot(ax, x_train, y_pred_initial, label="Initial prediction") # markdown-exec: hide +plot(ax, x_train, y_pred, label="Final prediction") # markdown-exec: hide +plt.legend() # markdown-exec: hide from docs import docsutils # markdown-exec: hide print(docsutils.fig_to_html(fig)) # markdown-exec: hide assert loss_fn(x_train, y_train) < 0.05 # markdown-exec: hide diff --git a/docs/digital_analog_qc/analog-qubo.md b/docs/digital_analog_qc/analog-qubo.md index 301adac5..8e861631 100644 --- a/docs/digital_analog_qc/analog-qubo.md +++ b/docs/digital_analog_qc/analog-qubo.md @@ -1,10 +1,11 @@ In this notebook we solve a quadratic unconstrained optimization problem with -`qadence` emulated analog interface using the QAOA variational algorithm. The +Qadence emulated analog interface using the QAOA variational algorithm. The problem is detailed in the Pulser documentation [here](https://pulser.readthedocs.io/en/stable/tutorials/qubo.html). +## Define and solve QUBO -??? note "Construct QUBO register (defines `qubo_register_coords` function)" +??? note "Pre-requisite: construct QUBO register" Before we start we have to define a register that fits into our device. ```python exec="on" source="material-block" session="qubo" import torch @@ -55,7 +56,6 @@ problem is detailed in the Pulser documentation ``` -## Define and solve QUBO ```python exec="on" source="material-block" session="qubo" import matplotlib.pyplot as plt @@ -70,7 +70,7 @@ np.random.seed(seed) torch.manual_seed(seed) ``` -The QUBO is defined by weighted connections `Q` and a cost function. +The QUBO problem is initially defined by a graph of weighted connections `Q` and a cost function. ```python exec="on" source="material-block" session="qubo" def cost_colouring(bitstring, Q): @@ -78,12 +78,13 @@ def cost_colouring(bitstring, Q): cost = z.T @ Q @ z return cost - +# Cost function. def cost_fn(counter, Q): cost = sum(counter[key] * cost_colouring(key, Q) for key in counter) return cost / sum(counter.values()) # Divide by total samples +# Weights. Q = np.array( [ [-10.0, 19.7365809, 19.7365809, 5.42015853, 5.42015853], @@ -95,36 +96,42 @@ Q = np.array( ) ``` -Build a register from graph extracted from the QUBO exactly -as you would do with Pulser. +Now, build a weighted register graph from the QUBO definition similarly to what is +done in Pulser. + ```python exec="on" source="material-block" session="qubo" reg = Register.from_coordinates(qubo_register_coords(Q)) ``` The analog circuit is composed of two global rotations per layer. The first rotation corresponds to the mixing Hamiltonian and the second one to the -embedding Hamiltonian. Subsequently we add the Ising interaction term to -emulate the analog circuit. This uses a principal quantum number n=70 for the -Rydberg level under the hood. +embedding Hamiltonian in the QAOA algorithm. Subsequently, there is an Ising interaction term to +emulate the analog circuit. Please note that the Rydberg level is set to 70. + ```python exec="on" source="material-block" result="json" session="qubo" from qadence.transpile.emulate import ising_interaction -LAYERS = 2 -block = chain(*[AnalogRX(f"t{i}") * AnalogRZ(f"s{i}") for i in range(LAYERS)]) +layers = 2 +block = chain(*[AnalogRX(f"t{i}") * AnalogRZ(f"s{i}") for i in range(layers)]) emulated = add_interaction( reg, block, interaction=lambda r, ps: ising_interaction(r, ps, rydberg_level=70) ) -print(emulated) +print(f"emulated = \n") # markdown-exec: hide +print(emulated) # markdown-exec: hide ``` -Sample the model to get the initial solution. -```python exec="on" source="material-block" session="qubo" +Next, an initial solution is computed by sampling the model: + +```python exec="on" source="material-block" result="json" session="qubo" model = QuantumModel(QuantumCircuit(reg, emulated), backend="pyqtorch", diff_mode='gpsr') initial_counts = model.sample({}, n_shots=1000)[0] + +print(f"initial_counts = {initial_counts}") # markdown-exec: hide ``` -The loss function is defined by averaging over the evaluated bitstrings. +Then, the loss function is defined by averaging over the evaluated bitstrings. + ```python exec="on" source="material-block" session="qubo" def loss(param, *args): Q = args[0] @@ -133,63 +140,64 @@ def loss(param, *args): C = model.sample({}, n_shots=1000)[0] return cost_fn(C, Q) ``` -Here we use a gradient-free optimization loop for reaching the optimal solution. + +And a gradient-free optimization loop is used to compute the optimal solution. + ```python exec="on" source="material-block" result="json" session="qubo" -# +# Optimization loop. for i in range(20): - try: - res = minimize( - loss, - args=Q, - x0=np.random.uniform(1, 10, size=2 * LAYERS), - method="COBYLA", - tol=1e-8, - options={"maxiter": 20}, - ) - except Exception: - pass - -# sample the optimal solution + res = minimize( + loss, + args=Q, + x0=np.random.uniform(1, 10, size=2 * layers), + method="COBYLA", + tol=1e-8, + options={"maxiter": 20}, + ) + +# Sample and visualize the optimal solution. model.reset_vparams(res.x) -optimal_count_dict = model.sample({}, n_shots=1000)[0] -print(optimal_count_dict) +optimal_count = model.sample({}, n_shots=1000)[0] +print(f"optimal_count = {optimal_count}") # markdown-exec: hide ``` +Finally, plot the solution: + ```python exec="on" source="material-block" html="1" session="qubo" -fig, axs = plt.subplots(1, 2, figsize=(12, 4)) +fig, axs = plt.subplots(1, 2, figsize=(12, 4)) # markdown-exec: hide -# known solutions to the QUBO +# Known solutions to the QUBO problem. solution_bitstrings=["01011", "00111"] -n_to_show = 20 -xs, ys = zip(*sorted( - initial_counts.items(), - key=lambda item: item[1], - reverse=True -)) -colors = ["r" if x in solution_bitstrings else "g" for x in xs] - -axs[0].set_xlabel("bitstrings") -axs[0].set_ylabel("counts") -axs[0].bar(xs[:n_to_show], ys[:n_to_show], width=0.5, color=colors) -axs[0].tick_params(axis="x", labelrotation=90) -axs[0].set_title("Initial solution") - -xs, ys = zip(*sorted(optimal_count_dict.items(), - key=lambda item: item[1], - reverse=True -)) +n_to_show = 20 # markdown-exec: hide +xs, ys = zip(*sorted( # markdown-exec: hide + initial_counts.items(), # markdown-exec: hide + key=lambda item: item[1], # markdown-exec: hide + reverse=True # markdown-exec: hide +)) # markdown-exec: hide +colors = ["r" if x in solution_bitstrings else "g" for x in xs] # markdown-exec: hide + +axs[0].set_xlabel("bitstrings") # markdown-exec: hide +axs[0].set_ylabel("counts") # markdown-exec: hide +axs[0].bar(xs[:n_to_show], ys[:n_to_show], width=0.5, color=colors) # markdown-exec: hide +axs[0].tick_params(axis="x", labelrotation=90) # markdown-exec: hide +axs[0].set_title("Initial solution") # markdown-exec: hide + +xs, ys = zip(*sorted(optimal_count.items(), # markdown-exec: hide + key=lambda item: item[1], # markdown-exec: hide + reverse=True # markdown-exec: hide +)) # markdown-exec: hide # xs = list(xs) # markdown-exec: hide # assert (xs[0] == "01011" and xs[1] == "00111") or (xs[1] == "01011" and xs[0] == "00111"), print(f"{xs=}") # markdown-exec: hide -colors = ["r" if x in solution_bitstrings else "g" for x in xs] +colors = ["r" if x in solution_bitstrings else "g" for x in xs] # markdown-exec: hide -axs[1].set_xlabel("bitstrings") -axs[1].set_ylabel("counts") -axs[1].bar(xs[:n_to_show], ys[:n_to_show], width=0.5, color=colors) -axs[1].tick_params(axis="x", labelrotation=90) -axs[1].set_title("Optimal solution") -plt.tight_layout() +axs[1].set_xlabel("bitstrings") # markdown-exec: hide +axs[1].set_ylabel("counts") # markdown-exec: hide +axs[1].bar(xs[:n_to_show], ys[:n_to_show], width=0.5, color=colors) # markdown-exec: hide +axs[1].tick_params(axis="x", labelrotation=90) # markdown-exec: hide +axs[1].set_title("Optimal solution") # markdown-exec: hide +plt.tight_layout() # markdown-exec: hide from docs import docsutils # markdown-exec: hide print(docsutils.fig_to_html(fig)) # markdown-exec: hide ``` diff --git a/docs/digital_analog_qc/daqc-cnot.md b/docs/digital_analog_qc/daqc-cnot.md index a3e3db38..7a0cc1f8 100644 --- a/docs/digital_analog_qc/daqc-cnot.md +++ b/docs/digital_analog_qc/daqc-cnot.md @@ -1,19 +1,19 @@ -# DAQC Transform +# `CNOT` with interacting qubits -Digital-analog quantum computing focuses on using simple digital gates combined with more complex and device-dependent analog interactions to represent quantum programs. Such techniques have been shown to be universal for quantum computation [^1]. However, while this approach may have advantages when adapting quantum programs to real devices, known quantum algorithms are very often expressed in a fully digital paradigm. As such, it is also important to have concrete ways to transform from one paradigm to another. +Digital-analog quantum computing focuses on using single qubit digital gates combined with more complex and device-dependent analog interactions to represent quantum programs. This paradigm has been shown to be universal for quantum computation[^1]. However, while this approach may have advantages when adapting quantum programs to real devices, known quantum algorithms are very often expressed in a fully digital paradigm. As such, it is also important to have concrete ways to transform from one paradigm to another. -In this tutorial we will exemplify this transformation starting with the representation of a simple digital CNOT using the universality of the Ising Hamiltonian [^2]. +This tutorial will exemplify the *DAQC transformation* starting with the representation of a simple digital `CNOT` using the universality of the Ising Hamiltonian[^2]. -## CNOT with CPHASE +## `CNOT` with `CPHASE` -Let's look at a single example of how the digital-analog transformation can be used to perform a CNOT on two qubits inside a register of globally interacting qubits. +Let's look at a single example of how the digital-analog transformation can be used to perform a `CNOT` on two qubits inside a register of globally interacting qubits. -First, note that the CNOT can be decomposed with two Hadamard and a CPHASE gate with $\phi=\pi$: +First, note that the `CNOT` can be decomposed with two Hadamard and a `CPHASE` gate with $\phi=\pi$: ```python exec="on" source="material-block" result="json" session="daqc-cnot" import torch -import qadence as qd +from qadence import chain, sample, product_state from qadence.draw import display from qadence import X, I, Z, H, N, CPHASE, CNOT, HamEvo @@ -26,15 +26,15 @@ cnot_gate = CNOT(0, 1) # CNOT decomposed phi = torch.pi -cnot_decomp = qd.chain(H(1), CPHASE(0, 1, phi), H(1)) +cnot_decomp = chain(H(1), CPHASE(0, 1, phi), H(1)) -init_state = qd.product_state("10") +init_state = product_state("10") -print(qd.sample(n_qubits, block = cnot_gate, state = init_state, n_shots = 100)) -print(qd.sample(n_qubits, block = cnot_decomp, state = init_state, n_shots = 100)) +print(f"sample from CNOT gate and 100 shots = {sample(n_qubits, block=cnot_gate, state=init_state, n_shots=100)}") # markdown-exec: hide +print(f"sample from decomposed CNOT gate and 100 shots = {sample(n_qubits, block=cnot_decomp, state=init_state, n_shots=100)}") # markdown-exec: hide ``` -The CPHASE gate is fully diagonal, and can be implemented by exponentiating an Ising-like Hamiltonian, or *generator*, +The `CPHASE` matrix is diagonal, and can be implemented by exponentiating an Ising-like Hamiltonian, or *generator*, $$\text{CPHASE}(i,j,\phi)=\text{exp}\left(-i\phi \mathcal{H}_\text{CP}(i, j)\right)$$ @@ -43,44 +43,46 @@ $$\begin{aligned} &=-N_iN_j \end{aligned}$$ -where we used the number operator $N_i = \frac{1}{2}(I_i-Z_i)$, leading to an Ising-like interaction $N_iN_j$ that is common in neutral-atom systems. Let's rebuild the CNOT using this evolution. +where the number operator $N_i = \frac{1}{2}(I_i-Z_i)=\hat{n}_i$ is used, leading to an Ising-like interaction $\hat{n}_i\hat{n}_j$ realisable in neutral-atom systems. Let's rebuild the `CNOT` using this evolution. + +```python exec="on" source="material-block" result="json" session="daqc-cnot" +from qadence import kron, block_to_tensor -```python exec="on" source="material-block" session="daqc-cnot" # Hamiltonian for the CPHASE gate -h_cphase = (-1.0) * qd.kron(N(0), N(1)) +h_cphase = (-1.0) * kron(N(0), N(1)) -# Exponentiating the Hamiltonian +# Exponentiating and time-evolving the Hamiltonian until t=phi. cphase_evo = HamEvo(h_cphase, phi) # Check that we have the CPHASE gate: -cphase_matrix = qd.block_to_tensor(CPHASE(0, 1, phi)) -cphase_evo_matrix = qd.block_to_tensor(cphase_evo) +cphase_matrix = block_to_tensor(CPHASE(0, 1, phi)) +cphase_evo_matrix = block_to_tensor(cphase_evo) -assert torch.allclose(cphase_matrix, cphase_evo_matrix) +print(f"cphase_matrix == cphase_evo_matrix: {torch.allclose(cphase_matrix, cphase_evo_matrix)}") # markdown-exec: hide ``` -Now that we have checked the generator of the CPHASE gate, we can use it to apply the CNOT: - +Now that the `CPHASE` generator is checked, it can be applied to the `CNOT`: ```python exec="on" source="material-block" result="json" session="daqc-cnot" # CNOT with Hamiltonian Evolution -cnot_evo = qd.chain( +cnot_evo = chain( H(1), cphase_evo, H(1) ) -init_state = qd.product_state("10") +# Initialize state to check CNOTs sample outcomes. +init_state = product_state("10") -print(qd.sample(n_qubits, block = cnot_gate, state = init_state, n_shots = 100)) -print(qd.sample(n_qubits, block = cnot_evo, state = init_state, n_shots = 100)) +print(f"sample cnot_gate = {sample(n_qubits, block = cnot_gate, state = init_state, n_shots = 100)}") # markdown-exec: hide +print(f"sample cnot_evo = {sample(n_qubits, block = cnot_evo, state = init_state, n_shots = 100)}") # markdown-exec: hide ``` -Thus, a CNOT gate can be applied by combining a few single-qubit gates together with a 2-qubit Ising interaction between the control and the target qubit. This is important because it now allows us to exemplify the usage of the Ising transform proposed in the DAQC paper [^2]. In the paper, the transform is described for $ZZ$ interactions. In `qadence` it works both with $ZZ$ and $NN$ interactions. +Thus, a `CNOT` gate can be created by combining a few single-qubit gates together with a two-qubit Ising interaction between the control and the target qubit which is the essence of the Ising transform proposed in the seminal DAQC paper[^2] for $ZZ$ interactions. In Qadence, both $ZZ$ and $NN$ interactions are supported. -## CNOT in an interacting system of 3 qubits +## `CNOT` in an interacting system of three qubits -Consider a simple experimental setup with $n=3$ interacting qubits in a triangular grid. For simplicity let's consider that all qubits interact with each other with an Ising ($NN$) interaction of constant strength $g_\text{int}$. The Hamiltonian for the system can be written by summing this interaction over all pairs: +Consider a simple experimental setup with $n=3$ interacting qubits laid out in a triangular grid. For the sake of simplicity, all qubits interact with each other with an $NN$-Ising interaction of constant strength $g_\text{int}$. The Hamiltonian for the system can be written by summing interaction terms over all pairs: $$\mathcal{H}_\text{sys}=\sum_{i=0}^{n}\sum_{j=0}^{i-1}g_\text{int}N_iN_j,$$ @@ -88,115 +90,119 @@ which in this case leads to only three interaction terms, $$\mathcal{H}_\text{sys}=g_\text{int}(N_0N_1+N_1N_2+N_0N_2)$$ -This generator can be easily built: - +This generator can be easily built in Qadence: ```python exec="on" source="material-block" result="json" session="daqc-cnot" +from qadence import add, kron n_qubits = 3 +# Interaction strength. g_int = 1.0 +# Build a list of interactions. interaction_list = [] for i in range(n_qubits): for j in range(i): - interaction_list.append(g_int * qd.kron(N(i), N(j))) + interaction_list.append(g_int * kron(N(i), N(j))) -h_sys = qd.add(*interaction_list) +h_sys = add(*interaction_list) -print(h_sys) +print(f"h_sys = {h_sys}") # markdown-exec: hide ``` -Now let's consider that the experimental system is fixed, and we cannot isolate the qubits from each other. All we can do is the following: +Now let's consider that the experimental system is fixed, and qubits can not be isolated one from another. The options are: - Turn on or off the global system Hamiltonian. -- Perform single-qubit rotations on individual qubits. - -How can we perform a CNOT on two specific qubits of our choice? - -To perform a *fully digital* CNOT we would need to isolate the control and target qubit from the third one and have those interact to implement the gate directly. While this may be relatively simple for a 3-qubit system, the experimental burden becomes much greater when we start going into the dozens of qubits. +- Perform local single-qubit rotations. -However, with the digital-analog paradigm that is not the case! In fact, we can represent the two qubit Ising interaction required for the CNOT by combining the global system Hamiltonian with a specific set of single-qubit rotations. The full details of this transformation are described in the DAQC paper [^2], and it is available in `qadence` by calling the `daqc_transform` function. +To perform a *fully digital* `CNOT(0,1)`, the interacting control on qubit 0 and target on qubit 1 must be isolated from the third one to implement the gate directly. While this can be achieved for a three-qubit system, it becomes experimentally untractable when scaling the qubit count. -The `daqc_transform` function will essentially return a program that represents the evolution of an Hamiltonian $H_\text{target}$ (*target Hamiltonian*) for a specified time $t_f$ by using only the evolution of an Hamiltonian $H_\text{build}$ (*build Hamiltonian*) for specific intervals of time together with specific single-qubit $X$ rotations. Currently, in `qadence` it is available for resource and target Hamiltonians composed only of $ZZ$ or $NN$ interactions. The generators are parsed by the `daqc_transform` function, the appropriate type is automatically determined, and the appropriate single-qubit detunings and global phases are applied. +However, this is not the case within the digital-analog paradigm. In fact, the two qubit Ising interaction required for the `CNOT` can be represented with a combination of the global system Hamiltonian and a specific set of single-qubit rotations. Full details about this transformation are to be found in the DAQC paper[^2] but a more succint yet in-depth description takes place in the next section. It is conveniently available in Qadence by calling the `daqc_transform` function. -Let's exemplify it for our CNOT problem: +In the most general sense, the `daqc_transform` function will return a circuit that represents the evolution of a target Hamiltonian $\mathcal{H}_\text{target}$ (here the unitary of the gate) until a specified time $t_f$ by using only the evolution of a build Hamiltonian $\mathcal{H}_\text{build}$ (here $\mathcal{H}_\text{sys}$) together with local $X$-gates. In Qadence, `daqc_transform` is applicable for $\mathcal{H}_\text{target}$ and $\mathcal{H}_\text{build}$ composed only of $ZZ$- or $NN$-interactions. These generators are parsed by the `daqc_transform` function and the appropriate type is automatically determined together with the appropriate single-qubit detunings and global phases. +Let's apply it for the `CNOT` implementation: ```python exec="on" source="material-block" html="1" result="json" session="daqc-cnot" -# The target operation -i = 0 # Control -j = 1 # Target +from qadence import daqc_transform, Strategy + +# Settings for the target CNOT operation +i = 0 # Control qubit +j = 1 # Target qubit k = 2 # The extra qubit -# CNOT on control and target, Identity on the extra qubit -cnot_target = qd.kron(CNOT(i, j), I(k)) +# Define the target CNOT operation +# by composing with identity on the extra qubit. +cnot_target = kron(CNOT(i, j), I(k)) -# The two-qubit Ising (NN) interaction for the CPHASE -h_int = (-1.0) * qd.kron(N(i), N(j)) +# The two-qubit NN-Ising interaction term for the CPHASE +h_int = (-1.0) * kron(N(i), N(j)) # Transforming the two-qubit Ising interaction using only our system Hamiltonian -transformed_ising = qd.daqc_transform( - n_qubits = 3, # Total number of qubits in the transformation - gen_target = h_int, # The target Ising generator - t_f = torch.pi, # The target evolution time - gen_build = h_sys, # The building block Ising generator to be used - strategy = "sDAQC", # Currently only sDAQC is implemented - ignore_global_phases = False # Global phases from mapping between Z and N +transformed_ising = daqc_transform( + n_qubits=3, # Total number of qubits in the transformation + gen_target=h_int, # The target Ising generator + t_f=torch.pi, # The target evolution time + gen_build=h_sys, # The building block Ising generator to be used + strategy=Strategy.SDAQC, # Currently only sDAQC is implemented + ignore_global_phases=False # Global phases from mapping between Z and N ) # display(transformed_ising) print(html_string(transformed_ising)) # markdown-exec: hide ``` -The circuit above actually only uses two evolutions of the global Hamiltonian. In the displayed circuit also see other instances of `HamEvo` which account for global-phases and single-qubit detunings related to the mapping between the $Z$ and $N$ operator. Optionally, the application of the global phases can also be ignored, as shown in the input of `daqc_transform`. This will not create exactly the same state or operator matrix in tensor form, but in practice they will be equivalent. - -In general, the mapping of a $n$-qubit Ising Hamiltonian will require at most $n(n-1)$ evolutions. The transformed circuit performs these evolutions for specific times that are computed from the solution of a linear system of equations involving the set of interactions in the target and build Hamiltonians. +The output circuit displays three groups of system Hamiltonian evolutions which account for global-phases and single-qubit detunings related to the mapping between the $Z$ and $N$ operators. Optionally, global phases can be ignored. -In this case the mapping is exact, since we used the *step-wise* DAQC technique (sDAQC). In *banged* DAQC (bDAQC) the mapping is not exact, but is easier to implement on a physical device with always-on interactions such as neutral-atom systems. Currently, only the sDAQC technique is available in `qadence`. +In general, the mapping of a $n$-qubit Ising Hamiltonian to another will require at most $n(n-1)$ evolutions. The transformed circuit performs these evolutions for specific times that are computed from the solution of a linear system of equations involving the set of interactions in the target and build Hamiltonians. -Just as before, we can check that using the transformed Ising circuit we exactly recover the CPHASE gate: +In this case, the mapping is exact when using the *step-wise* DAQC strategy (`Strategy.SDAQC`) available in Qadence. In *banged* DAQC (`Strategy.BDAQC`) the mapping is approximate, but easier to implement on a physical device with always-on interactions such as neutral-atom systems. +Just as before, the transformed Ising circuit can be checked to exactly recover the `CPHASE` gate: -```python exec="on" source="material-block" session="daqc-cnot" +```python exec="on" source="material-block" result="json" session="daqc-cnot" # CPHASE on (i, j), Identity on third qubit: -cphase_matrix = qd.block_to_tensor(qd.kron(CPHASE(i, j, phi), I(k))) +cphase_matrix = block_to_tensor(kron(CPHASE(i, j, phi), I(k))) # CPHASE using the transformed circuit: -cphase_evo_matrix = qd.block_to_tensor(transformed_ising) +cphase_evo_matrix = block_to_tensor(transformed_ising) -# Will fail if global phases are ignored: -assert torch.allclose(cphase_matrix, cphase_evo_matrix) +# Check that it implements the CPHASE. +# Will fail if global phases are ignored. +print(f"cphase_matrix == cphase_evo_matrix : {torch.allclose(cphase_matrix, cphase_evo_matrix)}") # markdown-exec: hide ``` -And we can now build the CNOT gate: +The `CNOT` gate can now finally be built: ```python exec="on" source="material-block" result="json" session="daqc-cnot" -cnot_daqc = qd.chain( +from qadence import equivalent_state, run, sample + +cnot_daqc = chain( H(j), transformed_ising, H(j) ) -# And finally run the CNOT on a specific 3-qubit initial state: -init_state = qd.product_state("101") +# And finally apply the CNOT on a specific 3-qubit initial state: +init_state = product_state("101") -# Check we get an equivalent wavefunction (will still pass if global phases are ignored) -wf_cnot = qd.run(n_qubits, block = cnot_target, state = init_state) -wf_daqc = qd.run(n_qubits, block = cnot_daqc, state = init_state) -assert qd.equivalent_state(wf_cnot, wf_daqc) +# Check we get an equivalent wavefunction +wf_cnot = run(n_qubits, block=cnot_target, state=init_state) +wf_daqc = run(n_qubits, block=cnot_daqc, state=init_state) +print(f"wf_cnot == wf_dacq : {equivalent_state(wf_cnot, wf_daqc)}") # markdown-exec: hide -# Visualize the CNOT bit-flip: -print(qd.sample(n_qubits, block = cnot_target, state = init_state, n_shots = 100)) -print(qd.sample(n_qubits, block = cnot_daqc, state = init_state, n_shots = 100)) +# Visualize the CNOT bit-flip in samples. +print(f"sample cnot_target = {sample(n_qubits, block=cnot_target, state=init_state, n_shots=100)}") # markdown-exec: hide +print(f"sample cnot_dacq = {sample(n_qubits, block=cnot_daqc, state=init_state, n_shots=100)}") # markdown-exec: hide ``` -And we are done! We have effectively performed a CNOT operation on our desired target qubits by using only the global interaction of the system as the building block Hamiltonian, together with single-qubit rotations. Going through the trouble of decomposing a single digital gate into its Ising Hamiltonian is certainly not very practical, but it serves as a proof of principle for the potential of this technique to represent universal quantum computation. In the next example, we will see it applied to the digital-analog Quantum Fourier Transform. +As one can see, a `CNOT` operation has been succesfully implemented on the desired target qubits by using only the global system as the building block Hamiltonian and single-qubit rotations. Decomposing a single digital gate into an Ising Hamiltonian serves as a proof of principle for the potential of this technique to represent universal quantum computation. ## Technical details on the DAQC transformation - The mapping between target generator and final circuit is performed by solving a linear system of size $n(n-1)$ where $n$ is the number of qubits, so it can be computed *efficiently* (i.e., with a polynomial cost in the number of qubits). - The linear system to be solved is actually not invertible for $n=4$ qubits. This is very specific edge case requiring a workaround, that is currently not yet implemented. -- As mentioned, the final circuit has at most $n(n-1)$ slices, so there is at most a polynomial overhead in circuit depth. +- As mentioned, the final circuit has at most $n(n-1)$ slices, so there is at most a quadratic overhead in circuit depth. Finally, and most important to its usage: @@ -213,46 +219,45 @@ def gen_build(g_int): return g_int * (Z(0) @ Z(1)) + 1.0 * (Z(1) @ Z(2)) ``` -And now we perform the DAQC transform by setting `g_int = 1.0`, matching the target Hamiltonian: +And now we perform the DAQC transform by setting `g_int=1.0`, exactly matching the target Hamiltonian: ```python exec="on" source="material-block" html="1" result="json" session="daqc-cnot" -transformed_ising = qd.daqc_transform( - n_qubits = 3, - gen_target = gen_target, - t_f = 1.0, - gen_build = gen_build(g_int = 1.0), +transformed_ising = daqc_transform( + n_qubits=3, + gen_target=gen_target, + t_f=1.0, + gen_build=gen_build(g_int=1.0), ) # display(transformed_ising) print(html_string(transformed_ising)) # markdown-exec: hide ``` -And we get the transformed circuit. What if our build Hamiltonian has a very weak interaction between qubits 0 and 1? +Now, if the interaction between qubits 0 and 1 is weakened in the build Hamiltonian: ```python exec="on" source="material-block" html="1" result="json" session="daqc-cnot" -transformed_ising = qd.daqc_transform( - n_qubits = 3, - gen_target = gen_target, - t_f = 1.0, - gen_build = gen_build(g_int = 0.001), +transformed_ising = daqc_transform( + n_qubits=3, + gen_target=gen_target, + t_f=1.0, + gen_build=gen_build(g_int=0.001), ) # display(transformed_ising) print(html_string(transformed_ising)) # markdown-exec: hide ``` -As we can see, to represent the same interaction between 0 and 1, the slices using the build Hamiltonian need to evolve for much longer, since the target interaction is not sufficiently represented in the building block Hamiltonian. - -In the limit where that interaction is not present at all, the transform will not work: +The times slices using the build Hamiltonian need now to evolve for much longer to represent the same interaction since it is not sufficiently represented in the building block Hamiltonian. +In the limit where that interaction is not present, the transform will not work: ```python exec="on" source="material-block" result="json" session="daqc-cnot" try: - transformed_ising = qd.daqc_transform( - n_qubits = 3, - gen_target = gen_target, - t_f = 1.0, - gen_build = gen_build(g_int = 0.0), + transformed_ising = daqc_transform( + n_qubits=3, + gen_target=gen_target, + t_f=1.0, + gen_build=gen_build(g_int = 0.0), ) except ValueError as error: print("Error:", error) diff --git a/docs/digital_analog_qc/pulser-basic.md b/docs/digital_analog_qc/pulser-basic.md index 19a8dbea..12a55170 100644 --- a/docs/digital_analog_qc/pulser-basic.md +++ b/docs/digital_analog_qc/pulser-basic.md @@ -1,27 +1,26 @@ Qadence offers a direct interface with Pulser[^1], an open-source pulse-level interface written in Python and specifically designed for programming neutral atom quantum computers. -Using directly Pulser requires deep knowledge on pulse-level programming and on how neutral atom devices work. Qadence abstracts this complexity out by using the familiar block-based interface for building pulse sequences in Pulser while leaving the possibility +Using directly Pulser requires advanced knowledge on pulse-level programming and on how neutral atom devices work. Qadence abstracts this complexity out by using the familiar block-based interface for building pulse sequences in Pulser while leaving the possibility to directly manipulate them if required by, for instance, optimal pulse shaping. !!! note The Pulser backend is still experimental and the interface might change in the future. - -Let's see it in action. + Please note that it does not support `DiffMode.AD`. ## Default qubit interaction -When simulating pulse sequences written using Pulser, the underlying Hamiltonian it -constructs is equivalent to a digital-analog quantum computing program with the following interaction -Hamiltonian (see [digital-analog emulation](analog-basics.md) for more details): +When simulating pulse sequences written using Pulser, the underlying constructed Hamiltonian +is equivalent to a digital-analog quantum computing program (see [digital-analog emulation](analog-basics.md) for more details) +with the following interaction term: $$ -\mathcal{H}_{int} = \sum_{i Date: Mon, 16 Oct 2023 10:19:45 +0200 Subject: [PATCH 2/2] Bump actions/checkout from 3 to 4 (#95) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test_fast.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_fast.yml b/.github/workflows/test_fast.yml index 76f52ee1..70436f2a 100644 --- a/.github/workflows/test_fast.yml +++ b/.github/workflows/test_fast.yml @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out Qadence - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Set up Python