qml.Special Unitary with qml.ctrl not working with Torch gradient

Hello! I’m trying to run a controlled operation on a SU(N) matrix generated via SpecialUnitary. I’m using the Torch interface and want to compute the gradient via this operation. I’m getting the error displayed below. As far as I understand, this is because ctrl is trying to get an explicit form of the matrix. This seems inefficient and would be something I’d like to avoid. Please help in this reagrds.

def func(extra_w, acting_qubits, control_qubits, num_layer):
    def to_int_list(wires):
        return [int(w.item()) if hasattr(w, "item") else int(w) for w in wires]

    acting_qubits = to_int_list(acting_qubits)
    control_qubits= to_int_list(control_qubits)
    measurement_combination = itertools.product('01', repeat = num_layer)

    for i, control_values in enumerate( measurement_combination):
      controlled_SU = qml.ctrl(qml.SpecialUnitary, control=control_qubits, control_values=control_values)
      controlled_SU(extra_w[i], wires=acting_qubits)

Here is the last error stack:

/usr/local/lib/python3.11/dist-packages/pennylane/ops/qubit/special_unitary.py in <genexpr>(.0)
    488             # Drop the identity from the generator of matrices
    489             _ = next(matrices)
--> 490             A = sum(t * reduce(np.kron, letters) for t, letters in zip(theta, matrices))
    491         else:
    492             A = qml.math.tensordot(theta, pauli_basis_matrices(num_wires), axes=[[-1], [0]])

/usr/local/lib/python3.11/dist-packages/torch/_tensor.py in __array__(self, dtype)
   1192             return handle_torch_function(Tensor.__array__, (self,), self, dtype=dtype)
   1193         if dtype is None:
-> 1194             return self.numpy()
   1195         else:
   1196             return self.numpy().astype(dtype, copy=False)

RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.

Hi @kekcikon, Welcome to the forum!

Could you provide a minimal working version of your code? It could be helpful so I can see how you set up everything.
I also wondering if you had seen this demo using the special unitary with JAX. Although, I see that you are using Torch. And also this and this past questions from the forum.
For now, I hope this helps. If not, let me know and we’ll keep the conversation going.

Thanks for the answer and links, but it seems none of them solve my problem, although the same error occurs in the first question, but the solution presented there seems too specific to that task.
I would not like to use JAX, since it is part of some neural network and I would like to use the Torch interface. (We have a self-written implementation with JAX)
Below I attach a minimal (non)working piece of code:

import pennylane as qml
import torch
import torch.nn as nn
from pennylane import numpy as np

def func(extra_w, acting_qubits, control_qubits, num_layer):
    def to_int_list(wires):
        return [int(w.item()) if hasattr(w, "item") else int(w) for w in wires]

    acting_qubits = to_int_list(acting_qubits)
    control_qubits= to_int_list(control_qubits)
    measurement_combination = itertools.product('01', repeat = num_layer)

    for i, control_values in enumerate( measurement_combination):
      controlled_SU = qml.ctrl(qml.SpecialUnitary, control=control_qubits, control_values=control_values)
      controlled_SU(extra_w[i], wires=acting_qubits)

