Need to Fuse One and Two Qubit Rotation gates with Noise Channels

Hi all,

I am trying to create custom gates/channel elements by fusing single qubit rotations (e.g, class of pennylane.ops.qubit.parametric_ops such as qml.RX) and two qubit rotations (e.g, class of pennylane.ops.qubit.qchem_ops such as SingleExcitation) with noise channels (e.g, one and two qubit depolarizing channels). I tried using Pennylane tutorials for custom gates[*], but I believe they are outdated and don’t cover my need.

First, I tried the template class as prescribed in [*] by:

@qml.template
def RX_0p001(phi,wires):
    qml.RX(phi, wires)
    qml.DepolarizingChannel(p = 0.001, wires = wires)

But it threw an error saying ‘pennylane’ has no attribute ‘template’.

Then I tried, also based on [*], the following:

class RX_0p001(Operation):
    num_params = 1
    num_wires= AnyWires
    par_domain = "R"
    
    grad_method = "A"
    grad_recipe= None
    
    def expand(self, theta, wires):
        with qml.tape.QuantumTape() as tape:
            qml.RX(theta, wires = wires)
            qml.DepolarizingChannel(p = 0.001, wires = wires)
            
        return tape

Initializing it using

RX_0p001(np.array(2),wires = 0)

yields the output

RX_0p001(tensor(2, requires_grad=True), wires=[0])

But when I try to do anything with it I get an error such as:

dev3 = qml.device('default.mixed', wires=1)


@qml.qnode(dev3)
def depolarizingless_circuit():
    # qml.Hadamard(wires=0)
    qml.RX(np.array(0.7),wires =0)
    return qml.expval(qml.PauliZ(0))
           
@qml.qnode(dev3)
def depolarizing0p001_circuit():
    # qml.Hadamard(wires=0)
    RX_0p001(np.array(0.7),wires =0)
    return qml.expval(qml.PauliZ(0))

It yields the error:
TypeError: RX_0p001.expand() missing 2 required positional arguments: 'theta' and 'wires'

Edit_1:

I also tried the following class:

class  RX_0p001(Operation):
    num_wires = AnyWires  

    def __init__(self, theta, wires, do_queue=True, id=None):
        all_wires = qml.wires.Wires(wires)
        super().__init__(theta, wires=all_wires, do_queue=do_queue, id=id)

    @staticmethod
    def compute_decomposition(theta, wires):
        decomp = []
        
        decomp.append(qml.RX(theta, wires))
        decomp.append(qml.DepolarizingChannel(p = 0.001, wires = wires))

        return decomp

It again gives me the output when I instantiate it with angle and wire:

RX_0p001(tensor(2, requires_grad=True), wires=[0])

But, feeding it to the function

depolarizing0p001_circuit()

yields the following error:


