diff --git a/demonstrations/ahs_aquila.metadata.json b/demonstrations/ahs_aquila.metadata.json index a2acd21fcc..24f4598958 100644 --- a/demonstrations/ahs_aquila.metadata.json +++ b/demonstrations/ahs_aquila.metadata.json @@ -6,20 +6,24 @@ } ], "dateOfPublication": "2023-05-16T00:00:00+00:00", - "dateOfLastModification": "2024-07-10T00:00:00+00:00", - "categories": ["Quantum Hardware", "Devices and Performance", "Quantum Computing"], + "dateOfLastModification": "2024-07-31T00:00:00+00:00", + "categories": [ + "Quantum Hardware", + "Devices and Performance", + "Quantum Computing" + ], "tags": [], "previewImages": [ - { - "type": "thumbnail", - "uri": "/_static/demonstration_assets/ahs_aquila/thumbnail_tutorial_pulse_on_hardware.png" - } - ], + { + "type": "thumbnail", + "uri": "/_static/demonstration_assets/ahs_aquila/thumbnail_tutorial_pulse_on_hardware.png" + } + ], "seoDescription": "Perform measurements on neutral atom hardware through PennyLane", "doi": "", "canonicalURL": "/qml/demos/ahs_aquila", "references": [ - { + { "id": "Semeghini", "type": "article", "title": "Probing topological spin liquids on a programmable quantum simulator", @@ -27,8 +31,8 @@ "year": "2021", "journal": "", "url": "https://arxiv.org/abs/2104.04119" - }, - { + }, + { "id": "Lienhard", "type": "article", "title": "Observing the Space- and Time-Dependent Growth of Correlations in Dynamically Tuned Synthetic Ising Models with Antiferromagnetic Interactions", @@ -36,16 +40,16 @@ "year": "2018", "journal": "", "url": "https://arxiv.org/abs/1711.01185" - }, - { + }, + { "id": "BraketDevGuide", "type": "webpage", "title": "Hello AHS: Run your first Analog Hamiltonian Simulation", "authors": "Amazon Web Services: Amazon Braket", "journal": "", "url": "https://docs.aws.amazon.com/braket/latest/developerguide/braket-get-started-hello-ahs.html" - }, - { + }, + { "id": "Asthana2022", "type": "article", "title": "AWS Quantum Technologies Blog: Realizing quantum spin liquid phase on an analog Hamiltonian Rydberg simulator", @@ -53,17 +57,17 @@ "year": "2021", "journal": "", "url": "https://aws.amazon.com/blogs/quantum-computing/realizing-quantum-spin-liquid-phase-on-an-analog-hamiltonian-rydberg-simulator/" - } - ], + } + ], "basedOnPapers": [], "referencedByPapers": [], "relatedContent": [ - { + { "type": "demonstration", "id": "tutorial_pasqal", "weight": 1.0 }, - { + { "type": "demonstration", "id": "tutorial_pulse_programming101", "weight": 1.0 @@ -76,4 +80,4 @@ "logo": "/_static/hardware_logos/aws.png" } ] - } \ No newline at end of file +} \ No newline at end of file diff --git a/demonstrations/ahs_aquila.py b/demonstrations/ahs_aquila.py index 86516ef183..a88320e3c5 100644 --- a/demonstrations/ahs_aquila.py +++ b/demonstrations/ahs_aquila.py @@ -512,7 +512,7 @@ def gaussian_fn(p, t): params = [amplitude_params] ts = [0.0, 1.75] -default_qubit = qml.device("default.qubit.jax", wires=3, shots=1000) +default_qubit = qml.device("default.qubit", wires=3, shots=1000) @qml.qnode(default_qubit, interface="jax") diff --git a/demonstrations/learning2learn.metadata.json b/demonstrations/learning2learn.metadata.json index 074820d550..473b6a310f 100644 --- a/demonstrations/learning2learn.metadata.json +++ b/demonstrations/learning2learn.metadata.json @@ -6,7 +6,7 @@ } ], "dateOfPublication": "2021-03-02T00:00:00+00:00", - "dateOfLastModification": "2024-07-03T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Quantum Machine Learning" ], @@ -21,7 +21,9 @@ "doi": "", "canonicalURL": "/qml/demos/learning2learn", "references": [], - "basedOnPapers": ["10.48550/arXiv.1907.05415"], + "basedOnPapers": [ + "10.48550/arXiv.1907.05415" + ], "referencedByPapers": [], "relatedContent": [ { @@ -35,4 +37,4 @@ "weight": 1.0 } ] -} +} \ No newline at end of file diff --git a/demonstrations/learning2learn.py b/demonstrations/learning2learn.py index 3cbf2c6a0e..526f0c5fc7 100644 --- a/demonstrations/learning2learn.py +++ b/demonstrations/learning2learn.py @@ -239,8 +239,7 @@ def circuit(params, **kwargs): def hamiltonian(params, **kwargs): """Evaluate the cost Hamiltonian, given the angles and the graph.""" - # We set the default.qubit.tf device for seamless integration with TensorFlow - dev = qml.device("default.qubit.tf", wires=len(graph.nodes)) + dev = qml.device("default.qubit", wires=len(graph.nodes)) # This qnode evaluates the expectation value of the cost hamiltonian operator cost = qml.QNode(circuit, dev, diff_method="backprop", interface="tf") @@ -373,9 +372,7 @@ def recurrent_loop(graph_cost, n_layers=1, intermediate_steps=False): # We perform five consecutive calls to 'rnn_iteration', thus creating the # recurrent loop. More iterations lead to better results, at the cost of # more computationally intensive simulations. - out0 = rnn_iteration( - [initial_cost, initial_params, initial_h, initial_c], graph_cost - ) + out0 = rnn_iteration([initial_cost, initial_params, initial_h, initial_c], graph_cost) out1 = rnn_iteration(out0, graph_cost) out2 = rnn_iteration(out1, graph_cost) out3 = rnn_iteration(out2, graph_cost) @@ -1030,9 +1027,7 @@ def call(self, inputs): _params = tf.reshape(new_params, shape=(2, self.qaoa_p)) # Cost evaluation, and reshaping to be consistent with other Keras tensors - new_cost = tf.reshape( - tf.cast(self.expectation(_params), dtype=tf.float32), shape=(1, 1) - ) + new_cost = tf.reshape(tf.cast(self.expectation(_params), dtype=tf.float32), shape=(1, 1)) return [new_cost, new_params, new_h, new_c] diff --git a/demonstrations/tutorial_eqnn_force_field.metadata.json b/demonstrations/tutorial_eqnn_force_field.metadata.json index 0bbcfc50e1..958297a51c 100644 --- a/demonstrations/tutorial_eqnn_force_field.metadata.json +++ b/demonstrations/tutorial_eqnn_force_field.metadata.json @@ -3,15 +3,16 @@ "authors": [ { "id": "oriel_kiss" - }, - { + }, + { "id": "isabel_nha_minh_le" } ], "dateOfPublication": "2024-03-12T00:00:00+00:00", - "dateOfLastModification": "2024-03-13T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ - "Quantum Machine Learning", "Quantum Chemistry" + "Quantum Machine Learning", + "Quantum Chemistry" ], "tags": [], "previewImages": [ @@ -57,7 +58,7 @@ "publisher": "APS", "journal": "Phys. Rev. A", "url": "https://journals.aps.org/pra/abstract/10.1103/PhysRevA.103.032430" - } , + }, { "id": "wierichs", "type": "article", @@ -67,7 +68,7 @@ "publisher": "", "journal": "", "url": "https://arxiv.org/abs/2312.06752" - } , + }, { "id": "meyer", "type": "article", @@ -79,10 +80,11 @@ "url": "https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.4.010328" } ], - "basedOnPapers": ["https://arxiv.org/abs/2311.11362"], + "basedOnPapers": [ + "https://arxiv.org/abs/2311.11362" + ], "referencedByPapers": [], "relatedContent": [ - { "type": "demonstration", "id": "tutorial_geometric_qml", @@ -104,4 +106,4 @@ "weight": 1.0 } ] -} +} \ No newline at end of file diff --git a/demonstrations/tutorial_eqnn_force_field.py b/demonstrations/tutorial_eqnn_force_field.py index 66dffd7edd..bad1eadf8a 100644 --- a/demonstrations/tutorial_eqnn_force_field.py +++ b/demonstrations/tutorial_eqnn_force_field.py @@ -133,12 +133,14 @@ import numpy as np import jax -jax.config.update('jax_platform_name', 'cpu') + +jax.config.update("jax_platform_name", "cpu") from jax import numpy as jnp import scipy import matplotlib.pyplot as plt import sklearn + ###################################################################### # Let us construct Pauli matrices, which are used to build the Hamiltonian. X = np.array([[0, 1], [1, 0]]) @@ -148,7 +150,11 @@ sigmas = jnp.array(np.array([X, Y, Z])) # Vector of Pauli matrices sigmas_sigmas = jnp.array( np.array( - [np.kron(X, X), np.kron(Y, Y), np.kron(Z, Z)] # Vector of tensor products of Pauli matrices + [ + np.kron(X, X), + np.kron(Y, Y), + np.kron(Z, Z), + ] # Vector of tensor products of Pauli matrices ) ) @@ -169,7 +175,6 @@ def singlet(wires): qml.CNOT(wires=wires) - ###################################################################### # Next, we need a rotationally equivariant data embedding. We choose to encode a three-dimensional # data point :math:`\vec{x}\in \mathbb{R}^3` via @@ -201,7 +206,6 @@ def equivariant_encoding(alpha, data, wires): qml.QubitUnitary(U, wires=wires, id="E") - ###################################################################### # Finally, we require an equivariant trainable map and an invariant observable. We take the Heisenberg # Hamiltonian, which is rotationally invariant, as an inspiration. We define a single summand of it, @@ -290,12 +294,12 @@ def noise_layer(epsilon, wires): rep = 2 # Number of repeated vertical encoding active_atoms = 2 # Number of active atoms - # Here we only have two active atoms since we fixed the oxygen (which becomes non-active) at the origin +# Here we only have two active atoms since we fixed the oxygen (which becomes non-active) at the origin num_qubits = active_atoms * rep ################################# -dev = qml.device("default.qubit.jax", wires=num_qubits) +dev = qml.device("default.qubit", wires=num_qubits) @qml.qnode(dev, interface="jax") @@ -311,9 +315,7 @@ def vqlm(data, params): # Initial encoding for i in range(num_qubits): - equivariant_encoding( - alphas[i, 0], jnp.asarray(data)[i % active_atoms, ...], wires=[i] - ) + equivariant_encoding(alphas[i, 0], jnp.asarray(data)[i % active_atoms, ...], wires=[i]) # Reuploading model for d in range(D): @@ -437,6 +439,7 @@ def inference(loss_data, opt_state): return E_pred, l + ################################# # **Parameter initialization:** # @@ -466,8 +469,8 @@ def inference(loss_data, opt_state): # We train our VQLM using stochastic gradient descent. -num_batches = 5000 # number of optimization steps -batch_size = 256 # number of training data per batch +num_batches = 5000 # number of optimization steps +batch_size = 256 # number of training data per batch for ibatch in range(num_batches): @@ -492,7 +495,7 @@ def inference(loss_data, opt_state): history_loss = np.array(running_loss) fontsize = 12 -plt.figure(figsize=(4,4)) +plt.figure(figsize=(4, 4)) plt.plot(history_loss[:, 0], "r-", label="training error") plt.plot(history_loss[:, 1], "b-", label="testing error") @@ -512,7 +515,7 @@ def inference(loss_data, opt_state): # could be improved, e.g. by using a deeper model as in the original paper. # -plt.figure(figsize=(4,4)) +plt.figure(figsize=(4, 4)) plt.title("Energy predictions", fontsize=fontsize) plt.plot(energy[indices_test], E_pred, "ro", label="Test predictions") plt.plot(energy[indices_test], energy[indices_test], "k.-", lw=1, label="Exact") diff --git a/demonstrations/tutorial_geometric_qml.metadata.json b/demonstrations/tutorial_geometric_qml.metadata.json index 8a418d212e..df377052c2 100644 --- a/demonstrations/tutorial_geometric_qml.metadata.json +++ b/demonstrations/tutorial_geometric_qml.metadata.json @@ -6,7 +6,7 @@ } ], "dateOfPublication": "2022-10-18T00:00:00+00:00", - "dateOfLastModification": "2024-01-01T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Quantum Machine Learning" ], @@ -58,4 +58,4 @@ "weight": 1.0 } ] -} +} \ No newline at end of file diff --git a/demonstrations/tutorial_geometric_qml.py b/demonstrations/tutorial_geometric_qml.py index 6bea565d4c..587e6984eb 100644 --- a/demonstrations/tutorial_geometric_qml.py +++ b/demonstrations/tutorial_geometric_qml.py @@ -177,7 +177,6 @@ """ - ############################################################################## # # Noughts and Crosses @@ -228,7 +227,7 @@ # will have a single-qubit :math:`R_x(\theta_1)` and :math:`R_y(\theta_2)` # rotation at each point. We will then use :math:`CR_y(\theta_3)` for two-qubit # entangling gates. This implies that, for each encoding, crudely, we'll -# need 18 single-qubit rotation parameters and :math:`\binom{9}{2}=36` +# need 18 single-qubit rotation parameters and :math:`\binom{9}{2}=36` # two-qubit gate rotations. Let's see how, by using symmetries, we can reduce # this. @@ -261,12 +260,12 @@ # :math:`s \in \mathcal{S}`. # # The twirling process applied to an arbitrary unitary will give us a new unitary that commutes with the group as we require. -# We remember that unitary gates typically have the form :math:`W = \exp(-i\theta H)`, where :math:`H` is a Hermitian -# matrix called a *generator*, and :math:`\theta` may be fixed or left as a free parameter. A recipe for creating a unitary -# that commutes with our symmetries is to *twirl the generator of the gate*, i.e., we move from the gate -# :math:`W = \exp(-i\theta H)` to the gate :math:`W' = \exp(-i\theta\mathcal{T}_U[H])`. -# When each term in the twirling formula acts on different qubits, then this unitary -# would further simplify to +# We remember that unitary gates typically have the form :math:`W = \exp(-i\theta H)`, where :math:`H` is a Hermitian +# matrix called a *generator*, and :math:`\theta` may be fixed or left as a free parameter. A recipe for creating a unitary +# that commutes with our symmetries is to *twirl the generator of the gate*, i.e., we move from the gate +# :math:`W = \exp(-i\theta H)` to the gate :math:`W' = \exp(-i\theta\mathcal{T}_U[H])`. +# When each term in the twirling formula acts on different qubits, then this unitary +# would further simplify to # # .. math:: W' = \bigotimes_{s\in\mathcal{S}}U(s)\exp(-i\tfrac{\theta}{\vert\mathcal{S}\vert})U(s)^\dagger. # @@ -291,9 +290,9 @@ # the symmetry action (the sum over the symmetry group actions). Having done this # we can see that for a single-qubit rotation the invariant maps are rotations # on the central qubit, at all the corners, and at all the central -# edges (when their rotation angles are fixed to be the same). +# edges (when their rotation angles are fixed to be the same). # -# As an example consider the following figure, +# As an example consider the following figure, # where we take a :math:`R_x` gate in the corner and then apply all the symmetries # of a square. The result of this twirling leads us to have the same gate at all the corners. @@ -354,7 +353,7 @@ ###################################################################### # Let's now implement this! -# +# # First let's generate some games. # Here we are creating a small program that will play Noughts and Crosses against itself in a random fashion. # On completion, it spits out the winner and the winning board, with noughts as +1, draw as 0, and crosses as -1. @@ -368,6 +367,7 @@ torch.manual_seed(16) random.seed(16) + # create an empty board def create_board(): return torch.tensor([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) @@ -499,12 +499,13 @@ def create_dataset(size_for_each_winner): import matplotlib.pyplot as plt # Set up a nine-qubit system -dev = qml.device("default.qubit.torch", wires=9) +dev = qml.device("default.qubit", wires=9) ob_center = qml.PauliZ(4) ob_corner = (qml.PauliZ(0) + qml.PauliZ(2) + qml.PauliZ(6) + qml.PauliZ(8)) * (1 / 4) ob_edge = (qml.PauliZ(1) + qml.PauliZ(3) + qml.PauliZ(5) + qml.PauliZ(7)) * (1 / 4) + # Now let's encode the data in the following qubit models, first with symmetry @qml.qnode(dev) def circuit(x, p): @@ -662,6 +663,7 @@ def circuit_no_sym(x, p): import math + def encode_game(game): board, res = game x = board * (2 * math.pi) / 3 @@ -679,6 +681,7 @@ def encode_game(game): # :math:`\mathcal{L}(\mathcal{D})=\frac{1}{|\mathcal{D}|} \sum_{(\boldsymbol{g}, \boldsymbol{y}) \in \mathcal{D}}\|\hat{\boldsymbol{y}}(\boldsymbol{g})-\boldsymbol{y}\|_{2}^{2}`. # We need to define this and then we can begin our optimisation. + # calculate the mean square error for this classification problem def cost_function(params, input, target): output = torch.stack([torch.hstack(circuit(x, params)) for x in input]) diff --git a/demonstrations/tutorial_implicit_diff_susceptibility.metadata.json b/demonstrations/tutorial_implicit_diff_susceptibility.metadata.json index 2f828173d9..5b93bb2a63 100644 --- a/demonstrations/tutorial_implicit_diff_susceptibility.metadata.json +++ b/demonstrations/tutorial_implicit_diff_susceptibility.metadata.json @@ -9,7 +9,7 @@ } ], "dateOfPublication": "2022-11-28T00:00:00+00:00", - "dateOfLastModification": "2024-01-01T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Optimization" ], @@ -80,7 +80,9 @@ "url": "http://emis.icm.edu.pl/journals/JIPAM/v4n1/061_02_www.pdf" } ], - "basedOnPapers": ["10.48550/arXiv.2211.13765"], + "basedOnPapers": [ + "10.48550/arXiv.2211.13765" + ], "referencedByPapers": [], "relatedContent": [ { @@ -94,4 +96,4 @@ "weight": 1.0 } ] -} +} \ No newline at end of file diff --git a/demonstrations/tutorial_implicit_diff_susceptibility.py b/demonstrations/tutorial_implicit_diff_susceptibility.py index fe1726a7d1..288e5c7708 100644 --- a/demonstrations/tutorial_implicit_diff_susceptibility.py +++ b/demonstrations/tutorial_implicit_diff_susceptibility.py @@ -251,6 +251,7 @@ *Talk is cheap. Show me the code.* - Linus Torvalds """ + ############################################################################## # Implicit differentiation of ground states in PennyLane # ------------------------------------------------------ @@ -272,7 +273,7 @@ import matplotlib.pyplot as plt -jax.config.update('jax_platform_name', 'cpu') +jax.config.update("jax_platform_name", "cpu") # Use double precision numbers config.update("jax_enable_x64", True) @@ -293,6 +294,7 @@ J = 1.0 gamma = 1.0 + def build_H0(N, J, gamma): """Builds the non-parametric part of the Hamiltonian of a spin system. @@ -321,6 +323,7 @@ def build_H0(N, J, gamma): return H + H0 = build_H0(N, J, gamma) H0_matrix = qml.matrix(H0) A = reduce(add, ((1 / N) * qml.PauliZ(i) for i in range(N))) @@ -329,17 +332,18 @@ def build_H0(N, J, gamma): ############################################################################### # Computing the exact ground state through eigendecomposition # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# # We now define a function that computes the exact ground state using # eigendecomposition. Ideally, we would like to take gradients of this function. # It is possible to simply apply automatic differentiation through this exact # ground-state computation. JAX has an implementation of differentiation # through eigendecomposition. -# +# # Note that we have some points in this plot that are ``nan``, where the gradient # computation through the eigendecomposition does not work. We will see later that # the computation through the VQA is more stable. + @jit def ground_state_solution_map_exact(a: float) -> jnp.array: """The ground state solution map that we want to differentiate @@ -363,11 +367,12 @@ def ground_state_solution_map_exact(a: float) -> jnp.array: ################################################################# # Susceptibility computation through the ground state solution map # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +# # Let us now compute the susceptibility function by taking gradients of the # expectation value of our operator :math:`A` w.r.t `a`. We can use `jax.vmap` # to vectorize the computation over different values of `a`. + @jit def expval_A_exact(a): """Expectation value of ``A`` as a function of ``a`` where we use the @@ -384,6 +389,7 @@ def expval_A_exact(a): eval = jnp.conj(z_star.T) @ A_matrix @ z_star return eval.real + # the susceptibility is the gradient of the expectation value _susceptibility_exact = jax.grad(expval_A_exact) susceptibility_exact = jax.vmap(_susceptibility_exact) @@ -415,7 +421,7 @@ def expval_A_exact(a): # :class:`~pennylane.SimplifiedTwoDesign`, which implements the :doc:`two-design ansatz `. # The ansatz consists of layers of Pauli-Y rotations with # controlled-Z gates. In each layer there are ``N - 1`` parameters for the Pauli-Y gates. -# Therefore, the ansatz is efficient as long as we have enough layers for it +# Therefore, the ansatz is efficient as long as we have enough layers for it # so that is expressive enough to represent the ground-state. # # We set ``n_layers = 5``, but you can redo this example with fewer layers to see @@ -433,7 +439,8 @@ def expval_A_exact(a): n_layers = 5 weights_shape = variational_ansatz.shape(n_layers, N) -dev = qml.device("default.qubit.jax", wires=N, shots=None) +dev = qml.device("default.qubit", wires=N, shots=None) + @jax.jit @qml.qnode(dev, interface="jax") @@ -465,7 +472,7 @@ def energy(z, a): # task. We are looking for variational parameters ``z`` that minimize the energy # function. Once we find a set of parameters ``z``, we wish to compute the # gradient of any function of the ground state w.r.t. ``a``. -# +# # For the implicit differentiation we will use the tool ``jaxopt``, which implements # modular implicit differentiation for various cases; e.g., for fixed-point # functions or optimization. We can directly use ``jaxopt`` to optimize our loss @@ -477,7 +484,7 @@ def energy(z, a): # ``jaxopt.GradientDescent`` optimizer with ``implicit_diff=True``. # We use the seamless integration between PennyLane, JAX # and JAXOpt to compute the susceptibility. -# +# # Since everything is written in JAX, simply calling the # ``jax.grad`` function works as ``jaxopt`` computes the implicit gradients and # plugs it any computation used by ``jax.grad``. We can also just-in-time (JIT) @@ -485,6 +492,7 @@ def energy(z, a): # number of spins or variational ansatz becomes more complicated. Once compiled, # all computes run very fast for any parameters. + def ground_state_solution_map_variational(a, z_init): """The ground state solution map that we want to differentiate through. @@ -498,6 +506,7 @@ def ground_state_solution_map_variational(a, z_init): z_star (jnp.array [jnp.float]): The parameters that define the ground-state solution. """ + @jax.jit def loss(z, a): """Loss function for the ground-state minimization with regularization. @@ -509,9 +518,8 @@ def loss(z, a): Returns: float: The loss value (energy + regularization) """ - return ( - energy(z, a) + 0.001 * jnp.sum(jnp.abs(z[0])) + 0.001 * jnp.sum(jnp.abs(z[1])) - ) + return energy(z, a) + 0.001 * jnp.sum(jnp.abs(z[0])) + 0.001 * jnp.sum(jnp.abs(z[1])) + gd = jaxopt.GradientDescent( fun=loss, stepsize=1e-2, @@ -523,6 +531,7 @@ def loss(z, a): z_star = gd.run(z_init, a=a).params return z_star + a = jnp.array(np.random.uniform(0, 1.0)) # A random ``a`` z_star_variational = ground_state_solution_map_variational(a, z_init) @@ -534,6 +543,7 @@ def loss(z, a): # first call, the function is compiled and subsequent calls become # much faster. + @jax.jit @qml.qnode(dev, interface="jax") def expval_A_variational(z: float) -> float: @@ -549,6 +559,7 @@ def expval_A_variational(z: float) -> float: variational_ansatz(*z, wires=range(N)) return qml.expval(A) + @jax.jit def groundstate_expval_variational(a, z_init) -> float: """Computes ground state and calculates the expectation value of the operator M. @@ -561,6 +572,7 @@ def groundstate_expval_variational(a, z_init) -> float: z_star = ground_state_solution_map_variational(a, z_init) return expval_A_variational(z_star) + susceptibility_variational = jax.jit(jax.grad(groundstate_expval_variational, argnums=0)) z_init = [jnp.array(2 * np.pi * np.random.random(s)) for s in weights_shape] print("Susceptibility", susceptibility_variational(alist[0], z_init)) @@ -599,7 +611,7 @@ def groundstate_expval_variational(a, z_init) -> float: # In future works, it would be important to assess the cost of running implicit # differentiation through an actual quantum computer and determine the quality # of such gradients as a function of noise as explored in a related recent work -# [#Matteo2021]_. +# [#Matteo2021]_. # # References # ---------- @@ -611,38 +623,38 @@ def groundstate_expval_variational(a, z_init) -> float: # The American Mathematical Monthly, 111:3, 216-229 # `10.1080/00029890.2004.11920067 `__, 2004. # -# .. [#Ahmed2022] +# .. [#Ahmed2022] # # Shahnawaz Ahmed, Nathan Killoran, Juan Felipe Carrasquilla Álvarez # "Implicit differentiation of variational quantum algorithms # `arXiv:2211.13765 `__, 2022. -# +# # .. [#Blondel2021] -# -# Mathieu Blondel, Quentin Berthet, Marco Cuturi, Roy Frostig, Stephan Hoyer, Felipe Llinares-López, Fabian Pedregosa, Jean-Philippe Vert +# +# Mathieu Blondel, Quentin Berthet, Marco Cuturi, Roy Frostig, Stephan Hoyer, Felipe Llinares-López, Fabian Pedregosa, Jean-Philippe Vert # "Efficient and modular implicit differentiation" # `arXiv:2105.15183 `__, 2021. # # .. [#implicitlayers] -# +# # Zico Kolter, David Duvenaud, Matt Johnson. # "Deep Implicit Layers - Neural ODEs, Deep Equilibirum Models, and Beyond" # `http://implicit-layers-tutorial.org `__, 2021. -# +# # .. [#Matteo2021] -# +# # Olivia Di Matteo, R. M. Woloshyn # "Quantum computing fidelity susceptibility using automatic differentiation" # `arXiv:2207.06526 `__, 2022. # # .. [#Chang2003] -# +# # Chang, Hung-Chieh, Wei He, and Nagabhushana Prabhu. # "The analytic domain in the implicit function theorem." # `JIPAM. J. Inequal. Pure Appl. Math 4.1 `__, (2003). -# +# # About the authors # ----------------- # .. include:: ../_static/authors/shahnawaz_ahmed.txt -# +# # .. include:: ../_static/authors/juan_felipe_carrasquilla_alvarez.txt diff --git a/demonstrations/tutorial_jax_transformations.metadata.json b/demonstrations/tutorial_jax_transformations.metadata.json index f0188060c4..e8da4900bd 100644 --- a/demonstrations/tutorial_jax_transformations.metadata.json +++ b/demonstrations/tutorial_jax_transformations.metadata.json @@ -6,7 +6,7 @@ } ], "dateOfPublication": "2021-04-12T00:00:00+00:00", - "dateOfLastModification": "2024-01-01T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Devices and Performance" ], @@ -40,4 +40,4 @@ "weight": 1.0 } ] -} +} \ No newline at end of file diff --git a/demonstrations/tutorial_jax_transformations.py b/demonstrations/tutorial_jax_transformations.py index 00fa98e64c..91dba6f564 100644 --- a/demonstrations/tutorial_jax_transformations.py +++ b/demonstrations/tutorial_jax_transformations.py @@ -19,23 +19,24 @@ classical machine learning (ML), many of its transformations are also useful for quantum machine learning (QML), and can be used directly with PennyLane. """ + ############################################################################## # .. figure:: ../_static/demonstration_assets/jax_logo/jax.png # :width: 50% # :align: center # # In this tutorial, we'll go over a number of JAX transformations and show how you can -# use them to build and optimize quantum circuits. We'll show examples of how to +# use them to build and optimize quantum circuits. We'll show examples of how to # do gradient descent with ``jax.grad``, run quantum circuits in parallel # using ``jax.vmap``, compile and optimize simulations with ``jax.jit``, # and control and seed the random nature of quantum computer simulations # with ``jax.random``. By the end of this tutorial you should feel just as comfortable -# transforming quantum computing programs with JAX as you do transforming your +# transforming quantum computing programs with JAX as you do transforming your # neural networks. # # If this is your first time reading PennyLane code, we recommend going through # the :doc:`basic tutorial ` -# first. It's all in vanilla NumPy, so you should be able to +# first. It's all in vanilla NumPy, so you should be able to # easily transfer what you learn to JAX when you come back. # # With that said, we begin by importing PennyLane, JAX, the JAX-provided version of NumPy and @@ -44,6 +45,7 @@ # Added to silence some warnings. from jax.config import config + config.update("jax_enable_x64", True) import jax @@ -56,9 +58,10 @@ # Let's start with a simple example circuit that generates a two-qubit entangled state, # then evaluates the expectation value of the Pauli-Z operator on the first wire. + @qml.qnode(dev, interface="jax") def circuit(param): - # These two gates represent our QML model. + # These two gates represent our QML model. qml.RX(param, wires=0) qml.CNOT(wires=[0, 1]) @@ -67,16 +70,17 @@ def circuit(param): # but for this example we'll just use a single PauliZ. return qml.expval(qml.PauliZ(0)) + ############################################################################## # We can now execute the circuit just like any other python function. print(f"Result: {repr(circuit(0.123))}") ############################################################################## # Notice that the output of the circuit is a JAX ``DeviceArray``. -# In fact, when we use the ``default.qubit`` device, the entire computation +# In fact, when we use the ``default.qubit`` device, the entire computation # is done in JAX, so we can use all of the JAX tools out of the box! # -# Now let's move on to an example of a transformation. The code we wrote above is entirely +# Now let's move on to an example of a transformation. The code we wrote above is entirely # differentiable, so let's calculate its gradient with ``jax.grad``. print("\nGradient Descent") print("---------------") @@ -89,13 +93,13 @@ def circuit(param): # We can then use this grad_circuit function to optimize the parameter value # via gradient descent. -param = 0.123 # Some initial value. +param = 0.123 # Some initial value. print(f"Initial param: {param:0.3f}") print(f"Initial cost: {circuit(param):0.3f}") -for _ in range(100): # Run for 100 steps. - param -= grad_circuit(param) # Gradient-descent update. +for _ in range(100): # Run for 100 steps. + param -= grad_circuit(param) # Gradient-descent update. print(f"Tuned param: {param:0.3f}") print(f"Tuned cost: {circuit(param):0.3f}") @@ -107,8 +111,8 @@ def circuit(param): # model and cost calculation. In the end, almost all QML problems involve tuning some # parameters and minimizing some cost function, just like classical ML. # While classical ML focuses on learning classical systems like language or vision, -# QML is most useful for learning about quantum systems. For example, -# :doc:`finding chemical ground states ` +# QML is most useful for learning about quantum systems. For example, +# :doc:`finding chemical ground states ` # or learning to :doc:`sample thermal energy states `. @@ -120,7 +124,7 @@ def circuit(param): # :width: 50% # :align: center # -# We just showed how we can use gradient methods to learn a parameter value, +# We just showed how we can use gradient methods to learn a parameter value, # but on real quantum computing hardware, calculating gradients can be really expensive and noisy. # Another approach is to use `evolutionary strategies `__ # (ES) to learn these parameters. @@ -146,7 +150,7 @@ def circuit(param): # Let's now set up our ES training loop. The idea is pretty simple. First, we # calculate the expected values of each of our parameters. The cost values # then determine the "weight" of that example. The lower the cost, the larger the weight. -# These batches are then used to generate a new set of parameters. +# These batches are then used to generate a new set of parameters. # Needed to do randomness with JAX. # For more info on how JAX handles randomness, see the documentation. @@ -165,7 +169,7 @@ def circuit(param): costs = vcircuit(params) # Use exp(-x) here since the costs could be negative. - weights = jnp.exp(-costs) + weights = jnp.exp(-costs) mean = jnp.average(params, weights=weights) # We decrease the variance as we converge to a solution. @@ -186,11 +190,11 @@ def circuit(param): # :width: 50% # :align: center # -# JAX is built on top of `XLA `__, a powerful -# numerics library that can optimize and cross compile computations to different hardware, -# including CPUs, GPUs, etc. JAX can compile its computation to XLA via the ``jax.jit`` +# JAX is built on top of `XLA `__, a powerful +# numerics library that can optimize and cross compile computations to different hardware, +# including CPUs, GPUs, etc. JAX can compile its computation to XLA via the ``jax.jit`` # `transform. `__ -# +# # When compiling an XLA program, the compiler will do several rounds of optimization # passes to enhance the performance of the computation. Because of this compilation overhead, # you'll generally find the first time calling the function to be slow, but all subsequent @@ -202,12 +206,14 @@ def circuit(param): print("\n\nJit Example") print("-----------") + @qml.qnode(dev, interface="jax") def circuit(param): qml.RX(param, wires=0) qml.CNOT(wires=[0, 1]) return qml.expval(qml.PauliZ(0)) + # Compiling your circuit with JAX is very easy, just add jax.jit! jit_circuit = jax.jit(circuit) @@ -239,12 +245,12 @@ def circuit(param): # You can see that for the cost of some compilation overhead, we can -# greatly increase our performance of our simulation by orders of magnitude. +# greatly increase our performance of our simulation by orders of magnitude. ############################################################################## # Shots and Sampling with JAX # ---------------------------- -# +# # JAX was designed to enable experiments to be as repeatable as possible. Because of this, # JAX requires us to seed all randomly generated values (as you saw in the above # batching example). Sadly, the universe doesn't allow us to seed real quantum computers, @@ -253,8 +259,8 @@ def circuit(param): # To learn more about how JAX handles randomness, visit their # `documentation site. `__ # -# .. note:: -# This example only applies if you are using ``jax.jit``. Otherwise, PennyLane +# .. note:: +# This example only applies if you are using ``jax.jit``. Otherwise, PennyLane # automatically seeds and resets the random-number-generator for you on each call. # # To set the random number generating key, you'll have to pass the ``jax.random.PRNGKey`` @@ -264,12 +270,12 @@ def circuit(param): print("\n\nRandomness") print("----------") + # Let's create our circuit with randomness and compile it with jax.jit. @jax.jit def circuit(key, param): # Notice how the device construction now happens within the jitted method. - # Also note the added '.jax' to the device path. - dev = qml.device("default.qubit.jax", wires=2, shots=10, prng_key=key) + dev = qml.device("default.qubit", wires=2, shots=10, seed=key) # Now we can create our qnode within the circuit function. @qml.qnode(dev, interface="jax", diff_method=None) @@ -277,8 +283,10 @@ def my_circuit(): qml.RX(param, wires=0) qml.CNOT(wires=[0, 1]) return qml.sample(qml.PauliZ(0)) + return my_circuit() + key1 = jax.random.PRNGKey(0) key2 = jax.random.PRNGKey(1) @@ -292,9 +300,9 @@ def my_circuit(): ################################################ # Closing Remarks # ---------------- -# By now, using JAX with PennyLane should feel very natural. They -# complement each other very nicely; JAX with its powerful transforms, and PennyLane -# with its easy access to quantum computers. We're still in early days of +# By now, using JAX with PennyLane should feel very natural. They +# complement each other very nicely; JAX with its powerful transforms, and PennyLane +# with its easy access to quantum computers. We're still in early days of # development, but we hope to continue to grow our ecosystem around JAX, # and by extension, grow JAX into quantum computing and quantum machine learning. # The future looks bright for this field, and we're excited to see what you build! @@ -302,4 +310,4 @@ def my_circuit(): # # About the author # ---------------- -# .. include:: ../_static/authors/chase_roberts.txt \ No newline at end of file +# .. include:: ../_static/authors/chase_roberts.txt diff --git a/demonstrations/tutorial_neutral_atoms.metadata.json b/demonstrations/tutorial_neutral_atoms.metadata.json index ba6a08ff7f..abbab6500d 100644 --- a/demonstrations/tutorial_neutral_atoms.metadata.json +++ b/demonstrations/tutorial_neutral_atoms.metadata.json @@ -6,7 +6,7 @@ } ], "dateOfPublication": "2023-05-30T00:00:00+00:00", - "dateOfLastModification": "2024-03-04T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Quantum Hardware", "Quantum Computing" diff --git a/demonstrations/tutorial_neutral_atoms.py b/demonstrations/tutorial_neutral_atoms.py index 6bf945e054..9fa70a6fe0 100644 --- a/demonstrations/tutorial_neutral_atoms.py +++ b/demonstrations/tutorial_neutral_atoms.py @@ -71,7 +71,7 @@ # so they can't be affected by electric fields. How can we even hope to manipulate them individually? # It turns out that the technology to do this has been around for decades [#Tweezers1985]_. # **Optical tweezers**—highly focused laser beams—can grab small objects and hold them in place, no -# need to charge them! Let's see how they are able to do this. +# need to charge them! Let's see how they are able to do this. # # Laser beams are nothing but electromagnetic waves, that is, oscillating electric and magnetic # fields. It would seem that a neutral atom could not be affected by them—but it can! To understand how, we need @@ -95,7 +95,7 @@ # .. # # Electric and magnetic fields are more effective in atoms that are larger in size and whose electrons can reach high energy -# levels. Atoms with these features are known as **Rydberg atoms** and, as we will see later, their extra sensitivity to +# levels. Atoms with these features are known as **Rydberg atoms** and, as we will see later, their extra sensitivity to # electric fields is also necessary to implemenent some quantum gates. # # In the last decade, optical tweezer technology has evolved to the point where we can move atoms around @@ -240,9 +240,9 @@ # .. # # .. note:: -# +# # What about atoms in the state :math:`\vert \bar{0}\rangle`? Wouldn't they become excited as well? -# We can actually choose the energy level :math:`\vert h\rangle` such that the transition +# We can actually choose the energy level :math:`\vert h\rangle` such that the transition # :math:`\vert \bar{0}\rangle \rightarrow \vert h\rangle` wouldn't conserve angular momentum, # so it would be suppressed. # @@ -277,9 +277,9 @@ # Here, the **detuning** :math:`\delta(t)` is defined as the difference between the photon's energy and the energy :math:`E_{01}` # needed to transition between the ground state :math:`\lvert 0 \rangle` and the excited state # :math:`\lvert 1 \rangle:` -# +# # .. math:: -# +# # \delta(t) = \hbar\nu(t)-E_{01}. # # We will call :math:`H_d` the **drive Hamiltonian**, since the electronic states of the atoms are being @@ -294,7 +294,7 @@ # # The mathematical expression of the Hamiltonian tells us that the time evolution depends on # the shape of the pulse, which we can control pretty much arbitrarily as long as it's finite in duration. -# We must choose a pulse shape that starts and dies off smoothly. It turns out that one of the best choices is +# We must choose a pulse shape that starts and dies off smoothly. It turns out that one of the best choices is # the *Blackman window* pulse, which minimizes noise [#Pulser2022]_. # The amplitude of a Blackman pulse of duration :math:`T` is given by # @@ -361,7 +361,7 @@ def blackman_window(peak, time): detuning = 0 # For now, let's act on only one neutral atom -single_qubit_dev = qml.device("default.qubit.jax", wires=1) +single_qubit_dev = qml.device("default.qubit", wires=1) @qml.qnode(single_qubit_dev) @@ -376,7 +376,7 @@ def state_evolution(): print("The final state is {}".format(state_evolution().round(2))) ############################################################################## # -# We see that the electronic state changes indeed. As a sanity check, let's see what happens when the detuning is +# We see that the electronic state changes indeed. As a sanity check, let's see what happens when the detuning is # large, such that we expect not to drive the transition. # Choose some arbitrary parameters @@ -503,7 +503,7 @@ def neutral_atom_RY(theta): # # for a system of :math:`N` atoms. Here, :math:`\hat{n}_{i}=(\mathbb{I}+\sigma^{z}_{i})/2,` :math:`C_6` is a coupling constant that # describes the interaction strength between the atoms, and :math:`R_{ij}` is the distance between atom :math:`i` and atom :math:`j.` -# If we add a pulse that addresses the transition between :math:`\vert 0\rangle` and :math:`\vert r\rangle,` the full Hamiltonian for :math:`N` +# If we add a pulse that addresses the transition between :math:`\vert 0\rangle` and :math:`\vert r\rangle,` the full Hamiltonian for :math:`N` # atoms is given by # # .. math:: @@ -542,8 +542,7 @@ def H_i(distance, coupling): def energy_gap(distance): - - """Calculates the energy eigenvalues for the full Hamiltonian, as a function + """Calculates the energy eigenvalues for the full Hamiltonian, as a function of the distance between the atoms.""" # create the terms @@ -551,7 +550,7 @@ def energy_gap(distance): interaction_term = H_i(distance, coupling) # combine and evaluate the Hamiltonian with peak and time parameters - H = (interaction_term + drive_term)([peak], time) + H = (interaction_term + drive_term)([peak], time) # Calculate the eigenvalues for the full Hamiltonian eigenvalues = jnp.linalg.eigvals(qml.matrix(H)) @@ -604,6 +603,7 @@ def energy_gap(distance): # order. Combined with the effects of the Rydberg blockade, this pulse combination will implement the desired gate. To see this, # let's code the pulses needed first. + def two_pi_pulse(distance, coupling, wires=[0]): # Build full Hamiltonian @@ -628,7 +628,8 @@ def pi_pulse(distance, coupling, wires=[0]): # Then, let's see the effect the sequence of pulses has on the :math:`\vert 00 \rangle` state when the atoms are close enough. # -dev_two_qubits = qml.device("default.qubit.jax", wires=2) +dev_two_qubits = qml.device("default.qubit", wires=2) + @qml.qnode(dev_two_qubits) def neutral_atom_CZ(distance, coupling): @@ -655,8 +656,8 @@ def neutral_atom_CZ(distance, coupling): ############################################################################## # # The effect is to multiply the two-qubit state by :math:`-1`, which doesn't happen without the Rydberg blockade! Indeed, when the atoms -# are far away from each other, each individual atomic state gets multiplied by :math:`-1.` Therefore, there would be -# no total phase change since the two-atom state gains a multiplier of :math:`(-1)\times(-1)=1`. It turns out that the Rydberg +# are far away from each other, each individual atomic state gets multiplied by :math:`-1.` Therefore, there would be +# no total phase change since the two-atom state gains a multiplier of :math:`(-1)\times(-1)=1`. It turns out that the Rydberg # blockade is only important when the initial state is :math:`\vert 00 \rangle.` # # .. figure:: ../_static/demonstration_assets/neutral_atoms/control_z00.png @@ -722,7 +723,7 @@ def neutral_atom_CZ(distance, coupling): # # The method shown here is only one of the ways to implement the :math:`CZ` gate. Here, we have chosen to encode the qubits # in a ground and a hyperfine state, which allows for simplicity. Depending on the hardware, one may also choose -# to encode the qubits in a ground state and a Rydberg state, or two Rydberg states. The Rydberg blockade is also +# to encode the qubits in a ground state and a Rydberg state, or two Rydberg states. The Rydberg blockade is also # the main phenomenon that allows for the implementation of two-qubit gates in these realizations, but the details # are a bit different [#Morgado2011]_. # @@ -736,8 +737,8 @@ def neutral_atom_CZ(distance, coupling): # An important issue to deal with in quantum hardware in general. Quantum states are short-lived in the presence of external # influences. We can never achieve a perfect vacuum in the chamber, and the particles and charges around the atoms will destroy # our carefully crafted quantum states in a matter of microseconds. While our qubit states are long-lived, the auxiliary Rydberg -# state is more prone to decohere, which limits the amount of computations we can perform in a succession. Overall -# improving our register preparation, gates, and measurement protocols is of the essence to make more progress on +# state is more prone to decohere, which limits the amount of computations we can perform in a succession. Overall +# improving our register preparation, gates, and measurement protocols is of the essence to make more progress on # neutral-atom technology. # # While we are able to trap many atoms with our current laser technology, scaling optical tweezer arrays to thousands of qubits @@ -747,14 +748,14 @@ def neutral_atom_CZ(distance, coupling): # but such technology is still being developed. Another solution is to use photons through optical fibres to communicate between # different processors, allowing for further connectivity and scalability. # -# Another issue with scalability is the preparation times of the registers. While, with hundreds of qubits, we can still prepare +# Another issue with scalability is the preparation times of the registers. While, with hundreds of qubits, we can still prepare # and arrange the atoms in reasonable times, it becomes increasingly costly the more atoms we have. And we do need # to reprepare the neutral atom array when we are done with a computation. It's not as easy as moving the atoms around -# faster—if we try to move the atoms around too fast, they will escape from the traps! Therefore, engineers are working -# on more efficient ways to move the tweezers around, minimizing the number of steps needed to prepare the initial +# faster—if we try to move the atoms around too fast, they will escape from the traps! Therefore, engineers are working +# on more efficient ways to move the tweezers around, minimizing the number of steps needed to prepare the initial # state [#NeutralHardware2023]_. # -# Finally, let us remark that there are some nuances with gate implementation—it's not nearly as simple in real-life as it is in theory. +# Finally, let us remark that there are some nuances with gate implementation—it's not nearly as simple in real-life as it is in theory. # It is not easy to address individual atoms with the driving laser pulses. This is necessary for universal quantum computing, as we saw in # the previous section. True local atom drives are still in the works, but even without them, we can still use these non-universal devices # for applications in quantum simulation. @@ -764,9 +765,9 @@ def neutral_atom_CZ(distance, coupling): # # Neutral-atom quantum hardware is a promising and quickly developing technology which we should keep an eye on. The ability to # easily create custom qubit topologies and the coherence time of the atoms are its main strong points, and its weaknesses are -# actually no too different from other qubit-based architectures. We can easily program neutral-atom devices using pulses, -# for which PennyLane is of great help. If you want to -# learn more, check out our tutorials on the :doc:`Aquila device, ` :doc:`neutral atom configurations, ` and +# actually no too different from other qubit-based architectures. We can easily program neutral-atom devices using pulses, +# for which PennyLane is of great help. If you want to +# learn more, check out our tutorials on the :doc:`Aquila device, ` :doc:`neutral atom configurations, ` and # :doc:`pulse programming `. And do take a look # at the references below to dive into much more detail about the topics introduced here. # @@ -786,11 +787,11 @@ def neutral_atom_CZ(distance, coupling): # (`arXiv `__) # # .. [#Tweezers1985] -# +# # A. Ashkin, J. M. Dziedzic, J. E. Bjorkholm, and Steven Chu. (1986) -# "Observation of a single-beam gradient force optical trap for dielectric particles", -# Opt. Lett. 11, 288-290 -# +# "Observation of a single-beam gradient force optical trap for dielectric particles", +# Opt. Lett. 11, 288-290 +# # .. [#AtomComputing] # # Atom Computing (May 23, 2023). Quantum Computing Technology, @@ -819,5 +820,3 @@ def neutral_atom_CZ(distance, coupling): # About the author # ---------------- # .. include:: ../_static/authors/alvaro_ballon.txt - - diff --git a/demonstrations/tutorial_optimal_control.metadata.json b/demonstrations/tutorial_optimal_control.metadata.json index 8dc35c3eb3..4b9889462c 100644 --- a/demonstrations/tutorial_optimal_control.metadata.json +++ b/demonstrations/tutorial_optimal_control.metadata.json @@ -6,7 +6,7 @@ } ], "dateOfPublication": "2023-08-08T00:00:00+00:00", - "dateOfLastModification": "2024-01-01T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Optimization", "Quantum Computing", @@ -93,5 +93,4 @@ "weight": 1.0 } ] -} - +} \ No newline at end of file diff --git a/demonstrations/tutorial_optimal_control.py b/demonstrations/tutorial_optimal_control.py index 390812a5ac..3db08d2489 100644 --- a/demonstrations/tutorial_optimal_control.py +++ b/demonstrations/tutorial_optimal_control.py @@ -242,6 +242,7 @@ R_k(t, (\Omega, t_0, t_1), k)= \Omega [1+\exp(-k (t-t_0))+\exp(-k (t_1-t))+\exp(-k(t_1-t_0))]^{-1}. """ + import jax from jax import numpy as jnp @@ -565,6 +566,7 @@ def run_adam(profit_fn, grad_fn, params, learning_rate, num_steps): colors = {0: "#70CEFF", 1: "#C756B2", 2: "#FDC357"} dashes = {"X": [10, 0], "Y": [2, 2, 10, 2], "Z": [6, 2]} + def plot_optimal_pulses(hist, pulse_fn, ops, T, target_name): _, profit_hist = list(zip(*hist)) fig, axs = plt.subplots(2, 1, figsize=(10, 9), gridspec_kw={"hspace": 0.0}, sharex=True) @@ -696,7 +698,7 @@ def profit(params): # flip the third qubit, returning a probability of one in the last entry # and zeros elsewhere. -dev = qml.device("default.qubit.jax", wires=3) +dev = qml.device("default.qubit", wires=3) @qml.qnode(dev, interface="jax") diff --git a/demonstrations/tutorial_pulse_programming101.metadata.json b/demonstrations/tutorial_pulse_programming101.metadata.json index 26c61441d4..a0891615dc 100644 --- a/demonstrations/tutorial_pulse_programming101.metadata.json +++ b/demonstrations/tutorial_pulse_programming101.metadata.json @@ -6,7 +6,7 @@ } ], "dateOfPublication": "2023-03-08T00:00:00+00:00", - "dateOfLastModification": "2024-01-01T00:00:00+00:00", + "dateOfLastModification": "2024-07-31T00:00:00+00:00", "categories": [ "Quantum Hardware", "Quantum Computing" diff --git a/demonstrations/tutorial_pulse_programming101.py b/demonstrations/tutorial_pulse_programming101.py index 707cb8c5a4..ff3f617446 100644 --- a/demonstrations/tutorial_pulse_programming101.py +++ b/demonstrations/tutorial_pulse_programming101.py @@ -71,31 +71,34 @@ jax.config.update("jax_enable_x64", True) jax.config.update("jax_platform_name", "cpu") + def f1(p, t): # polyval(p, t) evaluates a polynomial of degree N=len(p) # i.e. p[0]*t**(N-1) + p[1]*t**(N-2) + ... + p[N-2]*t + p[N-1] return jnp.polyval(p, t) + def f2(p, t): return p[0] * jnp.sin(p[1] * t) + Ht = f1 * qml.PauliX(0) + f2 * qml.PauliY(1) ############################################################################## # This constructs a :class:`~pennylane.pulse.ParametrizedHamiltonian`. Note that the ``callable`` functions ``f1`` and ``f2`` -# are expected to have the fixed signature ``(p, t)``. When calling the :class:`~pennylane.pulse.ParametrizedHamiltonian`, +# are expected to have the fixed signature ``(p, t)``. When calling the :class:`~pennylane.pulse.ParametrizedHamiltonian`, # a ``tuple`` or ``list`` of the parameters for each of the functions is passed in the same # order the Hamiltonian was constructed. -p1 = jnp.ones(5) # parameters for f1 -p2 = jnp.array([1.0, jnp.pi]) # parameters for f2 -t = 0.5 # some fixed point in time -print(Ht((p1, p2), t)) # order of parameters p1, p2 matters +p1 = jnp.ones(5) # parameters for f1 +p2 = jnp.array([1.0, jnp.pi]) # parameters for f2 +t = 0.5 # some fixed point in time +print(Ht((p1, p2), t)) # order of parameters p1, p2 matters ############################################################################## # We can construct general Hamiltonians of the form :math:`\sum_i H_i^d + \sum_i f_i(p_i, t) H_i` # using :func:`qml.dot `. Such a time-dependent Hamiltonian consists of time-independent drift terms :math:`H_i^d` -# and time-dependent control terms :math:`f_i(p_i, t) H_i` with scalar complex-valued functions :math:`f_i(p, t).` +# and time-dependent control terms :math:`f_i(p_i, t) H_i` with scalar complex-valued functions :math:`f_i(p, t).` # In the following we are going to construct :math:`\sum_i X_i X_{i+1} + \sum_i f_i(p_i, t) Z_i` with :math:`f_i(p_i, t) = \sin(p_i^0 t) + \sin(p_i^1 t) \forall i` as an example: coeffs = [1.0] * 2 @@ -107,7 +110,7 @@ def f2(p, t): # random coefficients key = jax.random.PRNGKey(777) -subkeys = jax.random.split(key, 3) # create list of 3 subkeys +subkeys = jax.random.split(key, 3) # create list of 3 subkeys params = [jax.random.uniform(subkeys[i], shape=[2], maxval=5) for i in range(3)] print(Ht(params, 0.5)) @@ -135,7 +138,7 @@ def f2(p, t): # gate :math:`U(t_0, t_1)`, which implicitly depends on the parameters ``p``. The objective of the program # is then to compute the expectation value of some objective Hamiltonian ``H_obj`` (here :math:`\sum_i Z_i` as a simple example). -dev = qml.device("default.qubit.jax", range(4)) +dev = qml.device("default.qubit", range(4)) ts = jnp.array([0.0, 3.0]) H_obj = sum([qml.PauliZ(i) for i in range(4)]) @@ -190,7 +193,7 @@ def qnode(params): # the corresponding piece-wise-constant function sampled at ``100`` different points in time. key = jax.random.PRNGKey(777) -subkeys = jax.random.split(key, 2) # creates a list of two sub-keys +subkeys = jax.random.split(key, 2) # creates a list of two sub-keys theta0 = jax.random.uniform(subkeys[0], shape=[4], maxval=5) theta1 = jax.random.uniform(subkeys[1], shape=[10], maxval=5) theta = [theta0, theta1] @@ -241,18 +244,22 @@ def qnode(params): # The order of magnitude of the resonance frequencies :math:`\omega_q` and coupling strength :math:`g_{pq}` are taken from [#Mitei]_ (in GHz). # Let us construct the Hamiltonian in PennyLane: + def a(wires): return 0.5 * qml.PauliX(wires) + 0.5j * qml.PauliY(wires) + def ad(wires): return 0.5 * qml.PauliX(wires) - 0.5j * qml.PauliY(wires) + omega = 2 * jnp.pi * jnp.array([4.8080, 4.8333]) g = 2 * jnp.pi * jnp.array([0.01831, 0.02131]) H_D = qml.dot(omega, [ad(i) @ a(i) for i in range(n_wires)]) H_D += qml.dot( - g, [ad(i) @ a((i + 1) % n_wires) + ad((i + 1) % n_wires) @ a(i) for i in range(n_wires)] + g, + [ad(i) @ a((i + 1) % n_wires) + ad((i + 1) % n_wires) @ a(i) for i in range(n_wires)], ) ############################################################################## @@ -271,9 +278,11 @@ def ad(wires): # (as is done in [#Mitei]_). We achieve this by normalizing the respective quantities with a shifted sigmoid :math:`\mathcal{N}(x) = \frac{1 - e^{-x}}{1 + e^{-x}}`, # which ensures differentiability. + def normalize(x): """Differentiable normalization to +/- 1 outputs (shifted sigmoid)""" - return (1 - jnp.exp(-x))/(1 + jnp.exp(-x)) + return (1 - jnp.exp(-x)) / (1 + jnp.exp(-x)) + # Because ParametrizedHamiltonian expects each callable function to have the signature # f(p, t) but we have additional parameters it depends on, we create a wrapper function @@ -283,7 +292,7 @@ def wrapped(p, t): # The first len(p)-1 values of the trainable params p characterize the pwc function amp = qml.pulse.pwc(T)(p[:-1], t) # The amplitude is normalized to maximally reach +/-20MHz (0.02GHz) - amp = 0.02*normalize(amp) + amp = 0.02 * normalize(amp) # The last value of the trainable params p provides the drive frequency deviation # We normalize as the difference to drive can maximally be +/-1 GHz @@ -293,6 +302,7 @@ def wrapped(p, t): return wrapped + duration = 15.0 fs = [drive_field(duration, omega[i], 1.0) for i in range(n_wires)] @@ -313,7 +323,8 @@ def wrapped(p, t): ############################################################################## # Now we define the ``qnode`` that computes the expectation value of the molecular Hamiltonian. -dev = qml.device("default.qubit.jax", wires=range(n_wires)) +dev = qml.device("default.qubit", wires=range(n_wires)) + @qml.qnode(dev, interface="jax") def qnode(theta, t=duration): @@ -321,6 +332,7 @@ def qnode(theta, t=duration): qml.evolve(H_pulse)(params=(*theta, *theta), t=t) return qml.expval(H_obj) + value_and_grad = jax.jit(jax.value_and_grad(qnode)) ############################################################################## @@ -330,13 +342,13 @@ def qnode(theta, t=duration): # It has been shown that the loss landscapes of pulse programs are trap-free for a variety of conditions and loss functions, including ours [#Russell2016]_. # In practice however, we see that the optimization is senstive to the initial values of the parameters and the optimization strategy. # In particular, we often find ourselves with very slow progress during optimization, indicating wide flat regions in the loss landscape. -# This can be salvaged by increasing the learning rate. Sometimes, it proved advantageous to increase the learning rate after an +# This can be salvaged by increasing the learning rate. Sometimes, it proved advantageous to increase the learning rate after an # initial finer search for a better starting point. Further, we note that with the increase in the number of parameters due to the continuous evolution, # the optimization becomes harder. # # Whether or not that is due to the increased parameter search space or an inherent effect of pulse programs like barren plateaus in variational quantum circuits # is to be determined in future work. -# +# # We systematically tried a variety of combinations of learning rate schedule, optimizer, and initial values. Here, we provide one possible choice leading to good results. # # We choose ``t_bins = 100`` segments for the piece-wise-constant parametrization of the pulses. @@ -344,7 +356,7 @@ def qnode(theta, t=duration): t_bins = 100 # number of time bins key = jax.random.PRNGKey(999) -theta = 0.9*jax.random.uniform(key, shape=jnp.array([n_wires, t_bins+1])) +theta = 0.9 * jax.random.uniform(key, shape=jnp.array([n_wires, t_bins + 1])) import optax from datetime import datetime @@ -400,7 +412,7 @@ def qnode(theta, t=duration): ############################################################################## # We can also visualize the envelopes for each qubit in time. -# We only plot the real amplitude :math:`\Omega(t)` and indicate the deviation +# We only plot the real amplitude :math:`\Omega(t)` and indicate the deviation # :math:`\Delta \nu_q = \omega_q - \nu_q` of the drive frequency :math:`\nu_q` from the qubit frequency :math:`\omega_q` # in the labels. @@ -411,7 +423,7 @@ def qnode(theta, t=duration): for n in range(n_channels): ax = axs[n] label = f"$\\Delta \\nu_{n}$: {normalize(theta[n][-1]):.3}" - ax.plot(ts, 0.02*normalize(theta[n][:-1]), ".:", label=label) + ax.plot(ts, 0.02 * normalize(theta[n][:-1]), ".:", label=label) ax.set_ylabel(f"$amp_{n}$ (GHz)") ax.legend() ax.set_xlabel("t (ns)")