@qml.qnode(dev, interface="torch")
def quantum_circuit( extra_w, inputs):

    indexes = [i for i in range(n+features)]
    temp_ind = np.arange(n+features)
    controls = []
    active = []

    for i in range(1,num_layers):
      cntrl = (n//2-i+features, -1)
      temp_ind = np.delete(temp_ind, cntrl)

      func(extra_w, temp_ind.numpy(), control_qubits=[int(n//2-i+features), int(indexes[-1]-i+1)], num_layer=num_layers)
    return [qml.expval(qml.PauliZ(i)) for i in temp_ind.numpy()]

class HybridModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.quantum_layer = quantum_layer
        self.classifier    = nn.Sequential(
            nn.Linear(n, m),
            nn.Softmax(dim=1),
        )

    def forward(self, x):
        batch = x.shape[0]
        x = x.view(batch, pic_size * pic_size)
        x = self.quantum_layer(x)
        return self.classifier(x)

m    = 2
n_pixels = pic_size * pic_size
n = int(np.log2(n_pixels))
num_layers   = 2

dev = qml.device("default.qubit", wires=(n+m))

quantum_layer = qml.qnn.TorchLayer(quantum_circuit, {"extra_w": ((4, 4**(n+m-2)-1)),})

model = HybridModel()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

images = torch.zeros((10, n_pixels))

model.train()
optimizer.zero_grad()
outputs = model(images)

I hope this isn’t too big a piece of code :slight_smile:

Hi @kekcikon ,

I ran your code with pic_size = 2 and features = 2 and I get a different error RuntimeError: shape '[10, -1]' is invalid for input of size 1. It’s hard to know where the error is coming from since your code has a lot going on. It would help if you can simplify it and add comments to explain it. I can see for example that you’re not using your inputs.

If you’re interested in using TorchLayer you can start by running the demo on TorchLayers. You may also find it useful to check out the documentation on TorchLayer and the Torch interface.

Once you have successfully ran the demo on TorchLayers as it is, you can start making small modifications one by one. This will allow you to identify any issues quickly and make it much easier to resolve them.

Let us know if you have any issues running the Torch demo and adapting it.

I hope this helps!

Hi @CatalinaAlbornoz,

I have no problems using Torch with Pennylane without qml.ctrl(qml.SpecialUnitary). I tried to add comments to make the code clearer. I think the problem occurs when trying to create a differentiable controlled gate.

import pennylane as qml
import torch
import torch.nn as nn
from pennylane import numpy as np
import itertools

pic_size = 8 # should be power of 2, >= 8
features   = 2 # >= 2
n_pixels = pic_size * pic_size
n = int(np.log2(n_pixels))
num_layers   = 2 # some const in that case

dev = qml.device("default.qubit", wires=(n+features))

def func(extra_w, acting_qubits, control_qubits, num_layer):
    def to_int_list(wires):
        return [int(w.item()) if hasattr(w, "item") else int(w) for w in wires]

    acting_qubits = to_int_list(acting_qubits)
    control_qubits= to_int_list(control_qubits)
    measurement_combination = itertools.product('01', repeat = num_layer)

    for i, control_values in enumerate( measurement_combination):
      # The problem arises here
      controlled_SU = qml.ctrl(qml.SpecialUnitary, control=control_qubits, control_values=control_values)
      controlled_SU(extra_w[i], wires=acting_qubits)

@qml.qnode(dev, interface="torch")
def quantum_circuit( extra_w, inputs):

    indexes = [i for i in range(n+features)]
    temp_ind = np.arange(n+features)
    controls = []
    active = []
    for i in range(1,num_layers):
      cntrl = (n//2-i+features, -1)
      temp_ind = np.delete(temp_ind, cntrl)

      func(extra_w, temp_ind.numpy(), control_qubits=[int(n//2-i+features), int(indexes[-1]-i+1)], num_layer=num_layers)
    return [qml.expval(qml.PauliZ(i)) for i in temp_ind.numpy()]

class HybridModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.quantum_layer = quantum_layer

    def forward(self, x):
        batch = x.shape[0]
        x = x.view(batch, pic_size * pic_size)
        x = self.quantum_layer(x)
        return smthg

# create Torch quantum layer with only(!!!) func usage
quantum_layer = qml.qnn.TorchLayer(quantum_circuit, {"extra_w": ((4, 4**(n + features -2)-1)),})

model = HybridModel()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# some tensor of images with size (batch_size, pixels_of_pic)
images = torch.zeros((10, n_pixels))

model.train()
optimizer.zero_grad()
outputs = model(images)

Hi!
I talked to the team and the short answer is that SpecialUnitary is not torch compatible at larger qubit numbers.
See the following simplified example:

import pennylane as qml
import torch
import torch.nn as nn
from pennylane import numpy as np

num_wires = 6
theta = 0.1 * torch.tensor(np.arange(2**num_wires, dtype=np.float32), requires_grad=True)

def g(theta):
    return torch.real(qml.SpecialUnitary.compute_matrix(theta, num_wires=num_wires))


_ = torch.autograd.functional.jacobian(g, theta)

Do you have an estimate of the number of qubits you will be using for this application?

I’ll keep you posted about any solutions that we could implement on our side, if possible. For now, that is the current status.

Thanks!