AttributeError Traceback (most recent call last)
Cell In[157], line 1
----> 1 depolarizing0p001_circuit()

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/qnode.py:619, in QNode.call(self, *args, **kwargs)
612 using_custom_cache = (
613 hasattr(cache, “getitem”)
614 and hasattr(cache, “setitem”)
615 and hasattr(cache, “delitem”)
616 )
617 self._tape_cached = using_custom_cache and self.tape.hash in cache
→ 619 res = qml.execute(
620 [self.tape],
621 device=self.device,
622 gradient_fn=self.gradient_fn,
623 interface=self.interface,
624 gradient_kwargs=self.gradient_kwargs,
625 override_shots=override_shots,
626 **self.execute_kwargs,
627 )
629 if autograd.isinstance(res, (tuple, list)) and len(res) == 1:
630 # If a device batch transform was applied, we need to ‘unpack’
631 # the returned tuple/list to a float.
(…)
638 # TODO: find a more explicit way of determining that a batch transform
639 # was applied.
641 res = res[0]

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/interfaces/execution.py:344, in execute(tapes, device, gradient_fn, interface, mode, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform)
340 return batch_fn(res)
342 if gradient_fn == “backprop” or interface is None:
343 return batch_fn(
→ 344 qml.interfaces.cache_execute(
345 batch_execute, cache, return_tuple=False, expand_fn=expand_fn
346 )(tapes)
347 )
349 # the default execution function is batch_execute
350 execute_fn = qml.interfaces.cache_execute(batch_execute, cache, expand_fn=expand_fn)

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/interfaces/execution.py:172, in cache_execute..wrapper(tapes, **kwargs)
168 return (res, ) if return_tuple else res
170 else:
171 # execute all unique tapes that do not exist in the cache
→ 172 res = fn(execution_tapes.values(), **kwargs)
174 final_res =
176 for i, tape in enumerate(tapes):

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/interfaces/execution.py:96, in cache_execute..fn(tapes, **kwargs)
95 def fn(tapes, **kwargs): # pylint: disable=function-redefined
—> 96 tapes = [expand_fn(tape) for tape in tapes]
97 return original_fn(tapes, **kwargs)

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/interfaces/execution.py:96, in (.0)
95 def fn(tapes, **kwargs): # pylint: disable=function-redefined
—> 96 tapes = [expand_fn(tape) for tape in tapes]
97 return original_fn(tapes, **kwargs)

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/interfaces/execution.py:325, in execute..(tape)
322 batch_execute = set_shots(device, override_shots)(device.batch_execute)
324 if expand_fn == “device”:
→ 325 expand_fn = lambda tape: device.expand_fn(tape, max_expansion=max_expansion)
327 if gradient_fn is None:
328 # don’t unwrap if it’s an interface device
329 if “passthru_interface” in device.capabilities():

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/_device.py:680, in Device.expand_fn(self, circuit, max_expansion)
677 if self.custom_expand_fn is not None:
678 return self.custom_expand_fn(circuit, max_expansion=max_expansion)
→ 680 return self.default_expand_fn(circuit, max_expansion=max_expansion)

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/_device.py:655, in Device.default_expand_fn(self, circuit, max_expansion)
652 ops_not_supported = not all(self.stopping_condition(op) for op in circuit.operations)
654 if ops_not_supported or obs_on_same_wire:
→ 655 circuit = circuit.expand(depth=max_expansion, stop_at=self.stopping_condition)
657 return circuit

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/tape/tape.py:610, in QuantumTape.expand(self, depth, stop_at, expand_measurements)
562 def expand(self, depth=1, stop_at=None, expand_measurements=False):
563 “”“Expand all operations in the processed queue to a specific depth.
564
565 Args:
(…)
608 RY(0.2, wires=[‘a’])]
609 “””
→ 610 new_tape = expand_tape(
611 self, depth=depth, stop_at=stop_at, expand_measurements=expand_measurements
612 )
613 new_tape._update()
614 return new_tape

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/tape/tape.py:184, in expand_tape(tape, depth, stop_at, expand_measurements)
181 if isinstance(obj, (qml.operation.Operator, qml.measurements.MeasurementProcess)):
182 # Object is an operation; query it for its expansion
183 try:
→ 184 obj = obj.expand()
185 except DecompositionUndefinedError:
186 # Object does not define an expansion; treat this as
187 # a stopping condition.
188 getattr(new_tape, queue).append(obj)

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/operation.py:1185, in Operator.expand(self)
1182 tape = qml.tape.QuantumTape(do_queue=False)
1184 with tape:
→ 1185 self.decomposition()
1187 if not self.data:
1188 # original operation has no trainable parameters
1189 tape.trainable_params = {}

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/operation.py:1026, in Operator.decomposition(self)
1014 def decomposition(self):
1015 r""“Representation of the operator as a product of other operators.
1016
1017 … math:: O = O_1 O_2 \dots O_n
(…)
1024 list[Operator]: decomposition of the operator
1025 “””
→ 1026 return self.compute_decomposition(
1027 *self.parameters, wires=self.wires, **self.hyperparameters
1028 )

Cell In[154], line 13, in RX_0p001.compute_decomposition(theta, wires)
10 decomp =
12 decomp.append(qml.RX(theta, wires))
—> 13 decomp.append(qml.DepolarizingChannel(p = 0.001, wires = wires))
15 return decomp

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/ops/channel.py:300, in DepolarizingChannel.init(self, p, wires, do_queue, id)
299 def init(self, p, wires, do_queue=True, id=None):
→ 300 super().init(p, wires=wires, do_queue=do_queue, id=id)

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/operation.py:1452, in Operation.init(self, wires, do_queue, id, *params)
1449 def init(self, *params, wires=None, do_queue=True, id=None):
1451 self._inverse = False
→ 1452 super().init(*params, wires=wires, do_queue=do_queue, id=id)
1454 # check the grad_recipe validity
1455 if self.grad_recipe is None:
1456 # Make sure grad_recipe is an iterable of correct length instead of None

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/operation.py:883, in Operator.init(self, wires, do_queue, id, *params)
877 if self.num_wires not in {AllWires, AnyWires} and len(self._wires) != self.num_wires:
878 raise ValueError(
879 f"{self.name}: wrong number of wires. "
880 f"{len(self._wires)} wires given, {self.num_wires} expected."
881 )
→ 883 self._check_batching(params)
885 self.data = list(params) #: list[Any]: parameters of the operator
887 if do_queue:

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/operation.py:903, in Operator._check_batching(self, params)
901 self._batch_size = None
902 try:
→ 903 ndims = tuple(qml.math.ndim(p) for p in params)
904 except ValueError as e:
905 # TODO:[dwierichs] When using tf.function with an input_signature that contains
906 # an unknown-shaped input, ndim() will not be able to determine the number of
(…)
911 # investigated. For now, the batch_size is left to be None when instantiating
912 # an operation with abstract parameters that make qml.math.ndim fail.
913 if any(qml.math.is_abstract(p) for p in params):

File ~/JaxQuantumML/lib/python3.10/site-packages/pennylane/operation.py:903, in (.0)
901 self._batch_size = None
902 try:
→ 903 ndims = tuple(qml.math.ndim(p) for p in params)
904 except ValueError as e:
905 # TODO:[dwierichs] When using tf.function with an input_signature that contains
906 # an unknown-shaped input, ndim() will not be able to determine the number of
(…)
911 # investigated. For now, the batch_size is left to be None when instantiating
912 # an operation with abstract parameters that make qml.math.ndim fail.
913 if any(qml.math.is_abstract(p) for p in params):

File ~/JaxQuantumML/lib/python3.10/site-packages/autoray/autoray.py:79, in do(fn, like, *args, **kwargs)
30 “”“Do function named fn on (*args, **kwargs), peforming single
31 dispatch to retrieve fn based on whichever library defines the class of
32 the args[0], or the like keyword argument if specified.
(…)
76 <tf.Tensor: id=91, shape=(3, 3), dtype=float32>
77 “””
78 backend = choose_backend(fn, *args, like=like, **kwargs)
—> 79 return get_lib_fn(backend, fn)(*args, **kwargs)

File ~/JaxQuantumML/lib/python3.10/site-packages/autoray/autoray.py:886, in ndim(x)
884 @compose
885 def ndim(x):
→ 886 return x.ndim

AttributeError: ‘float’ object has no attribute ‘ndim’

My goal is to be able to compute gradients out of each of these fused block elements so that I can add them into circuits within an operator pool. Using a noiseless operator pool I’m benchmarking Adapt VQE, and trying to use that noisy operator pool for Adapt VQE under noise.

I would appreciate your help in somehow creating these custom gates/channels.

Best,
Onur

[*]How to add custom gates and templates to PennyLane | PennyLane Blog

Hey @Onur_Danaci! Welcome to the forum :smile:!

It looks like you don’t need to define expand here; this works for me:

import pennylane as qml
from pennylane import numpy as np

class RX_0p001(qml.operation.Operation):
    num_params = 1
    num_wires = qml.operation.AnyWires
    par_domain = "R"

    grad_method = "A"
    grad_recipe = None

    @staticmethod
    def compute_decomposition(theta, wires):
        decomp = []
        
        decomp.append(qml.RX(theta, wires))
        decomp.append(qml.DepolarizingChannel(p = 0.001, wires = wires))

        return decomp

dev3 = qml.device("default.mixed", wires=1)


@qml.qnode(dev3)
def depolarizingless_circuit():
    qml.RX(np.array(0.7), wires=0)
    return qml.expval(qml.PauliZ(0))


@qml.qnode(dev3)
def depolarizing0p001_circuit():
    RX_0p001(np.array(0.7), wires=0)
    return qml.expval(qml.PauliZ(0))
>>> depolarizingless_circuit()
tensor(0.76484219, requires_grad=True)
>>> depolarizing0p001_circuit()
tensor(0.7638224, requires_grad=True)

Is your PennyLane version out of date? We’re on v0.31 currently :slight_smile:, which is what I’m using. Let me know if that helps!